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