EnterTransitionCoordinator.java revision 41725dedc33906aaafee36b2d6523596e2a8a52e
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 mSharedElementNames.clear(); 252 mSharedElements.clear(); 253 mAllSharedElementNames.clear(); 254 startSharedElementTransition(null); 255 onRemoteExitTransitionComplete(); 256 } 257 } 258 259 public boolean isReturning() { 260 return mIsReturning; 261 } 262 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 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 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 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 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 static void removeNullViews(ArrayList<View> views) { 370 if (views != null) { 371 for (int i = views.size() - 1; i >= 0; i--) { 372 if (views.get(i) == null) { 373 views.remove(i); 374 } 375 } 376 } 377 } 378 379 private void onTakeSharedElements() { 380 if (!mIsReadyForTransition || mSharedElementsBundle == null) { 381 return; 382 } 383 final Bundle sharedElementState = mSharedElementsBundle; 384 mSharedElementsBundle = null; 385 OnSharedElementsReadyListener listener = new OnSharedElementsReadyListener() { 386 @Override 387 public void onSharedElementsReady() { 388 final View decorView = getDecor(); 389 if (decorView != null) { 390 decorView.getViewTreeObserver() 391 .addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() { 392 @Override 393 public boolean onPreDraw() { 394 decorView.getViewTreeObserver().removeOnPreDrawListener(this); 395 startTransition(new Runnable() { 396 @Override 397 public void run() { 398 startSharedElementTransition(sharedElementState); 399 } 400 }); 401 return false; 402 } 403 }); 404 decorView.invalidate(); 405 } 406 } 407 }; 408 if (mListener == null) { 409 listener.onSharedElementsReady(); 410 } else { 411 mListener.onSharedElementsArrived(mSharedElementNames, mSharedElements, listener); 412 } 413 } 414 415 private void requestLayoutForSharedElements() { 416 int numSharedElements = mSharedElements.size(); 417 for (int i = 0; i < numSharedElements; i++) { 418 mSharedElements.get(i).requestLayout(); 419 } 420 } 421 422 private Transition beginTransition(ViewGroup decorView, boolean startEnterTransition, 423 boolean startSharedElementTransition) { 424 Transition sharedElementTransition = null; 425 if (startSharedElementTransition) { 426 if (!mSharedElementNames.isEmpty()) { 427 sharedElementTransition = configureTransition(getSharedElementTransition(), false); 428 } 429 if (sharedElementTransition == null) { 430 sharedElementTransitionStarted(); 431 sharedElementTransitionComplete(); 432 } else { 433 sharedElementTransition.addListener(new Transition.TransitionListenerAdapter() { 434 @Override 435 public void onTransitionStart(Transition transition) { 436 sharedElementTransitionStarted(); 437 } 438 439 @Override 440 public void onTransitionEnd(Transition transition) { 441 transition.removeListener(this); 442 sharedElementTransitionComplete(); 443 } 444 }); 445 } 446 } 447 Transition viewsTransition = null; 448 if (startEnterTransition) { 449 mIsViewsTransitionStarted = true; 450 if (mTransitioningViews != null && !mTransitioningViews.isEmpty()) { 451 viewsTransition = configureTransition(getViewsTransition(), true); 452 if (viewsTransition != null && !mIsReturning) { 453 stripOffscreenViews(); 454 } 455 } 456 if (viewsTransition == null) { 457 viewsTransitionComplete(); 458 } else { 459 viewsTransition.forceVisibility(View.INVISIBLE, true); 460 final ArrayList<View> transitioningViews = mTransitioningViews; 461 viewsTransition.addListener(new ContinueTransitionListener() { 462 @Override 463 public void onTransitionStart(Transition transition) { 464 mEnterViewsTransition = transition; 465 if (transitioningViews != null) { 466 showViews(transitioningViews, false); 467 } 468 super.onTransitionStart(transition); 469 } 470 471 @Override 472 public void onTransitionEnd(Transition transition) { 473 mEnterViewsTransition = null; 474 transition.removeListener(this); 475 viewsTransitionComplete(); 476 super.onTransitionEnd(transition); 477 } 478 }); 479 } 480 } 481 482 Transition transition = mergeTransitions(sharedElementTransition, viewsTransition); 483 if (transition != null) { 484 transition.addListener(new ContinueTransitionListener()); 485 TransitionManager.beginDelayedTransition(decorView, transition); 486 if (startSharedElementTransition && !mSharedElementNames.isEmpty()) { 487 mSharedElements.get(0).invalidate(); 488 } else if (startEnterTransition && mTransitioningViews != null && 489 !mTransitioningViews.isEmpty()) { 490 mTransitioningViews.get(0).invalidate(); 491 } 492 } else { 493 transitionStarted(); 494 } 495 return transition; 496 } 497 498 @Override 499 protected void onTransitionsComplete() { 500 moveSharedElementsFromOverlay(); 501 } 502 503 private void sharedElementTransitionStarted() { 504 mSharedElementTransitionStarted = true; 505 if (mIsExitTransitionComplete) { 506 send(MSG_EXIT_TRANSITION_COMPLETE, null); 507 } 508 } 509 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 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 */ 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 586 private void makeOpaque() { 587 if (!mHasStopped && mActivity != null) { 588 if (mWasOpaque) { 589 mActivity.convertFromTranslucent(); 590 } 591 mActivity = null; 592 } 593 } 594 595 private boolean allowOverlappingTransitions() { 596 return mIsReturning ? getWindow().getAllowExitTransitionOverlap() 597 : getWindow().getAllowEnterTransitionOverlap(); 598 } 599 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 628 protected void onRemoteExitTransitionComplete() { 629 if (!allowOverlappingTransitions()) { 630 startEnterTransitionOnly(); 631 } 632 } 633 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