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.app.SharedElementCallback.OnSharedElementsReadyListener; 22 import android.graphics.drawable.Drawable; 23 import android.os.Bundle; 24 import android.os.ResultReceiver; 25 import android.text.TextUtils; 26 import android.transition.Transition; 27 import android.transition.TransitionManager; 28 import android.util.ArrayMap; 29 import android.view.View; 30 import android.view.ViewGroup; 31 import android.view.ViewGroupOverlay; 32 import android.view.ViewTreeObserver; 33 import android.view.Window; 34 35 import 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 */ 42 class 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 EnterTransitionCoordinator(Activity activity, ResultReceiver resultReceiver, ArrayList<String> sharedElementNames, boolean isReturning)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 viewInstancesReady(ArrayList<String> accepted, ArrayList<String> localNames, ArrayList<View> localViews)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 namedViewsReady(ArrayList<String> accepted, ArrayList<String> localNames)103 public void namedViewsReady(ArrayList<String> accepted, ArrayList<String> localNames) { 104 triggerViewsReady(mapNamedElements(accepted, localNames)); 105 } 106 getEnterViewsTransition()107 public Transition getEnterViewsTransition() { 108 return mEnterViewsTransition; 109 } 110 111 @Override viewsReady(ArrayMap<String, View> sharedElements)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 triggerViewsReady(final ArrayMap<String, View> sharedElements)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 mapNamedElements(ArrayList<String> accepted, ArrayList<String> localNames)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 sendSharedElementDestination()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 getListener(Activity activity, boolean isReturning)216 private static SharedElementCallback getListener(Activity activity, boolean isReturning) { 217 return isReturning ? activity.mExitTransitionListener : activity.mEnterTransitionListener; 218 } 219 220 @Override onReceiveResult(int resultCode, Bundle resultData)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 cancel()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 mSharedElementNames.clear(); 252 mSharedElements.clear(); 253 mAllSharedElementNames.clear(); 254 startSharedElementTransition(null); 255 onRemoteExitTransitionComplete(); 256 } 257 } 258 isReturning()259 public boolean isReturning() { 260 return mIsReturning; 261 } 262 prepareEnter()263 protected void prepareEnter() { 264 ViewGroup decorView = getDecor(); 265 if (mActivity == null || decorView == null) { 266 return; 267 } 268 mActivity.overridePendingTransition(0, 0); 269 if (!mIsReturning) { 270 mWasOpaque = mActivity.convertToTranslucent(null, null); 271 Drawable background = decorView.getBackground(); 272 if (background != null) { 273 getWindow().setBackgroundDrawable(null); 274 background = background.mutate(); 275 background.setAlpha(0); 276 getWindow().setBackgroundDrawable(background); 277 } 278 } else { 279 mActivity = null; // all done with it now. 280 } 281 } 282 283 @Override getViewsTransition()284 protected Transition getViewsTransition() { 285 Window window = getWindow(); 286 if (window == null) { 287 return null; 288 } 289 if (mIsReturning) { 290 return window.getReenterTransition(); 291 } else { 292 return window.getEnterTransition(); 293 } 294 } 295 getSharedElementTransition()296 protected Transition getSharedElementTransition() { 297 Window window = getWindow(); 298 if (window == null) { 299 return null; 300 } 301 if (mIsReturning) { 302 return window.getSharedElementReenterTransition(); 303 } else { 304 return window.getSharedElementEnterTransition(); 305 } 306 } 307 startSharedElementTransition(Bundle sharedElementState)308 private void startSharedElementTransition(Bundle sharedElementState) { 309 ViewGroup decorView = getDecor(); 310 if (decorView == null) { 311 return; 312 } 313 // Remove rejected shared elements 314 ArrayList<String> rejectedNames = new ArrayList<String>(mAllSharedElementNames); 315 rejectedNames.removeAll(mSharedElementNames); 316 ArrayList<View> rejectedSnapshots = createSnapshots(sharedElementState, rejectedNames); 317 if (mListener != null) { 318 mListener.onRejectSharedElements(rejectedSnapshots); 319 } 320 removeNullViews(rejectedSnapshots); 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 pauseInput(); 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 OnSharedElementsReadyListener listener = new OnSharedElementsReadyListener() { 387 @Override 388 public void onSharedElementsReady() { 389 final View decorView = getDecor(); 390 if (decorView != null) { 391 decorView.getViewTreeObserver() 392 .addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() { 393 @Override 394 public boolean onPreDraw() { 395 decorView.getViewTreeObserver().removeOnPreDrawListener(this); 396 startTransition(new Runnable() { 397 @Override 398 public void run() { 399 startSharedElementTransition(sharedElementState); 400 } 401 }); 402 return false; 403 } 404 }); 405 decorView.invalidate(); 406 } 407 } 408 }; 409 if (mListener == null) { 410 listener.onSharedElementsReady(); 411 } else { 412 mListener.onSharedElementsArrived(mSharedElementNames, mSharedElements, listener); 413 } 414 } 415 requestLayoutForSharedElements()416 private void requestLayoutForSharedElements() { 417 int numSharedElements = mSharedElements.size(); 418 for (int i = 0; i < numSharedElements; i++) { 419 mSharedElements.get(i).requestLayout(); 420 } 421 } 422 beginTransition(ViewGroup decorView, boolean startEnterTransition, boolean startSharedElementTransition)423 private Transition beginTransition(ViewGroup decorView, boolean startEnterTransition, 424 boolean startSharedElementTransition) { 425 Transition sharedElementTransition = null; 426 if (startSharedElementTransition) { 427 if (!mSharedElementNames.isEmpty()) { 428 sharedElementTransition = configureTransition(getSharedElementTransition(), false); 429 } 430 if (sharedElementTransition == null) { 431 sharedElementTransitionStarted(); 432 sharedElementTransitionComplete(); 433 } else { 434 sharedElementTransition.addListener(new Transition.TransitionListenerAdapter() { 435 @Override 436 public void onTransitionStart(Transition transition) { 437 sharedElementTransitionStarted(); 438 } 439 440 @Override 441 public void onTransitionEnd(Transition transition) { 442 transition.removeListener(this); 443 sharedElementTransitionComplete(); 444 } 445 }); 446 } 447 } 448 Transition viewsTransition = null; 449 if (startEnterTransition) { 450 mIsViewsTransitionStarted = true; 451 if (mTransitioningViews != null && !mTransitioningViews.isEmpty()) { 452 viewsTransition = configureTransition(getViewsTransition(), true); 453 if (viewsTransition != null && !mIsReturning) { 454 stripOffscreenViews(); 455 } 456 } 457 if (viewsTransition == null) { 458 viewsTransitionComplete(); 459 } else { 460 viewsTransition.forceVisibility(View.INVISIBLE, true); 461 final ArrayList<View> transitioningViews = mTransitioningViews; 462 viewsTransition.addListener(new ContinueTransitionListener() { 463 @Override 464 public void onTransitionStart(Transition transition) { 465 mEnterViewsTransition = transition; 466 if (transitioningViews != null) { 467 showViews(transitioningViews, false); 468 } 469 super.onTransitionStart(transition); 470 } 471 472 @Override 473 public void onTransitionEnd(Transition transition) { 474 mEnterViewsTransition = null; 475 transition.removeListener(this); 476 viewsTransitionComplete(); 477 super.onTransitionEnd(transition); 478 } 479 }); 480 } 481 } 482 483 Transition transition = mergeTransitions(sharedElementTransition, viewsTransition); 484 if (transition != null) { 485 transition.addListener(new ContinueTransitionListener()); 486 TransitionManager.beginDelayedTransition(decorView, transition); 487 if (startSharedElementTransition && !mSharedElementNames.isEmpty()) { 488 mSharedElements.get(0).invalidate(); 489 } else if (startEnterTransition && mTransitioningViews != null && 490 !mTransitioningViews.isEmpty()) { 491 mTransitioningViews.get(0).invalidate(); 492 } 493 } else { 494 transitionStarted(); 495 } 496 return transition; 497 } 498 499 @Override onTransitionsComplete()500 protected void onTransitionsComplete() { 501 moveSharedElementsFromOverlay(); 502 } 503 sharedElementTransitionStarted()504 private void sharedElementTransitionStarted() { 505 mSharedElementTransitionStarted = true; 506 if (mIsExitTransitionComplete) { 507 send(MSG_EXIT_TRANSITION_COMPLETE, null); 508 } 509 } 510 startEnterTransition(Transition transition)511 private void startEnterTransition(Transition transition) { 512 ViewGroup decorView = getDecor(); 513 if (!mIsReturning && decorView != null) { 514 Drawable background = decorView.getBackground(); 515 if (background != null) { 516 background = background.mutate(); 517 getWindow().setBackgroundDrawable(background); 518 mBackgroundAnimator = ObjectAnimator.ofInt(background, "alpha", 255); 519 mBackgroundAnimator.setDuration(getFadeDuration()); 520 mBackgroundAnimator.addListener(new AnimatorListenerAdapter() { 521 @Override 522 public void onAnimationEnd(Animator animation) { 523 makeOpaque(); 524 } 525 }); 526 mBackgroundAnimator.start(); 527 } else if (transition != null) { 528 transition.addListener(new Transition.TransitionListenerAdapter() { 529 @Override 530 public void onTransitionEnd(Transition transition) { 531 transition.removeListener(this); 532 makeOpaque(); 533 } 534 }); 535 } else { 536 makeOpaque(); 537 } 538 } 539 } 540 stop()541 public void stop() { 542 // Restore the background to its previous state since the 543 // Activity is stopping. 544 if (mBackgroundAnimator != null) { 545 mBackgroundAnimator.end(); 546 mBackgroundAnimator = null; 547 } else if (mWasOpaque) { 548 ViewGroup decorView = getDecor(); 549 if (decorView != null) { 550 Drawable drawable = decorView.getBackground(); 551 if (drawable != null) { 552 drawable.setAlpha(1); 553 } 554 } 555 } 556 makeOpaque(); 557 mIsCanceled = true; 558 mResultReceiver = null; 559 mActivity = null; 560 moveSharedElementsFromOverlay(); 561 if (mTransitioningViews != null) { 562 showViews(mTransitioningViews, true); 563 } 564 showViews(mSharedElements, true); 565 clearState(); 566 } 567 568 /** 569 * Cancels the enter transition. 570 * @return True if the enter transition is still pending capturing the target state. If so, 571 * any transition started on the decor will do nothing. 572 */ cancelEnter()573 public boolean cancelEnter() { 574 setGhostVisibility(View.INVISIBLE); 575 mHasStopped = true; 576 mIsCanceled = true; 577 mResultReceiver = null; 578 if (mBackgroundAnimator != null) { 579 mBackgroundAnimator.cancel(); 580 mBackgroundAnimator = null; 581 } 582 mActivity = null; 583 clearState(); 584 return super.cancelPendingTransitions(); 585 } 586 makeOpaque()587 private void makeOpaque() { 588 if (!mHasStopped && mActivity != null) { 589 if (mWasOpaque) { 590 mActivity.convertFromTranslucent(); 591 } 592 mActivity = null; 593 } 594 } 595 allowOverlappingTransitions()596 private boolean allowOverlappingTransitions() { 597 return mIsReturning ? getWindow().getAllowReturnTransitionOverlap() 598 : getWindow().getAllowEnterTransitionOverlap(); 599 } 600 startRejectedAnimations(final ArrayList<View> rejectedSnapshots)601 private void startRejectedAnimations(final ArrayList<View> rejectedSnapshots) { 602 if (rejectedSnapshots == null || rejectedSnapshots.isEmpty()) { 603 return; 604 } 605 final ViewGroup decorView = getDecor(); 606 if (decorView != null) { 607 ViewGroupOverlay overlay = decorView.getOverlay(); 608 ObjectAnimator animator = null; 609 int numRejected = rejectedSnapshots.size(); 610 for (int i = 0; i < numRejected; i++) { 611 View snapshot = rejectedSnapshots.get(i); 612 overlay.add(snapshot); 613 animator = ObjectAnimator.ofFloat(snapshot, View.ALPHA, 1, 0); 614 animator.start(); 615 } 616 animator.addListener(new AnimatorListenerAdapter() { 617 @Override 618 public void onAnimationEnd(Animator animation) { 619 ViewGroupOverlay overlay = decorView.getOverlay(); 620 int numRejected = rejectedSnapshots.size(); 621 for (int i = 0; i < numRejected; i++) { 622 overlay.remove(rejectedSnapshots.get(i)); 623 } 624 } 625 }); 626 } 627 } 628 onRemoteExitTransitionComplete()629 protected void onRemoteExitTransitionComplete() { 630 if (!allowOverlappingTransitions()) { 631 startEnterTransitionOnly(); 632 } 633 } 634 startEnterTransitionOnly()635 private void startEnterTransitionOnly() { 636 startTransition(new Runnable() { 637 @Override 638 public void run() { 639 boolean startEnterTransition = true; 640 boolean startSharedElementTransition = false; 641 ViewGroup decorView = getDecor(); 642 if (decorView != null) { 643 Transition transition = beginTransition(decorView, startEnterTransition, 644 startSharedElementTransition); 645 startEnterTransition(transition); 646 } 647 } 648 }); 649 } 650 651 } 652