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