EnterTransitionCoordinator.java revision e0c37bdea37f78778f6c4f23f03604e59dfb0d55
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 */ 16package android.app; 17 18import android.animation.Animator; 19import android.animation.AnimatorListenerAdapter; 20import android.animation.ObjectAnimator; 21import android.app.SharedElementCallback.OnSharedElementsReadyListener; 22import android.graphics.drawable.Drawable; 23import android.os.Bundle; 24import android.os.ResultReceiver; 25import android.text.TextUtils; 26import android.transition.Transition; 27import android.transition.TransitionListenerAdapter; 28import android.transition.TransitionManager; 29import android.util.ArrayMap; 30import android.view.View; 31import android.view.ViewGroup; 32import android.view.ViewGroupOverlay; 33import android.view.ViewTreeObserver; 34import android.view.Window; 35import android.view.accessibility.AccessibilityEvent; 36 37import com.android.internal.view.OneShotPreDrawListener; 38 39import java.util.ArrayList; 40 41/** 42 * This ActivityTransitionCoordinator is created by the Activity to manage 43 * the enter scene and shared element transfer into the Scene, either during 44 * launch of an Activity or returning from a launched Activity. 45 */ 46class EnterTransitionCoordinator extends ActivityTransitionCoordinator { 47 private static final String TAG = "EnterTransitionCoordinator"; 48 49 private static final int MIN_ANIMATION_FRAMES = 2; 50 51 private boolean mSharedElementTransitionStarted; 52 private Activity mActivity; 53 private boolean mHasStopped; 54 private boolean mIsCanceled; 55 private ObjectAnimator mBackgroundAnimator; 56 private boolean mIsExitTransitionComplete; 57 private boolean mIsReadyForTransition; 58 private Bundle mSharedElementsBundle; 59 private boolean mWasOpaque; 60 private boolean mAreViewsReady; 61 private boolean mIsViewsTransitionStarted; 62 private Transition mEnterViewsTransition; 63 private OneShotPreDrawListener mViewsReadyListener; 64 private final boolean mIsCrossTask; 65 66 public EnterTransitionCoordinator(Activity activity, ResultReceiver resultReceiver, 67 ArrayList<String> sharedElementNames, boolean isReturning, boolean isCrossTask) { 68 super(activity.getWindow(), sharedElementNames, 69 getListener(activity, isReturning && !isCrossTask), isReturning); 70 mActivity = activity; 71 mIsCrossTask = isCrossTask; 72 setResultReceiver(resultReceiver); 73 prepareEnter(); 74 Bundle resultReceiverBundle = new Bundle(); 75 resultReceiverBundle.putParcelable(KEY_REMOTE_RECEIVER, this); 76 mResultReceiver.send(MSG_SET_REMOTE_RECEIVER, resultReceiverBundle); 77 final View decorView = getDecor(); 78 if (decorView != null) { 79 final ViewTreeObserver viewTreeObserver = decorView.getViewTreeObserver(); 80 viewTreeObserver.addOnPreDrawListener( 81 new ViewTreeObserver.OnPreDrawListener() { 82 @Override 83 public boolean onPreDraw() { 84 if (mIsReadyForTransition) { 85 if (viewTreeObserver.isAlive()) { 86 viewTreeObserver.removeOnPreDrawListener(this); 87 } else { 88 decorView.getViewTreeObserver().removeOnPreDrawListener(this); 89 } 90 } 91 return mIsReadyForTransition; 92 } 93 }); 94 } 95 } 96 97 boolean isCrossTask() { 98 return mIsCrossTask; 99 } 100 101 public void viewInstancesReady(ArrayList<String> accepted, ArrayList<String> localNames, 102 ArrayList<View> localViews) { 103 boolean remap = false; 104 for (int i = 0; i < localViews.size(); i++) { 105 View view = localViews.get(i); 106 if (!TextUtils.equals(view.getTransitionName(), localNames.get(i)) 107 || !view.isAttachedToWindow()) { 108 remap = true; 109 break; 110 } 111 } 112 if (remap) { 113 triggerViewsReady(mapNamedElements(accepted, localNames)); 114 } else { 115 triggerViewsReady(mapSharedElements(accepted, localViews)); 116 } 117 } 118 119 public void namedViewsReady(ArrayList<String> accepted, ArrayList<String> localNames) { 120 triggerViewsReady(mapNamedElements(accepted, localNames)); 121 } 122 123 public Transition getEnterViewsTransition() { 124 return mEnterViewsTransition; 125 } 126 127 @Override 128 protected void viewsReady(ArrayMap<String, View> sharedElements) { 129 super.viewsReady(sharedElements); 130 mIsReadyForTransition = true; 131 hideViews(mSharedElements); 132 if (getViewsTransition() != null && mTransitioningViews != null) { 133 stripOffscreenViews(); 134 hideViews(mTransitioningViews); 135 } 136 if (mIsReturning) { 137 sendSharedElementDestination(); 138 } else { 139 moveSharedElementsToOverlay(); 140 } 141 if (mSharedElementsBundle != null) { 142 onTakeSharedElements(); 143 } 144 } 145 146 private void triggerViewsReady(final ArrayMap<String, View> sharedElements) { 147 if (mAreViewsReady) { 148 return; 149 } 150 mAreViewsReady = true; 151 final ViewGroup decor = getDecor(); 152 // Ensure the views have been laid out before capturing the views -- we need the epicenter. 153 if (decor == null || (decor.isAttachedToWindow() && 154 (sharedElements.isEmpty() || !sharedElements.valueAt(0).isLayoutRequested()))) { 155 viewsReady(sharedElements); 156 } else { 157 mViewsReadyListener = OneShotPreDrawListener.add(decor, () -> { 158 mViewsReadyListener = null; 159 viewsReady(sharedElements); 160 }); 161 decor.invalidate(); 162 } 163 } 164 165 private ArrayMap<String, View> mapNamedElements(ArrayList<String> accepted, 166 ArrayList<String> localNames) { 167 ArrayMap<String, View> sharedElements = new ArrayMap<String, View>(); 168 ViewGroup decorView = getDecor(); 169 if (decorView != null) { 170 decorView.findNamedViews(sharedElements); 171 } 172 if (accepted != null) { 173 for (int i = 0; i < localNames.size(); i++) { 174 String localName = localNames.get(i); 175 String acceptedName = accepted.get(i); 176 if (localName != null && !localName.equals(acceptedName)) { 177 View view = sharedElements.get(localName); 178 if (view != null) { 179 sharedElements.put(acceptedName, view); 180 } 181 } 182 } 183 } 184 return sharedElements; 185 } 186 187 private void sendSharedElementDestination() { 188 boolean allReady; 189 final View decorView = getDecor(); 190 if (allowOverlappingTransitions() && getEnterViewsTransition() != null) { 191 allReady = false; 192 } else if (decorView == null) { 193 allReady = true; 194 } else { 195 allReady = !decorView.isLayoutRequested(); 196 if (allReady) { 197 for (int i = 0; i < mSharedElements.size(); i++) { 198 if (mSharedElements.get(i).isLayoutRequested()) { 199 allReady = false; 200 break; 201 } 202 } 203 } 204 } 205 if (allReady) { 206 Bundle state = captureSharedElementState(); 207 moveSharedElementsToOverlay(); 208 mResultReceiver.send(MSG_SHARED_ELEMENT_DESTINATION, state); 209 } else if (decorView != null) { 210 OneShotPreDrawListener.add(decorView, () -> { 211 if (mResultReceiver != null) { 212 Bundle state = captureSharedElementState(); 213 moveSharedElementsToOverlay(); 214 mResultReceiver.send(MSG_SHARED_ELEMENT_DESTINATION, state); 215 } 216 }); 217 } 218 if (allowOverlappingTransitions()) { 219 startEnterTransitionOnly(); 220 } 221 } 222 223 private static SharedElementCallback getListener(Activity activity, boolean isReturning) { 224 return isReturning ? activity.mExitTransitionListener : activity.mEnterTransitionListener; 225 } 226 227 @Override 228 protected void onReceiveResult(int resultCode, Bundle resultData) { 229 switch (resultCode) { 230 case MSG_TAKE_SHARED_ELEMENTS: 231 if (!mIsCanceled) { 232 mSharedElementsBundle = resultData; 233 onTakeSharedElements(); 234 } 235 break; 236 case MSG_EXIT_TRANSITION_COMPLETE: 237 if (!mIsCanceled) { 238 mIsExitTransitionComplete = true; 239 if (mSharedElementTransitionStarted) { 240 onRemoteExitTransitionComplete(); 241 } 242 } 243 break; 244 case MSG_CANCEL: 245 cancel(); 246 break; 247 } 248 } 249 250 public boolean isWaitingForRemoteExit() { 251 return mIsReturning && mResultReceiver != null; 252 } 253 254 /** 255 * This is called onResume. If an Activity is resuming and the transitions 256 * haven't started yet, force the views to appear. This is likely to be 257 * caused by the top Activity finishing before the transitions started. 258 * In that case, we can finish any transition that was started, but we 259 * should cancel any pending transition and just bring those Views visible. 260 */ 261 public void forceViewsToAppear() { 262 if (!mIsReturning) { 263 return; 264 } 265 if (!mIsReadyForTransition) { 266 mIsReadyForTransition = true; 267 final ViewGroup decor = getDecor(); 268 if (decor != null && mViewsReadyListener != null) { 269 mViewsReadyListener.removeListener(); 270 mViewsReadyListener = null; 271 } 272 showViews(mTransitioningViews, true); 273 setTransitioningViewsVisiblity(View.VISIBLE, true); 274 mSharedElements.clear(); 275 mAllSharedElementNames.clear(); 276 mTransitioningViews.clear(); 277 mIsReadyForTransition = true; 278 viewsTransitionComplete(); 279 sharedElementTransitionComplete(); 280 } else { 281 if (!mSharedElementTransitionStarted) { 282 moveSharedElementsFromOverlay(); 283 mSharedElementTransitionStarted = true; 284 showViews(mSharedElements, true); 285 mSharedElements.clear(); 286 sharedElementTransitionComplete(); 287 } 288 if (!mIsViewsTransitionStarted) { 289 mIsViewsTransitionStarted = true; 290 showViews(mTransitioningViews, true); 291 setTransitioningViewsVisiblity(View.VISIBLE, true); 292 mTransitioningViews.clear(); 293 viewsTransitionComplete(); 294 } 295 cancelPendingTransitions(); 296 } 297 mAreViewsReady = true; 298 if (mResultReceiver != null) { 299 mResultReceiver.send(MSG_CANCEL, null); 300 mResultReceiver = null; 301 } 302 } 303 304 private void cancel() { 305 if (!mIsCanceled) { 306 mIsCanceled = true; 307 if (getViewsTransition() == null || mIsViewsTransitionStarted) { 308 showViews(mSharedElements, true); 309 } else if (mTransitioningViews != null) { 310 mTransitioningViews.addAll(mSharedElements); 311 } 312 moveSharedElementsFromOverlay(); 313 mSharedElementNames.clear(); 314 mSharedElements.clear(); 315 mAllSharedElementNames.clear(); 316 startSharedElementTransition(null); 317 onRemoteExitTransitionComplete(); 318 } 319 } 320 321 public boolean isReturning() { 322 return mIsReturning; 323 } 324 325 protected void prepareEnter() { 326 ViewGroup decorView = getDecor(); 327 if (mActivity == null || decorView == null) { 328 return; 329 } 330 if (!isCrossTask()) { 331 mActivity.overridePendingTransition(0, 0); 332 } 333 if (!mIsReturning) { 334 mWasOpaque = mActivity.convertToTranslucent(null, null); 335 Drawable background = decorView.getBackground(); 336 if (background != null) { 337 getWindow().setBackgroundDrawable(null); 338 background = background.mutate(); 339 background.setAlpha(0); 340 getWindow().setBackgroundDrawable(background); 341 } 342 } else { 343 mActivity = null; // all done with it now. 344 } 345 } 346 347 @Override 348 protected Transition getViewsTransition() { 349 Window window = getWindow(); 350 if (window == null) { 351 return null; 352 } 353 if (mIsReturning) { 354 return window.getReenterTransition(); 355 } else { 356 return window.getEnterTransition(); 357 } 358 } 359 360 protected Transition getSharedElementTransition() { 361 Window window = getWindow(); 362 if (window == null) { 363 return null; 364 } 365 if (mIsReturning) { 366 return window.getSharedElementReenterTransition(); 367 } else { 368 return window.getSharedElementEnterTransition(); 369 } 370 } 371 372 private void startSharedElementTransition(Bundle sharedElementState) { 373 ViewGroup decorView = getDecor(); 374 if (decorView == null) { 375 return; 376 } 377 // Remove rejected shared elements 378 ArrayList<String> rejectedNames = new ArrayList<String>(mAllSharedElementNames); 379 rejectedNames.removeAll(mSharedElementNames); 380 ArrayList<View> rejectedSnapshots = createSnapshots(sharedElementState, rejectedNames); 381 if (mListener != null) { 382 mListener.onRejectSharedElements(rejectedSnapshots); 383 } 384 removeNullViews(rejectedSnapshots); 385 startRejectedAnimations(rejectedSnapshots); 386 387 // Now start shared element transition 388 ArrayList<View> sharedElementSnapshots = createSnapshots(sharedElementState, 389 mSharedElementNames); 390 showViews(mSharedElements, true); 391 scheduleSetSharedElementEnd(sharedElementSnapshots); 392 ArrayList<SharedElementOriginalState> originalImageViewState = 393 setSharedElementState(sharedElementState, sharedElementSnapshots); 394 requestLayoutForSharedElements(); 395 396 boolean startEnterTransition = allowOverlappingTransitions() && !mIsReturning; 397 boolean startSharedElementTransition = true; 398 setGhostVisibility(View.INVISIBLE); 399 scheduleGhostVisibilityChange(View.INVISIBLE); 400 pauseInput(); 401 Transition transition = beginTransition(decorView, startEnterTransition, 402 startSharedElementTransition); 403 scheduleGhostVisibilityChange(View.VISIBLE); 404 setGhostVisibility(View.VISIBLE); 405 406 if (startEnterTransition) { 407 startEnterTransition(transition); 408 } 409 410 setOriginalSharedElementState(mSharedElements, originalImageViewState); 411 412 if (mResultReceiver != null) { 413 // We can't trust that the view will disappear on the same frame that the shared 414 // element appears here. Assure that we get at least 2 frames for double-buffering. 415 decorView.postOnAnimation(new Runnable() { 416 int mAnimations; 417 418 @Override 419 public void run() { 420 if (mAnimations++ < MIN_ANIMATION_FRAMES) { 421 View decorView = getDecor(); 422 if (decorView != null) { 423 decorView.postOnAnimation(this); 424 } 425 } else if (mResultReceiver != null) { 426 mResultReceiver.send(MSG_HIDE_SHARED_ELEMENTS, null); 427 mResultReceiver = null; // all done sending messages. 428 } 429 } 430 }); 431 } 432 } 433 434 private static void removeNullViews(ArrayList<View> views) { 435 if (views != null) { 436 for (int i = views.size() - 1; i >= 0; i--) { 437 if (views.get(i) == null) { 438 views.remove(i); 439 } 440 } 441 } 442 } 443 444 private void onTakeSharedElements() { 445 if (!mIsReadyForTransition || mSharedElementsBundle == null) { 446 return; 447 } 448 final Bundle sharedElementState = mSharedElementsBundle; 449 mSharedElementsBundle = null; 450 OnSharedElementsReadyListener listener = new OnSharedElementsReadyListener() { 451 @Override 452 public void onSharedElementsReady() { 453 final View decorView = getDecor(); 454 if (decorView != null) { 455 OneShotPreDrawListener.add(decorView, () -> { 456 startTransition(() -> { 457 startSharedElementTransition(sharedElementState); 458 }); 459 }); 460 decorView.invalidate(); 461 } 462 } 463 }; 464 if (mListener == null) { 465 listener.onSharedElementsReady(); 466 } else { 467 mListener.onSharedElementsArrived(mSharedElementNames, mSharedElements, listener); 468 } 469 } 470 471 private void requestLayoutForSharedElements() { 472 int numSharedElements = mSharedElements.size(); 473 for (int i = 0; i < numSharedElements; i++) { 474 mSharedElements.get(i).requestLayout(); 475 } 476 } 477 478 private Transition beginTransition(ViewGroup decorView, boolean startEnterTransition, 479 boolean startSharedElementTransition) { 480 Transition sharedElementTransition = null; 481 if (startSharedElementTransition) { 482 if (!mSharedElementNames.isEmpty()) { 483 sharedElementTransition = configureTransition(getSharedElementTransition(), false); 484 } 485 if (sharedElementTransition == null) { 486 sharedElementTransitionStarted(); 487 sharedElementTransitionComplete(); 488 } else { 489 sharedElementTransition.addListener(new TransitionListenerAdapter() { 490 @Override 491 public void onTransitionStart(Transition transition) { 492 sharedElementTransitionStarted(); 493 } 494 495 @Override 496 public void onTransitionEnd(Transition transition) { 497 transition.removeListener(this); 498 sharedElementTransitionComplete(); 499 } 500 }); 501 } 502 } 503 Transition viewsTransition = null; 504 if (startEnterTransition) { 505 mIsViewsTransitionStarted = true; 506 if (mTransitioningViews != null && !mTransitioningViews.isEmpty()) { 507 viewsTransition = configureTransition(getViewsTransition(), true); 508 } 509 if (viewsTransition == null) { 510 viewsTransitionComplete(); 511 } else { 512 final ArrayList<View> transitioningViews = mTransitioningViews; 513 viewsTransition.addListener(new ContinueTransitionListener() { 514 @Override 515 public void onTransitionStart(Transition transition) { 516 mEnterViewsTransition = transition; 517 if (transitioningViews != null) { 518 showViews(transitioningViews, false); 519 } 520 super.onTransitionStart(transition); 521 } 522 523 @Override 524 public void onTransitionEnd(Transition transition) { 525 mEnterViewsTransition = null; 526 transition.removeListener(this); 527 viewsTransitionComplete(); 528 super.onTransitionEnd(transition); 529 } 530 }); 531 } 532 } 533 534 Transition transition = mergeTransitions(sharedElementTransition, viewsTransition); 535 if (transition != null) { 536 transition.addListener(new ContinueTransitionListener()); 537 if (startEnterTransition) { 538 setTransitioningViewsVisiblity(View.INVISIBLE, false); 539 } 540 TransitionManager.beginDelayedTransition(decorView, transition); 541 if (startEnterTransition) { 542 setTransitioningViewsVisiblity(View.VISIBLE, false); 543 } 544 decorView.invalidate(); 545 } else { 546 transitionStarted(); 547 } 548 return transition; 549 } 550 551 @Override 552 protected void onTransitionsComplete() { 553 moveSharedElementsFromOverlay(); 554 final ViewGroup decorView = getDecor(); 555 if (decorView != null) { 556 decorView.sendAccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED); 557 } 558 } 559 560 private void sharedElementTransitionStarted() { 561 mSharedElementTransitionStarted = true; 562 if (mIsExitTransitionComplete) { 563 send(MSG_EXIT_TRANSITION_COMPLETE, null); 564 } 565 } 566 567 private void startEnterTransition(Transition transition) { 568 ViewGroup decorView = getDecor(); 569 if (!mIsReturning && decorView != null) { 570 Drawable background = decorView.getBackground(); 571 if (background != null) { 572 background = background.mutate(); 573 getWindow().setBackgroundDrawable(background); 574 mBackgroundAnimator = ObjectAnimator.ofInt(background, "alpha", 255); 575 mBackgroundAnimator.setDuration(getFadeDuration()); 576 mBackgroundAnimator.addListener(new AnimatorListenerAdapter() { 577 @Override 578 public void onAnimationEnd(Animator animation) { 579 makeOpaque(); 580 } 581 }); 582 mBackgroundAnimator.start(); 583 } else if (transition != null) { 584 transition.addListener(new TransitionListenerAdapter() { 585 @Override 586 public void onTransitionEnd(Transition transition) { 587 transition.removeListener(this); 588 makeOpaque(); 589 } 590 }); 591 } else { 592 makeOpaque(); 593 } 594 } 595 } 596 597 public void stop() { 598 // Restore the background to its previous state since the 599 // Activity is stopping. 600 if (mBackgroundAnimator != null) { 601 mBackgroundAnimator.end(); 602 mBackgroundAnimator = null; 603 } else if (mWasOpaque) { 604 ViewGroup decorView = getDecor(); 605 if (decorView != null) { 606 Drawable drawable = decorView.getBackground(); 607 if (drawable != null) { 608 drawable.setAlpha(1); 609 } 610 } 611 } 612 makeOpaque(); 613 mIsCanceled = true; 614 mResultReceiver = null; 615 mActivity = null; 616 moveSharedElementsFromOverlay(); 617 if (mTransitioningViews != null) { 618 showViews(mTransitioningViews, true); 619 setTransitioningViewsVisiblity(View.VISIBLE, true); 620 } 621 showViews(mSharedElements, true); 622 clearState(); 623 } 624 625 /** 626 * Cancels the enter transition. 627 * @return True if the enter transition is still pending capturing the target state. If so, 628 * any transition started on the decor will do nothing. 629 */ 630 public boolean cancelEnter() { 631 setGhostVisibility(View.INVISIBLE); 632 mHasStopped = true; 633 mIsCanceled = true; 634 clearState(); 635 return super.cancelPendingTransitions(); 636 } 637 638 @Override 639 protected void clearState() { 640 mSharedElementsBundle = null; 641 mEnterViewsTransition = null; 642 mResultReceiver = null; 643 if (mBackgroundAnimator != null) { 644 mBackgroundAnimator.cancel(); 645 mBackgroundAnimator = null; 646 } 647 super.clearState(); 648 } 649 650 private void makeOpaque() { 651 if (!mHasStopped && mActivity != null) { 652 if (mWasOpaque) { 653 mActivity.convertFromTranslucent(); 654 } 655 mActivity = null; 656 } 657 } 658 659 private boolean allowOverlappingTransitions() { 660 return mIsReturning ? getWindow().getAllowReturnTransitionOverlap() 661 : getWindow().getAllowEnterTransitionOverlap(); 662 } 663 664 private void startRejectedAnimations(final ArrayList<View> rejectedSnapshots) { 665 if (rejectedSnapshots == null || rejectedSnapshots.isEmpty()) { 666 return; 667 } 668 final ViewGroup decorView = getDecor(); 669 if (decorView != null) { 670 ViewGroupOverlay overlay = decorView.getOverlay(); 671 ObjectAnimator animator = null; 672 int numRejected = rejectedSnapshots.size(); 673 for (int i = 0; i < numRejected; i++) { 674 View snapshot = rejectedSnapshots.get(i); 675 overlay.add(snapshot); 676 animator = ObjectAnimator.ofFloat(snapshot, View.ALPHA, 1, 0); 677 animator.start(); 678 } 679 animator.addListener(new AnimatorListenerAdapter() { 680 @Override 681 public void onAnimationEnd(Animator animation) { 682 ViewGroupOverlay overlay = decorView.getOverlay(); 683 int numRejected = rejectedSnapshots.size(); 684 for (int i = 0; i < numRejected; i++) { 685 overlay.remove(rejectedSnapshots.get(i)); 686 } 687 } 688 }); 689 } 690 } 691 692 protected void onRemoteExitTransitionComplete() { 693 if (!allowOverlappingTransitions()) { 694 startEnterTransitionOnly(); 695 } 696 } 697 698 private void startEnterTransitionOnly() { 699 startTransition(new Runnable() { 700 @Override 701 public void run() { 702 boolean startEnterTransition = true; 703 boolean startSharedElementTransition = false; 704 ViewGroup decorView = getDecor(); 705 if (decorView != null) { 706 Transition transition = beginTransition(decorView, startEnterTransition, 707 startSharedElementTransition); 708 startEnterTransition(transition); 709 } 710 } 711 }); 712 } 713} 714