1 /* 2 * Copyright (C) 2014 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 package android.app; 17 18 import android.animation.Animator; 19 import android.animation.AnimatorListenerAdapter; 20 import android.animation.ObjectAnimator; 21 import android.annotation.NonNull; 22 import android.app.SharedElementCallback.OnSharedElementsReadyListener; 23 import android.content.Intent; 24 import android.graphics.Color; 25 import android.graphics.Matrix; 26 import android.graphics.RectF; 27 import android.graphics.drawable.ColorDrawable; 28 import android.graphics.drawable.Drawable; 29 import android.os.Build.VERSION_CODES; 30 import android.os.Bundle; 31 import android.os.Handler; 32 import android.os.Message; 33 import android.os.ResultReceiver; 34 import android.transition.Transition; 35 import android.transition.TransitionListenerAdapter; 36 import android.transition.TransitionManager; 37 import android.view.View; 38 import android.view.ViewGroup; 39 import android.view.Window; 40 41 import com.android.internal.view.OneShotPreDrawListener; 42 43 import java.util.ArrayList; 44 45 /** 46 * This ActivityTransitionCoordinator is created in ActivityOptions#makeSceneTransitionAnimation 47 * to govern the exit of the Scene and the shared elements when calling an Activity as well as 48 * the reentry of the Scene when coming back from the called Activity. 49 * 50 * @hide 51 */ 52 public class ExitTransitionCoordinator extends ActivityTransitionCoordinator { 53 private static final String TAG = "ExitTransitionCoordinator"; 54 static long sMaxWaitMillis = 1000; 55 56 private Bundle mSharedElementBundle; 57 private boolean mExitNotified; 58 private boolean mSharedElementNotified; 59 private ExitTransitionCallbacks mExitCallbacks; 60 private boolean mIsBackgroundReady; 61 private boolean mIsCanceled; 62 private Handler mHandler; 63 private ObjectAnimator mBackgroundAnimator; 64 private boolean mIsHidden; 65 private Bundle mExitSharedElementBundle; 66 private boolean mIsExitStarted; 67 private boolean mSharedElementsHidden; 68 ExitTransitionCoordinator(ExitTransitionCallbacks exitCallbacks, Window window, SharedElementCallback listener, ArrayList<String> names, ArrayList<String> accepted, ArrayList<View> mapped, boolean isReturning)69 public ExitTransitionCoordinator(ExitTransitionCallbacks exitCallbacks, 70 Window window, SharedElementCallback listener, ArrayList<String> names, 71 ArrayList<String> accepted, ArrayList<View> mapped, boolean isReturning) { 72 super(window, names, listener, isReturning); 73 viewsReady(mapSharedElements(accepted, mapped)); 74 stripOffscreenViews(); 75 mIsBackgroundReady = !isReturning; 76 mExitCallbacks = exitCallbacks; 77 } 78 79 @Override onReceiveResult(int resultCode, Bundle resultData)80 protected void onReceiveResult(int resultCode, Bundle resultData) { 81 switch (resultCode) { 82 case MSG_SET_REMOTE_RECEIVER: 83 stopCancel(); 84 mResultReceiver = resultData.getParcelable(KEY_REMOTE_RECEIVER, android.os.ResultReceiver.class); 85 if (mIsCanceled) { 86 mResultReceiver.send(MSG_CANCEL, null); 87 mResultReceiver = null; 88 } else { 89 notifyComplete(); 90 } 91 break; 92 case MSG_HIDE_SHARED_ELEMENTS: 93 stopCancel(); 94 if (!mIsCanceled) { 95 hideSharedElements(); 96 } 97 break; 98 case MSG_START_EXIT_TRANSITION: 99 mHandler.removeMessages(MSG_CANCEL); 100 startExit(); 101 break; 102 case MSG_SHARED_ELEMENT_DESTINATION: 103 mExitSharedElementBundle = resultData; 104 sharedElementExitBack(); 105 break; 106 case MSG_CANCEL: 107 mIsCanceled = true; 108 finish(); 109 break; 110 } 111 } 112 stopCancel()113 private void stopCancel() { 114 if (mHandler != null) { 115 mHandler.removeMessages(MSG_CANCEL); 116 } 117 } 118 delayCancel()119 private void delayCancel() { 120 if (mHandler != null) { 121 mHandler.sendEmptyMessageDelayed(MSG_CANCEL, sMaxWaitMillis); 122 } 123 } 124 resetViews()125 public void resetViews() { 126 ViewGroup decorView = getDecor(); 127 if (decorView != null) { 128 TransitionManager.endTransitions(decorView); 129 } 130 if (mTransitioningViews != null) { 131 showViews(mTransitioningViews, true); 132 setTransitioningViewsVisiblity(View.VISIBLE, true); 133 } 134 showViews(mSharedElements, true); 135 mIsHidden = true; 136 if (!mIsReturning && decorView != null) { 137 decorView.suppressLayout(false); 138 } 139 moveSharedElementsFromOverlay(); 140 clearState(); 141 } 142 sharedElementExitBack()143 private void sharedElementExitBack() { 144 final ViewGroup decorView = getDecor(); 145 if (decorView != null) { 146 decorView.suppressLayout(true); 147 } 148 if (decorView != null && mExitSharedElementBundle != null && 149 !mExitSharedElementBundle.isEmpty() && 150 !mSharedElements.isEmpty() && getSharedElementTransition() != null) { 151 startTransition(new Runnable() { 152 public void run() { 153 startSharedElementExit(decorView); 154 } 155 }); 156 } else { 157 sharedElementTransitionComplete(); 158 } 159 } 160 startSharedElementExit(final ViewGroup decorView)161 private void startSharedElementExit(final ViewGroup decorView) { 162 Transition transition = getSharedElementExitTransition(); 163 transition.addListener(new TransitionListenerAdapter() { 164 @Override 165 public void onTransitionEnd(Transition transition) { 166 transition.removeListener(this); 167 if (isViewsTransitionComplete()) { 168 delayCancel(); 169 } 170 } 171 }); 172 final ArrayList<View> sharedElementSnapshots = createSnapshots(mExitSharedElementBundle, 173 mSharedElementNames); 174 OneShotPreDrawListener.add(decorView, () -> { 175 setSharedElementState(mExitSharedElementBundle, sharedElementSnapshots); 176 }); 177 setGhostVisibility(View.INVISIBLE); 178 scheduleGhostVisibilityChange(View.INVISIBLE); 179 if (mListener != null) { 180 mListener.onSharedElementEnd(mSharedElementNames, mSharedElements, 181 sharedElementSnapshots); 182 } 183 TransitionManager.beginDelayedTransition(decorView, transition); 184 scheduleGhostVisibilityChange(View.VISIBLE); 185 setGhostVisibility(View.VISIBLE); 186 decorView.invalidate(); 187 } 188 hideSharedElements()189 private void hideSharedElements() { 190 moveSharedElementsFromOverlay(); 191 if (mExitCallbacks != null) { 192 mExitCallbacks.hideSharedElements(); 193 } 194 if (!mIsHidden) { 195 hideViews(mSharedElements); 196 } 197 mSharedElementsHidden = true; 198 finishIfNecessary(); 199 } 200 startExit()201 public void startExit() { 202 if (!mIsExitStarted) { 203 backgroundAnimatorComplete(); 204 mIsExitStarted = true; 205 pauseInput(); 206 ViewGroup decorView = getDecor(); 207 if (decorView != null) { 208 decorView.suppressLayout(true); 209 } 210 moveSharedElementsToOverlay(); 211 startTransition(this::beginTransitions); 212 } 213 } 214 215 /** 216 * Starts the exit animation and sends back the activity result 217 */ startExit(Activity activity)218 public void startExit(Activity activity) { 219 int resultCode = activity.mResultCode; 220 Intent data = activity.mResultData; 221 if (!mIsExitStarted) { 222 mIsExitStarted = true; 223 pauseInput(); 224 ViewGroup decorView = getDecor(); 225 if (decorView != null) { 226 decorView.suppressLayout(true); 227 } 228 mHandler = new Handler() { 229 @Override 230 public void handleMessage(Message msg) { 231 mIsCanceled = true; 232 finish(); 233 } 234 }; 235 delayCancel(); 236 moveSharedElementsToOverlay(); 237 if (decorView != null && decorView.getBackground() == null) { 238 getWindow().setBackgroundDrawable(new ColorDrawable(Color.TRANSPARENT)); 239 } 240 final boolean targetsM = decorView == null || decorView.getContext() 241 .getApplicationInfo().targetSdkVersion >= VERSION_CODES.M; 242 ArrayList<String> sharedElementNames = targetsM ? mSharedElementNames : 243 mAllSharedElementNames; 244 ActivityOptions options = ActivityOptions.makeSceneTransitionAnimation(activity, this, 245 sharedElementNames, resultCode, data); 246 activity.convertToTranslucent(new Activity.TranslucentConversionListener() { 247 @Override 248 public void onTranslucentConversionComplete(boolean drawComplete) { 249 if (!mIsCanceled) { 250 fadeOutBackground(); 251 } 252 } 253 }, options); 254 startTransition(this::startExitTransition); 255 } 256 } 257 258 /** 259 * Called from {@link Activity#onStop()} 260 */ stop(Activity activity)261 public void stop(Activity activity) { 262 if (mIsReturning && mExitCallbacks != null) { 263 // Override the previous ActivityOptions. We don't want the 264 // activity to have options since we're essentially canceling the 265 // transition and finishing right now. 266 activity.convertToTranslucent(null, null); 267 finish(); 268 } 269 } 270 startExitTransition()271 private void startExitTransition() { 272 Transition transition = getExitTransition(); 273 ViewGroup decorView = getDecor(); 274 if (transition != null && decorView != null && mTransitioningViews != null) { 275 setTransitioningViewsVisiblity(View.VISIBLE, false); 276 TransitionManager.beginDelayedTransition(decorView, transition); 277 setTransitioningViewsVisiblity(View.INVISIBLE, false); 278 decorView.invalidate(); 279 } else { 280 transitionStarted(); 281 } 282 } 283 fadeOutBackground()284 private void fadeOutBackground() { 285 if (mBackgroundAnimator == null) { 286 ViewGroup decor = getDecor(); 287 Drawable background; 288 if (decor != null && (background = decor.getBackground()) != null) { 289 background = background.mutate(); 290 getWindow().setBackgroundDrawable(background); 291 mBackgroundAnimator = ObjectAnimator.ofInt(background, "alpha", 0); 292 mBackgroundAnimator.addListener(new AnimatorListenerAdapter() { 293 @Override 294 public void onAnimationEnd(Animator animation) { 295 mBackgroundAnimator = null; 296 if (!mIsCanceled) { 297 mIsBackgroundReady = true; 298 notifyComplete(); 299 } 300 backgroundAnimatorComplete(); 301 } 302 }); 303 mBackgroundAnimator.setDuration(getFadeDuration()); 304 mBackgroundAnimator.start(); 305 } else { 306 backgroundAnimatorComplete(); 307 mIsBackgroundReady = true; 308 } 309 } 310 } 311 getExitTransition()312 private Transition getExitTransition() { 313 Transition viewsTransition = null; 314 if (mTransitioningViews != null && !mTransitioningViews.isEmpty()) { 315 viewsTransition = configureTransition(getViewsTransition(), true); 316 removeExcludedViews(viewsTransition, mTransitioningViews); 317 if (mTransitioningViews.isEmpty()) { 318 viewsTransition = null; 319 } 320 } 321 if (viewsTransition == null) { 322 viewsTransitionComplete(); 323 } else { 324 final ArrayList<View> transitioningViews = mTransitioningViews; 325 viewsTransition.addListener(new ContinueTransitionListener() { 326 @Override 327 public void onTransitionEnd(Transition transition) { 328 viewsTransitionComplete(); 329 if (mIsHidden && transitioningViews != null) { 330 showViews(transitioningViews, true); 331 setTransitioningViewsVisiblity(View.VISIBLE, true); 332 } 333 if (mSharedElementBundle != null) { 334 delayCancel(); 335 } 336 super.onTransitionEnd(transition); 337 } 338 }); 339 } 340 return viewsTransition; 341 } 342 getSharedElementExitTransition()343 private Transition getSharedElementExitTransition() { 344 Transition sharedElementTransition = null; 345 if (!mSharedElements.isEmpty()) { 346 sharedElementTransition = configureTransition(getSharedElementTransition(), false); 347 } 348 if (sharedElementTransition == null) { 349 sharedElementTransitionComplete(); 350 } else { 351 sharedElementTransition.addListener(new ContinueTransitionListener() { 352 @Override 353 public void onTransitionEnd(Transition transition) { 354 sharedElementTransitionComplete(); 355 if (mIsHidden) { 356 showViews(mSharedElements, true); 357 } 358 super.onTransitionEnd(transition); 359 } 360 }); 361 mSharedElements.get(0).invalidate(); 362 } 363 return sharedElementTransition; 364 } 365 beginTransitions()366 private void beginTransitions() { 367 Transition sharedElementTransition = getSharedElementExitTransition(); 368 Transition viewsTransition = getExitTransition(); 369 370 Transition transition = mergeTransitions(sharedElementTransition, viewsTransition); 371 ViewGroup decorView = getDecor(); 372 if (transition != null && decorView != null) { 373 setGhostVisibility(View.INVISIBLE); 374 scheduleGhostVisibilityChange(View.INVISIBLE); 375 if (viewsTransition != null) { 376 setTransitioningViewsVisiblity(View.VISIBLE, false); 377 } 378 TransitionManager.beginDelayedTransition(decorView, transition); 379 scheduleGhostVisibilityChange(View.VISIBLE); 380 setGhostVisibility(View.VISIBLE); 381 if (viewsTransition != null) { 382 setTransitioningViewsVisiblity(View.INVISIBLE, false); 383 } 384 decorView.invalidate(); 385 } else { 386 transitionStarted(); 387 } 388 } 389 isReadyToNotify()390 protected boolean isReadyToNotify() { 391 return mSharedElementBundle != null && mResultReceiver != null && mIsBackgroundReady; 392 } 393 394 @Override sharedElementTransitionComplete()395 protected void sharedElementTransitionComplete() { 396 mSharedElementBundle = mExitSharedElementBundle == null 397 ? captureSharedElementState() : captureExitSharedElementsState(); 398 super.sharedElementTransitionComplete(); 399 } 400 captureExitSharedElementsState()401 private Bundle captureExitSharedElementsState() { 402 Bundle bundle = new Bundle(); 403 RectF bounds = new RectF(); 404 Matrix matrix = new Matrix(); 405 for (int i = 0; i < mSharedElements.size(); i++) { 406 String name = mSharedElementNames.get(i); 407 Bundle sharedElementState = mExitSharedElementBundle.getBundle(name); 408 if (sharedElementState != null) { 409 bundle.putBundle(name, sharedElementState); 410 } else { 411 View view = mSharedElements.get(i); 412 captureSharedElementState(view, name, bundle, matrix, bounds); 413 } 414 } 415 return bundle; 416 } 417 418 @Override onTransitionsComplete()419 protected void onTransitionsComplete() { 420 notifyComplete(); 421 } 422 notifyComplete()423 protected void notifyComplete() { 424 if (isReadyToNotify()) { 425 if (!mSharedElementNotified) { 426 mSharedElementNotified = true; 427 delayCancel(); 428 429 if (mExitCallbacks != null && mExitCallbacks.isReturnTransitionAllowed()) { 430 mResultReceiver.send(MSG_ALLOW_RETURN_TRANSITION, null); 431 } 432 433 if (mListener == null) { 434 mResultReceiver.send(MSG_TAKE_SHARED_ELEMENTS, mSharedElementBundle); 435 notifyExitComplete(); 436 } else { 437 final ResultReceiver resultReceiver = mResultReceiver; 438 final Bundle sharedElementBundle = mSharedElementBundle; 439 mListener.onSharedElementsArrived(mSharedElementNames, mSharedElements, 440 new OnSharedElementsReadyListener() { 441 @Override 442 public void onSharedElementsReady() { 443 resultReceiver.send(MSG_TAKE_SHARED_ELEMENTS, 444 sharedElementBundle); 445 notifyExitComplete(); 446 } 447 }); 448 } 449 } else { 450 notifyExitComplete(); 451 } 452 } 453 } 454 notifyExitComplete()455 private void notifyExitComplete() { 456 if (!mExitNotified && isViewsTransitionComplete()) { 457 mExitNotified = true; 458 mResultReceiver.send(MSG_EXIT_TRANSITION_COMPLETE, null); 459 mResultReceiver = null; // done talking 460 ViewGroup decorView = getDecor(); 461 if (!mIsReturning && decorView != null) { 462 decorView.suppressLayout(false); 463 } 464 finishIfNecessary(); 465 } 466 } 467 finishIfNecessary()468 private void finishIfNecessary() { 469 if (mIsReturning && mExitNotified && mExitCallbacks != null && (mSharedElements.isEmpty() 470 || mSharedElementsHidden)) { 471 finish(); 472 } 473 } 474 finish()475 private void finish() { 476 stopCancel(); 477 if (mExitCallbacks != null) { 478 mExitCallbacks.onFinish(); 479 mExitCallbacks = null; 480 } 481 // Clear the state so that we can't hold any references accidentally and leak memory. 482 clearState(); 483 } 484 485 @Override clearState()486 protected void clearState() { 487 mHandler = null; 488 mSharedElementBundle = null; 489 if (mBackgroundAnimator != null) { 490 mBackgroundAnimator.cancel(); 491 mBackgroundAnimator = null; 492 } 493 mExitSharedElementBundle = null; 494 super.clearState(); 495 } 496 497 @Override moveSharedElementWithParent()498 protected boolean moveSharedElementWithParent() { 499 return !mIsReturning; 500 } 501 502 @Override getViewsTransition()503 protected Transition getViewsTransition() { 504 if (mIsReturning) { 505 return getWindow().getReturnTransition(); 506 } else { 507 return getWindow().getExitTransition(); 508 } 509 } 510 getSharedElementTransition()511 protected Transition getSharedElementTransition() { 512 if (mIsReturning) { 513 return getWindow().getSharedElementReturnTransition(); 514 } else { 515 return getWindow().getSharedElementExitTransition(); 516 } 517 } 518 519 /** 520 * @hide 521 */ 522 public interface ExitTransitionCallbacks { 523 524 /** 525 * Returns true if reverse exit animation is supported 526 */ isReturnTransitionAllowed()527 boolean isReturnTransitionAllowed(); 528 529 /** 530 * Called then the transition finishes 531 */ onFinish()532 void onFinish(); 533 534 /** 535 * Optional callback when the transition is hiding elements in the source surface 536 */ hideSharedElements()537 default void hideSharedElements() { }; 538 } 539 540 /** 541 * @hide 542 */ 543 public static class ActivityExitTransitionCallbacks implements ExitTransitionCallbacks { 544 545 @NonNull 546 final Activity mActivity; 547 ActivityExitTransitionCallbacks(@onNull Activity activity)548 ActivityExitTransitionCallbacks(@NonNull Activity activity) { 549 mActivity = activity; 550 } 551 552 @Override isReturnTransitionAllowed()553 public boolean isReturnTransitionAllowed() { 554 return true; 555 } 556 557 @Override onFinish()558 public void onFinish() { 559 mActivity.mActivityTransitionState.clear(); 560 mActivity.finish(); 561 mActivity.overridePendingTransition(0, 0); 562 } 563 } 564 } 565