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