EnterTransitionCoordinator.java revision 333b8093eaab4b55d91a7a0b3b1484424f8ac975
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 Transition mEnterViewsTransition; 61 62 public EnterTransitionCoordinator(Activity activity, ResultReceiver resultReceiver, 63 ArrayList<String> sharedElementNames, boolean isReturning) { 64 super(activity.getWindow(), sharedElementNames, 65 getListener(activity, isReturning), isReturning); 66 mActivity = activity; 67 setResultReceiver(resultReceiver); 68 prepareEnter(); 69 Bundle resultReceiverBundle = new Bundle(); 70 resultReceiverBundle.putParcelable(KEY_REMOTE_RECEIVER, this); 71 mResultReceiver.send(MSG_SET_REMOTE_RECEIVER, resultReceiverBundle); 72 final View decorView = getDecor(); 73 if (decorView != null) { 74 decorView.getViewTreeObserver().addOnPreDrawListener( 75 new ViewTreeObserver.OnPreDrawListener() { 76 @Override 77 public boolean onPreDraw() { 78 if (mIsReadyForTransition) { 79 decorView.getViewTreeObserver().removeOnPreDrawListener(this); 80 } 81 return mIsReadyForTransition; 82 } 83 }); 84 } 85 } 86 87 public void viewInstancesReady(ArrayList<String> accepted, ArrayList<String> localNames, 88 ArrayList<View> localViews) { 89 boolean remap = false; 90 for (int i = 0; i < localViews.size(); i++) { 91 View view = localViews.get(i); 92 if (!TextUtils.equals(view.getTransitionName(), localNames.get(i)) 93 || !view.isAttachedToWindow()) { 94 remap = true; 95 break; 96 } 97 } 98 if (remap) { 99 triggerViewsReady(mapNamedElements(accepted, localNames)); 100 } else { 101 triggerViewsReady(mapSharedElements(accepted, localViews)); 102 } 103 } 104 105 public void namedViewsReady(ArrayList<String> accepted, ArrayList<String> localNames) { 106 triggerViewsReady(mapNamedElements(accepted, localNames)); 107 } 108 109 public Transition getEnterViewsTransition() { 110 return mEnterViewsTransition; 111 } 112 113 @Override 114 protected void viewsReady(ArrayMap<String, View> sharedElements) { 115 super.viewsReady(sharedElements); 116 mIsReadyForTransition = true; 117 hideViews(mSharedElements); 118 if (getViewsTransition() != null && mTransitioningViews != null) { 119 hideViews(mTransitioningViews); 120 } 121 if (mIsReturning) { 122 sendSharedElementDestination(); 123 } else { 124 moveSharedElementsToOverlay(); 125 } 126 if (mSharedElementsBundle != null) { 127 onTakeSharedElements(); 128 } 129 } 130 131 private void triggerViewsReady(final ArrayMap<String, View> sharedElements) { 132 if (mAreViewsReady) { 133 return; 134 } 135 mAreViewsReady = true; 136 // Ensure the views have been laid out before capturing the views -- we need the epicenter. 137 if (sharedElements.isEmpty() || !sharedElements.valueAt(0).isLayoutRequested()) { 138 viewsReady(sharedElements); 139 } else { 140 final View sharedElement = sharedElements.valueAt(0); 141 sharedElement.getViewTreeObserver() 142 .addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() { 143 @Override 144 public boolean onPreDraw() { 145 sharedElement.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 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 startRejectedAnimations(rejectedSnapshots); 322 323 // Now start shared element transition 324 ArrayList<View> sharedElementSnapshots = createSnapshots(sharedElementState, 325 mSharedElementNames); 326 showViews(mSharedElements, true); 327 scheduleSetSharedElementEnd(sharedElementSnapshots); 328 ArrayList<SharedElementOriginalState> originalImageViewState = 329 setSharedElementState(sharedElementState, sharedElementSnapshots); 330 requestLayoutForSharedElements(); 331 332 boolean startEnterTransition = allowOverlappingTransitions() && !mIsReturning; 333 boolean startSharedElementTransition = true; 334 setGhostVisibility(View.INVISIBLE); 335 scheduleGhostVisibilityChange(View.INVISIBLE); 336 Transition transition = beginTransition(decorView, startEnterTransition, 337 startSharedElementTransition); 338 scheduleGhostVisibilityChange(View.VISIBLE); 339 setGhostVisibility(View.VISIBLE); 340 341 if (startEnterTransition) { 342 startEnterTransition(transition); 343 } 344 345 setOriginalSharedElementState(mSharedElements, originalImageViewState); 346 347 if (mResultReceiver != null) { 348 // We can't trust that the view will disappear on the same frame that the shared 349 // element appears here. Assure that we get at least 2 frames for double-buffering. 350 decorView.postOnAnimation(new Runnable() { 351 int mAnimations; 352 353 @Override 354 public void run() { 355 if (mAnimations++ < MIN_ANIMATION_FRAMES) { 356 View decorView = getDecor(); 357 if (decorView != null) { 358 decorView.postOnAnimation(this); 359 } 360 } else if (mResultReceiver != null) { 361 mResultReceiver.send(MSG_HIDE_SHARED_ELEMENTS, null); 362 mResultReceiver = null; // all done sending messages. 363 } 364 } 365 }); 366 } 367 } 368 369 private void onTakeSharedElements() { 370 if (!mIsReadyForTransition || mSharedElementsBundle == null) { 371 return; 372 } 373 final Bundle sharedElementState = mSharedElementsBundle; 374 mSharedElementsBundle = null; 375 final View decorView = getDecor(); 376 if (decorView != null) { 377 decorView.getViewTreeObserver() 378 .addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() { 379 @Override 380 public boolean onPreDraw() { 381 decorView.getViewTreeObserver().removeOnPreDrawListener(this); 382 startTransition(new Runnable() { 383 @Override 384 public void run() { 385 startSharedElementTransition(sharedElementState); 386 } 387 }); 388 return false; 389 } 390 }); 391 decorView.invalidate(); 392 } 393 } 394 395 private void requestLayoutForSharedElements() { 396 int numSharedElements = mSharedElements.size(); 397 for (int i = 0; i < numSharedElements; i++) { 398 mSharedElements.get(i).requestLayout(); 399 } 400 } 401 402 private Transition beginTransition(ViewGroup decorView, boolean startEnterTransition, 403 boolean startSharedElementTransition) { 404 Transition sharedElementTransition = null; 405 if (startSharedElementTransition) { 406 if (!mSharedElementNames.isEmpty()) { 407 sharedElementTransition = configureTransition(getSharedElementTransition(), false); 408 } 409 if (sharedElementTransition == null) { 410 sharedElementTransitionStarted(); 411 sharedElementTransitionComplete(); 412 } else { 413 sharedElementTransition.addListener(new Transition.TransitionListenerAdapter() { 414 @Override 415 public void onTransitionStart(Transition transition) { 416 sharedElementTransitionStarted(); 417 } 418 419 @Override 420 public void onTransitionEnd(Transition transition) { 421 transition.removeListener(this); 422 sharedElementTransitionComplete(); 423 } 424 }); 425 } 426 } 427 Transition viewsTransition = null; 428 if (startEnterTransition) { 429 mIsViewsTransitionStarted = true; 430 if (mTransitioningViews != null && !mTransitioningViews.isEmpty()) { 431 viewsTransition = configureTransition(getViewsTransition(), true); 432 if (viewsTransition != null && !mIsReturning) { 433 stripOffscreenViews(); 434 } 435 } 436 if (viewsTransition == null) { 437 viewTransitionComplete(); 438 } else { 439 viewsTransition.forceVisibility(View.INVISIBLE, true); 440 final ArrayList<View> transitioningViews = mTransitioningViews; 441 viewsTransition.addListener(new ContinueTransitionListener() { 442 @Override 443 public void onTransitionStart(Transition transition) { 444 mEnterViewsTransition = transition; 445 if (transitioningViews != null) { 446 showViews(transitioningViews, false); 447 } 448 super.onTransitionStart(transition); 449 } 450 451 @Override 452 public void onTransitionEnd(Transition transition) { 453 mEnterViewsTransition = null; 454 transition.removeListener(this); 455 viewTransitionComplete(); 456 super.onTransitionEnd(transition); 457 } 458 }); 459 } 460 } 461 462 Transition transition = mergeTransitions(sharedElementTransition, viewsTransition); 463 if (transition != null) { 464 transition.addListener(new ContinueTransitionListener()); 465 TransitionManager.beginDelayedTransition(decorView, transition); 466 if (startSharedElementTransition && !mSharedElementNames.isEmpty()) { 467 mSharedElements.get(0).invalidate(); 468 } else if (startEnterTransition && mTransitioningViews != null && 469 !mTransitioningViews.isEmpty()) { 470 mTransitioningViews.get(0).invalidate(); 471 } 472 } else { 473 transitionStarted(); 474 } 475 return transition; 476 } 477 478 private void viewTransitionComplete() { 479 mIsViewsTransitionComplete = true; 480 if (mIsSharedElementTransitionComplete) { 481 moveSharedElementsFromOverlay(); 482 } 483 } 484 485 private void sharedElementTransitionComplete() { 486 mIsSharedElementTransitionComplete = true; 487 if (mIsViewsTransitionComplete) { 488 moveSharedElementsFromOverlay(); 489 } 490 } 491 492 private void sharedElementTransitionStarted() { 493 mSharedElementTransitionStarted = true; 494 if (mIsExitTransitionComplete) { 495 send(MSG_EXIT_TRANSITION_COMPLETE, null); 496 } 497 } 498 499 private void startEnterTransition(Transition transition) { 500 ViewGroup decorView = getDecor(); 501 if (!mIsReturning && decorView != null) { 502 Drawable background = decorView.getBackground(); 503 if (background != null) { 504 background = background.mutate(); 505 getWindow().setBackgroundDrawable(background); 506 mBackgroundAnimator = ObjectAnimator.ofInt(background, "alpha", 255); 507 mBackgroundAnimator.setDuration(getFadeDuration()); 508 mBackgroundAnimator.addListener(new AnimatorListenerAdapter() { 509 @Override 510 public void onAnimationEnd(Animator animation) { 511 makeOpaque(); 512 } 513 }); 514 mBackgroundAnimator.start(); 515 } else if (transition != null) { 516 transition.addListener(new Transition.TransitionListenerAdapter() { 517 @Override 518 public void onTransitionEnd(Transition transition) { 519 transition.removeListener(this); 520 makeOpaque(); 521 } 522 }); 523 } else { 524 makeOpaque(); 525 } 526 } 527 } 528 529 public void stop() { 530 // Restore the background to its previous state since the 531 // Activity is stopping. 532 if (mBackgroundAnimator != null) { 533 mBackgroundAnimator.end(); 534 mBackgroundAnimator = null; 535 } else if (mWasOpaque) { 536 ViewGroup decorView = getDecor(); 537 if (decorView != null) { 538 Drawable drawable = decorView.getBackground(); 539 if (drawable != null) { 540 drawable.setAlpha(1); 541 } 542 } 543 } 544 makeOpaque(); 545 mIsCanceled = true; 546 mResultReceiver = null; 547 mActivity = null; 548 moveSharedElementsFromOverlay(); 549 if (mTransitioningViews != null) { 550 showViews(mTransitioningViews, true); 551 } 552 showViews(mSharedElements, true); 553 clearState(); 554 } 555 556 public void cancelEnter() { 557 setGhostVisibility(View.INVISIBLE); 558 mHasStopped = true; 559 mIsCanceled = true; 560 mResultReceiver = null; 561 if (mBackgroundAnimator != null) { 562 mBackgroundAnimator.cancel(); 563 mBackgroundAnimator = null; 564 } 565 mActivity = null; 566 clearState(); 567 } 568 569 private void makeOpaque() { 570 if (!mHasStopped && mActivity != null) { 571 if (mWasOpaque) { 572 mActivity.convertFromTranslucent(); 573 } 574 mActivity = null; 575 } 576 } 577 578 private boolean allowOverlappingTransitions() { 579 return mIsReturning ? getWindow().getAllowExitTransitionOverlap() 580 : getWindow().getAllowEnterTransitionOverlap(); 581 } 582 583 private void startRejectedAnimations(final ArrayList<View> rejectedSnapshots) { 584 if (rejectedSnapshots == null || rejectedSnapshots.isEmpty()) { 585 return; 586 } 587 final ViewGroup decorView = getDecor(); 588 if (decorView != null) { 589 ViewGroupOverlay overlay = decorView.getOverlay(); 590 ObjectAnimator animator = null; 591 int numRejected = rejectedSnapshots.size(); 592 for (int i = 0; i < numRejected; i++) { 593 View snapshot = rejectedSnapshots.get(i); 594 overlay.add(snapshot); 595 animator = ObjectAnimator.ofFloat(snapshot, View.ALPHA, 1, 0); 596 animator.start(); 597 } 598 animator.addListener(new AnimatorListenerAdapter() { 599 @Override 600 public void onAnimationEnd(Animator animation) { 601 ViewGroupOverlay overlay = decorView.getOverlay(); 602 int numRejected = rejectedSnapshots.size(); 603 for (int i = 0; i < numRejected; i++) { 604 overlay.remove(rejectedSnapshots.get(i)); 605 } 606 } 607 }); 608 } 609 } 610 611 protected void onRemoteExitTransitionComplete() { 612 if (!allowOverlappingTransitions()) { 613 startEnterTransitionOnly(); 614 } 615 } 616 617 private void startEnterTransitionOnly() { 618 startTransition(new Runnable() { 619 @Override 620 public void run() { 621 boolean startEnterTransition = true; 622 boolean startSharedElementTransition = false; 623 ViewGroup decorView = getDecor(); 624 if (decorView != null) { 625 Transition transition = beginTransition(decorView, startEnterTransition, 626 startSharedElementTransition); 627 startEnterTransition(transition); 628 } 629 } 630 }); 631 } 632 633} 634