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