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 */ 16 package android.app; 17 18 import android.animation.Animator; 19 import android.animation.AnimatorListenerAdapter; 20 import android.animation.ObjectAnimator; 21 import android.graphics.drawable.Drawable; 22 import android.os.Bundle; 23 import android.os.ResultReceiver; 24 import android.text.TextUtils; 25 import android.transition.Transition; 26 import android.transition.TransitionManager; 27 import android.util.ArrayMap; 28 import android.view.View; 29 import android.view.ViewGroup; 30 import android.view.ViewGroupOverlay; 31 import android.view.ViewTreeObserver; 32 import android.view.Window; 33 34 import java.util.ArrayList; 35 36 /** 37 * This ActivityTransitionCoordinator is created by the Activity to manage 38 * the enter scene and shared element transfer into the Scene, either during 39 * launch of an Activity or returning from a launched Activity. 40 */ 41 class EnterTransitionCoordinator extends ActivityTransitionCoordinator { 42 private static final String TAG = "EnterTransitionCoordinator"; 43 44 private static final int MIN_ANIMATION_FRAMES = 2; 45 46 private boolean mSharedElementTransitionStarted; 47 private Activity mActivity; 48 private boolean mHasStopped; 49 private boolean mIsCanceled; 50 private ObjectAnimator mBackgroundAnimator; 51 private boolean mIsExitTransitionComplete; 52 private boolean mIsReadyForTransition; 53 private Bundle mSharedElementsBundle; 54 private boolean mWasOpaque; 55 private boolean mAreViewsReady; 56 private boolean mIsViewsTransitionStarted; 57 private boolean mIsViewsTransitionComplete; 58 private boolean mIsSharedElementTransitionComplete; 59 private Transition mEnterViewsTransition; 60 EnterTransitionCoordinator(Activity activity, ResultReceiver resultReceiver, ArrayList<String> sharedElementNames, boolean isReturning)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 viewInstancesReady(ArrayList<String> accepted, ArrayList<String> localNames, ArrayList<View> localViews)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 namedViewsReady(ArrayList<String> accepted, ArrayList<String> localNames)104 public void namedViewsReady(ArrayList<String> accepted, ArrayList<String> localNames) { 105 triggerViewsReady(mapNamedElements(accepted, localNames)); 106 } 107 getEnterViewsTransition()108 public Transition getEnterViewsTransition() { 109 return mEnterViewsTransition; 110 } 111 112 @Override viewsReady(ArrayMap<String, View> sharedElements)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 triggerViewsReady(final ArrayMap<String, View> sharedElements)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 mapNamedElements(ArrayList<String> accepted, ArrayList<String> localNames)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 sendSharedElementDestination()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 getListener(Activity activity, boolean isReturning)217 private static SharedElementCallback getListener(Activity activity, boolean isReturning) { 218 return isReturning ? activity.mExitTransitionListener : activity.mEnterTransitionListener; 219 } 220 221 @Override onReceiveResult(int resultCode, Bundle resultData)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 cancel()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 isReturning()260 public boolean isReturning() { 261 return mIsReturning; 262 } 263 prepareEnter()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 getViewsTransition()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 getSharedElementTransition()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 startSharedElementTransition(Bundle sharedElementState)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 Transition transition = beginTransition(decorView, startEnterTransition, 338 startSharedElementTransition); 339 scheduleGhostVisibilityChange(View.VISIBLE); 340 setGhostVisibility(View.VISIBLE); 341 342 if (startEnterTransition) { 343 startEnterTransition(transition); 344 } 345 346 setOriginalSharedElementState(mSharedElements, originalImageViewState); 347 348 if (mResultReceiver != null) { 349 // We can't trust that the view will disappear on the same frame that the shared 350 // element appears here. Assure that we get at least 2 frames for double-buffering. 351 decorView.postOnAnimation(new Runnable() { 352 int mAnimations; 353 354 @Override 355 public void run() { 356 if (mAnimations++ < MIN_ANIMATION_FRAMES) { 357 View decorView = getDecor(); 358 if (decorView != null) { 359 decorView.postOnAnimation(this); 360 } 361 } else if (mResultReceiver != null) { 362 mResultReceiver.send(MSG_HIDE_SHARED_ELEMENTS, null); 363 mResultReceiver = null; // all done sending messages. 364 } 365 } 366 }); 367 } 368 } 369 removeNullViews(ArrayList<View> views)370 private static void removeNullViews(ArrayList<View> views) { 371 if (views != null) { 372 for (int i = views.size() - 1; i >= 0; i--) { 373 if (views.get(i) == null) { 374 views.remove(i); 375 } 376 } 377 } 378 } 379 onTakeSharedElements()380 private void onTakeSharedElements() { 381 if (!mIsReadyForTransition || mSharedElementsBundle == null) { 382 return; 383 } 384 final Bundle sharedElementState = mSharedElementsBundle; 385 mSharedElementsBundle = null; 386 final View decorView = getDecor(); 387 if (decorView != null) { 388 decorView.getViewTreeObserver() 389 .addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() { 390 @Override 391 public boolean onPreDraw() { 392 decorView.getViewTreeObserver().removeOnPreDrawListener(this); 393 startTransition(new Runnable() { 394 @Override 395 public void run() { 396 startSharedElementTransition(sharedElementState); 397 } 398 }); 399 return false; 400 } 401 }); 402 decorView.invalidate(); 403 } 404 } 405 requestLayoutForSharedElements()406 private void requestLayoutForSharedElements() { 407 int numSharedElements = mSharedElements.size(); 408 for (int i = 0; i < numSharedElements; i++) { 409 mSharedElements.get(i).requestLayout(); 410 } 411 } 412 beginTransition(ViewGroup decorView, boolean startEnterTransition, boolean startSharedElementTransition)413 private Transition beginTransition(ViewGroup decorView, boolean startEnterTransition, 414 boolean startSharedElementTransition) { 415 Transition sharedElementTransition = null; 416 if (startSharedElementTransition) { 417 if (!mSharedElementNames.isEmpty()) { 418 sharedElementTransition = configureTransition(getSharedElementTransition(), false); 419 } 420 if (sharedElementTransition == null) { 421 sharedElementTransitionStarted(); 422 sharedElementTransitionComplete(); 423 } else { 424 sharedElementTransition.addListener(new Transition.TransitionListenerAdapter() { 425 @Override 426 public void onTransitionStart(Transition transition) { 427 sharedElementTransitionStarted(); 428 } 429 430 @Override 431 public void onTransitionEnd(Transition transition) { 432 transition.removeListener(this); 433 sharedElementTransitionComplete(); 434 } 435 }); 436 } 437 } 438 Transition viewsTransition = null; 439 if (startEnterTransition) { 440 mIsViewsTransitionStarted = true; 441 if (mTransitioningViews != null && !mTransitioningViews.isEmpty()) { 442 viewsTransition = configureTransition(getViewsTransition(), true); 443 if (viewsTransition != null && !mIsReturning) { 444 stripOffscreenViews(); 445 } 446 } 447 if (viewsTransition == null) { 448 viewTransitionComplete(); 449 } else { 450 viewsTransition.forceVisibility(View.INVISIBLE, true); 451 final ArrayList<View> transitioningViews = mTransitioningViews; 452 viewsTransition.addListener(new ContinueTransitionListener() { 453 @Override 454 public void onTransitionStart(Transition transition) { 455 mEnterViewsTransition = transition; 456 if (transitioningViews != null) { 457 showViews(transitioningViews, false); 458 } 459 super.onTransitionStart(transition); 460 } 461 462 @Override 463 public void onTransitionEnd(Transition transition) { 464 mEnterViewsTransition = null; 465 transition.removeListener(this); 466 viewTransitionComplete(); 467 super.onTransitionEnd(transition); 468 } 469 }); 470 } 471 } 472 473 Transition transition = mergeTransitions(sharedElementTransition, viewsTransition); 474 if (transition != null) { 475 transition.addListener(new ContinueTransitionListener()); 476 TransitionManager.beginDelayedTransition(decorView, transition); 477 if (startSharedElementTransition && !mSharedElementNames.isEmpty()) { 478 mSharedElements.get(0).invalidate(); 479 } else if (startEnterTransition && mTransitioningViews != null && 480 !mTransitioningViews.isEmpty()) { 481 mTransitioningViews.get(0).invalidate(); 482 } 483 } else { 484 transitionStarted(); 485 } 486 return transition; 487 } 488 viewTransitionComplete()489 private void viewTransitionComplete() { 490 mIsViewsTransitionComplete = true; 491 if (mIsSharedElementTransitionComplete) { 492 moveSharedElementsFromOverlay(); 493 } 494 } 495 sharedElementTransitionComplete()496 private void sharedElementTransitionComplete() { 497 mIsSharedElementTransitionComplete = true; 498 if (mIsViewsTransitionComplete) { 499 moveSharedElementsFromOverlay(); 500 } 501 } 502 sharedElementTransitionStarted()503 private void sharedElementTransitionStarted() { 504 mSharedElementTransitionStarted = true; 505 if (mIsExitTransitionComplete) { 506 send(MSG_EXIT_TRANSITION_COMPLETE, null); 507 } 508 } 509 startEnterTransition(Transition transition)510 private void startEnterTransition(Transition transition) { 511 ViewGroup decorView = getDecor(); 512 if (!mIsReturning && decorView != null) { 513 Drawable background = decorView.getBackground(); 514 if (background != null) { 515 background = background.mutate(); 516 getWindow().setBackgroundDrawable(background); 517 mBackgroundAnimator = ObjectAnimator.ofInt(background, "alpha", 255); 518 mBackgroundAnimator.setDuration(getFadeDuration()); 519 mBackgroundAnimator.addListener(new AnimatorListenerAdapter() { 520 @Override 521 public void onAnimationEnd(Animator animation) { 522 makeOpaque(); 523 } 524 }); 525 mBackgroundAnimator.start(); 526 } else if (transition != null) { 527 transition.addListener(new Transition.TransitionListenerAdapter() { 528 @Override 529 public void onTransitionEnd(Transition transition) { 530 transition.removeListener(this); 531 makeOpaque(); 532 } 533 }); 534 } else { 535 makeOpaque(); 536 } 537 } 538 } 539 stop()540 public void stop() { 541 // Restore the background to its previous state since the 542 // Activity is stopping. 543 if (mBackgroundAnimator != null) { 544 mBackgroundAnimator.end(); 545 mBackgroundAnimator = null; 546 } else if (mWasOpaque) { 547 ViewGroup decorView = getDecor(); 548 if (decorView != null) { 549 Drawable drawable = decorView.getBackground(); 550 if (drawable != null) { 551 drawable.setAlpha(1); 552 } 553 } 554 } 555 makeOpaque(); 556 mIsCanceled = true; 557 mResultReceiver = null; 558 mActivity = null; 559 moveSharedElementsFromOverlay(); 560 if (mTransitioningViews != null) { 561 showViews(mTransitioningViews, true); 562 } 563 showViews(mSharedElements, true); 564 clearState(); 565 } 566 567 /** 568 * Cancels the enter transition. 569 * @return True if the enter transition is still pending capturing the target state. If so, 570 * any transition started on the decor will do nothing. 571 */ cancelEnter()572 public boolean cancelEnter() { 573 setGhostVisibility(View.INVISIBLE); 574 mHasStopped = true; 575 mIsCanceled = true; 576 mResultReceiver = null; 577 if (mBackgroundAnimator != null) { 578 mBackgroundAnimator.cancel(); 579 mBackgroundAnimator = null; 580 } 581 mActivity = null; 582 clearState(); 583 return super.cancelPendingTransitions(); 584 } 585 makeOpaque()586 private void makeOpaque() { 587 if (!mHasStopped && mActivity != null) { 588 if (mWasOpaque) { 589 mActivity.convertFromTranslucent(); 590 } 591 mActivity = null; 592 } 593 } 594 allowOverlappingTransitions()595 private boolean allowOverlappingTransitions() { 596 return mIsReturning ? getWindow().getAllowExitTransitionOverlap() 597 : getWindow().getAllowEnterTransitionOverlap(); 598 } 599 startRejectedAnimations(final ArrayList<View> rejectedSnapshots)600 private void startRejectedAnimations(final ArrayList<View> rejectedSnapshots) { 601 if (rejectedSnapshots == null || rejectedSnapshots.isEmpty()) { 602 return; 603 } 604 final ViewGroup decorView = getDecor(); 605 if (decorView != null) { 606 ViewGroupOverlay overlay = decorView.getOverlay(); 607 ObjectAnimator animator = null; 608 int numRejected = rejectedSnapshots.size(); 609 for (int i = 0; i < numRejected; i++) { 610 View snapshot = rejectedSnapshots.get(i); 611 overlay.add(snapshot); 612 animator = ObjectAnimator.ofFloat(snapshot, View.ALPHA, 1, 0); 613 animator.start(); 614 } 615 animator.addListener(new AnimatorListenerAdapter() { 616 @Override 617 public void onAnimationEnd(Animator animation) { 618 ViewGroupOverlay overlay = decorView.getOverlay(); 619 int numRejected = rejectedSnapshots.size(); 620 for (int i = 0; i < numRejected; i++) { 621 overlay.remove(rejectedSnapshots.get(i)); 622 } 623 } 624 }); 625 } 626 } 627 onRemoteExitTransitionComplete()628 protected void onRemoteExitTransitionComplete() { 629 if (!allowOverlappingTransitions()) { 630 startEnterTransitionOnly(); 631 } 632 } 633 startEnterTransitionOnly()634 private void startEnterTransitionOnly() { 635 startTransition(new Runnable() { 636 @Override 637 public void run() { 638 boolean startEnterTransition = true; 639 boolean startSharedElementTransition = false; 640 ViewGroup decorView = getDecor(); 641 if (decorView != null) { 642 Transition transition = beginTransition(decorView, startEnterTransition, 643 startSharedElementTransition); 644 startEnterTransition(transition); 645 } 646 } 647 }); 648 } 649 650 } 651