EnterTransitionCoordinator.java revision 1fb941dd4f089fd778d1438fe8588be0eca8848a
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 final ViewGroup decor = getDecor(); 137 // Ensure the views have been laid out before capturing the views -- we need the epicenter. 138 if (decor == null || (decor.isAttachedToWindow() && 139 (sharedElements.isEmpty() || !sharedElements.valueAt(0).isLayoutRequested()))) { 140 viewsReady(sharedElements); 141 } else { 142 decor.getViewTreeObserver() 143 .addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() { 144 @Override 145 public boolean onPreDraw() { 146 decor.getViewTreeObserver().removeOnPreDrawListener(this); 147 viewsReady(sharedElements); 148 return true; 149 } 150 }); 151 } 152 } 153 154 private ArrayMap<String, View> mapNamedElements(ArrayList<String> accepted, 155 ArrayList<String> localNames) { 156 ArrayMap<String, View> sharedElements = new ArrayMap<String, View>(); 157 ViewGroup decorView = getDecor(); 158 if (decorView != null) { 159 decorView.findNamedViews(sharedElements); 160 } 161 if (accepted != null) { 162 for (int i = 0; i < localNames.size(); i++) { 163 String localName = localNames.get(i); 164 String acceptedName = accepted.get(i); 165 if (localName != null && !localName.equals(acceptedName)) { 166 View view = sharedElements.remove(localName); 167 if (view != null) { 168 sharedElements.put(acceptedName, view); 169 } 170 } 171 } 172 } 173 return sharedElements; 174 } 175 176 private void sendSharedElementDestination() { 177 boolean allReady; 178 final View decorView = getDecor(); 179 if (allowOverlappingTransitions() && getEnterViewsTransition() != null) { 180 allReady = false; 181 } else if (decorView == null) { 182 allReady = true; 183 } else { 184 allReady = !decorView.isLayoutRequested(); 185 if (allReady) { 186 for (int i = 0; i < mSharedElements.size(); i++) { 187 if (mSharedElements.get(i).isLayoutRequested()) { 188 allReady = false; 189 break; 190 } 191 } 192 } 193 } 194 if (allReady) { 195 Bundle state = captureSharedElementState(); 196 moveSharedElementsToOverlay(); 197 mResultReceiver.send(MSG_SHARED_ELEMENT_DESTINATION, state); 198 } else if (decorView != null) { 199 decorView.getViewTreeObserver() 200 .addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() { 201 @Override 202 public boolean onPreDraw() { 203 decorView.getViewTreeObserver().removeOnPreDrawListener(this); 204 if (mResultReceiver != null) { 205 Bundle state = captureSharedElementState(); 206 moveSharedElementsToOverlay(); 207 mResultReceiver.send(MSG_SHARED_ELEMENT_DESTINATION, state); 208 } 209 return true; 210 } 211 }); 212 } 213 if (allowOverlappingTransitions()) { 214 startEnterTransitionOnly(); 215 } 216 } 217 218 private static SharedElementCallback getListener(Activity activity, boolean isReturning) { 219 return isReturning ? activity.mExitTransitionListener : activity.mEnterTransitionListener; 220 } 221 222 @Override 223 protected void onReceiveResult(int resultCode, Bundle resultData) { 224 switch (resultCode) { 225 case MSG_TAKE_SHARED_ELEMENTS: 226 if (!mIsCanceled) { 227 mSharedElementsBundle = resultData; 228 onTakeSharedElements(); 229 } 230 break; 231 case MSG_EXIT_TRANSITION_COMPLETE: 232 if (!mIsCanceled) { 233 mIsExitTransitionComplete = true; 234 if (mSharedElementTransitionStarted) { 235 onRemoteExitTransitionComplete(); 236 } 237 } 238 break; 239 case MSG_CANCEL: 240 cancel(); 241 break; 242 } 243 } 244 245 private void cancel() { 246 if (!mIsCanceled) { 247 mIsCanceled = true; 248 if (getViewsTransition() == null || mIsViewsTransitionStarted) { 249 showViews(mSharedElements, true); 250 } else if (mTransitioningViews != null) { 251 mTransitioningViews.addAll(mSharedElements); 252 } 253 mSharedElementNames.clear(); 254 mSharedElements.clear(); 255 mAllSharedElementNames.clear(); 256 startSharedElementTransition(null); 257 onRemoteExitTransitionComplete(); 258 } 259 } 260 261 public boolean isReturning() { 262 return mIsReturning; 263 } 264 265 protected void prepareEnter() { 266 ViewGroup decorView = getDecor(); 267 if (mActivity == null || decorView == null) { 268 return; 269 } 270 mActivity.overridePendingTransition(0, 0); 271 if (!mIsReturning) { 272 mWasOpaque = mActivity.convertToTranslucent(null, null); 273 Drawable background = decorView.getBackground(); 274 if (background != null) { 275 getWindow().setBackgroundDrawable(null); 276 background = background.mutate(); 277 background.setAlpha(0); 278 getWindow().setBackgroundDrawable(background); 279 } 280 } else { 281 mActivity = null; // all done with it now. 282 } 283 } 284 285 @Override 286 protected Transition getViewsTransition() { 287 Window window = getWindow(); 288 if (window == null) { 289 return null; 290 } 291 if (mIsReturning) { 292 return window.getReenterTransition(); 293 } else { 294 return window.getEnterTransition(); 295 } 296 } 297 298 protected Transition getSharedElementTransition() { 299 Window window = getWindow(); 300 if (window == null) { 301 return null; 302 } 303 if (mIsReturning) { 304 return window.getSharedElementReenterTransition(); 305 } else { 306 return window.getSharedElementEnterTransition(); 307 } 308 } 309 310 private void startSharedElementTransition(Bundle sharedElementState) { 311 ViewGroup decorView = getDecor(); 312 if (decorView == null) { 313 return; 314 } 315 // Remove rejected shared elements 316 ArrayList<String> rejectedNames = new ArrayList<String>(mAllSharedElementNames); 317 rejectedNames.removeAll(mSharedElementNames); 318 ArrayList<View> rejectedSnapshots = createSnapshots(sharedElementState, rejectedNames); 319 if (mListener != null) { 320 mListener.onRejectSharedElements(rejectedSnapshots); 321 } 322 removeNullViews(rejectedSnapshots); 323 startRejectedAnimations(rejectedSnapshots); 324 325 // Now start shared element transition 326 ArrayList<View> sharedElementSnapshots = createSnapshots(sharedElementState, 327 mSharedElementNames); 328 showViews(mSharedElements, true); 329 scheduleSetSharedElementEnd(sharedElementSnapshots); 330 ArrayList<SharedElementOriginalState> originalImageViewState = 331 setSharedElementState(sharedElementState, sharedElementSnapshots); 332 requestLayoutForSharedElements(); 333 334 boolean startEnterTransition = allowOverlappingTransitions() && !mIsReturning; 335 boolean startSharedElementTransition = true; 336 setGhostVisibility(View.INVISIBLE); 337 scheduleGhostVisibilityChange(View.INVISIBLE); 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 final View decorView = getDecor(); 388 if (decorView != null) { 389 decorView.getViewTreeObserver() 390 .addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() { 391 @Override 392 public boolean onPreDraw() { 393 decorView.getViewTreeObserver().removeOnPreDrawListener(this); 394 startTransition(new Runnable() { 395 @Override 396 public void run() { 397 startSharedElementTransition(sharedElementState); 398 } 399 }); 400 return false; 401 } 402 }); 403 decorView.invalidate(); 404 } 405 } 406 407 private void requestLayoutForSharedElements() { 408 int numSharedElements = mSharedElements.size(); 409 for (int i = 0; i < numSharedElements; i++) { 410 mSharedElements.get(i).requestLayout(); 411 } 412 } 413 414 private Transition beginTransition(ViewGroup decorView, boolean startEnterTransition, 415 boolean startSharedElementTransition) { 416 Transition sharedElementTransition = null; 417 if (startSharedElementTransition) { 418 if (!mSharedElementNames.isEmpty()) { 419 sharedElementTransition = configureTransition(getSharedElementTransition(), false); 420 } 421 if (sharedElementTransition == null) { 422 sharedElementTransitionStarted(); 423 sharedElementTransitionComplete(); 424 } else { 425 sharedElementTransition.addListener(new Transition.TransitionListenerAdapter() { 426 @Override 427 public void onTransitionStart(Transition transition) { 428 sharedElementTransitionStarted(); 429 } 430 431 @Override 432 public void onTransitionEnd(Transition transition) { 433 transition.removeListener(this); 434 sharedElementTransitionComplete(); 435 } 436 }); 437 } 438 } 439 Transition viewsTransition = null; 440 if (startEnterTransition) { 441 mIsViewsTransitionStarted = true; 442 if (mTransitioningViews != null && !mTransitioningViews.isEmpty()) { 443 viewsTransition = configureTransition(getViewsTransition(), true); 444 if (viewsTransition != null && !mIsReturning) { 445 stripOffscreenViews(); 446 } 447 } 448 if (viewsTransition == null) { 449 viewTransitionComplete(); 450 } else { 451 viewsTransition.forceVisibility(View.INVISIBLE, true); 452 final ArrayList<View> transitioningViews = mTransitioningViews; 453 viewsTransition.addListener(new ContinueTransitionListener() { 454 @Override 455 public void onTransitionStart(Transition transition) { 456 mEnterViewsTransition = transition; 457 if (transitioningViews != null) { 458 showViews(transitioningViews, false); 459 } 460 super.onTransitionStart(transition); 461 } 462 463 @Override 464 public void onTransitionEnd(Transition transition) { 465 mEnterViewsTransition = null; 466 transition.removeListener(this); 467 viewTransitionComplete(); 468 super.onTransitionEnd(transition); 469 } 470 }); 471 } 472 } 473 474 Transition transition = mergeTransitions(sharedElementTransition, viewsTransition); 475 if (transition != null) { 476 transition.addListener(new ContinueTransitionListener()); 477 TransitionManager.beginDelayedTransition(decorView, transition); 478 if (startSharedElementTransition && !mSharedElementNames.isEmpty()) { 479 mSharedElements.get(0).invalidate(); 480 } else if (startEnterTransition && mTransitioningViews != null && 481 !mTransitioningViews.isEmpty()) { 482 mTransitioningViews.get(0).invalidate(); 483 } 484 } else { 485 transitionStarted(); 486 } 487 return transition; 488 } 489 490 private void viewTransitionComplete() { 491 mIsViewsTransitionComplete = true; 492 if (mIsSharedElementTransitionComplete) { 493 moveSharedElementsFromOverlay(); 494 } 495 } 496 497 private void sharedElementTransitionComplete() { 498 mIsSharedElementTransitionComplete = true; 499 if (mIsViewsTransitionComplete) { 500 moveSharedElementsFromOverlay(); 501 } 502 } 503 504 private void sharedElementTransitionStarted() { 505 mSharedElementTransitionStarted = true; 506 if (mIsExitTransitionComplete) { 507 send(MSG_EXIT_TRANSITION_COMPLETE, null); 508 } 509 } 510 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 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 public void cancelEnter() { 569 setGhostVisibility(View.INVISIBLE); 570 mHasStopped = true; 571 mIsCanceled = true; 572 mResultReceiver = null; 573 if (mBackgroundAnimator != null) { 574 mBackgroundAnimator.cancel(); 575 mBackgroundAnimator = null; 576 } 577 mActivity = null; 578 clearState(); 579 } 580 581 private void makeOpaque() { 582 if (!mHasStopped && mActivity != null) { 583 if (mWasOpaque) { 584 mActivity.convertFromTranslucent(); 585 } 586 mActivity = null; 587 } 588 } 589 590 private boolean allowOverlappingTransitions() { 591 return mIsReturning ? getWindow().getAllowExitTransitionOverlap() 592 : getWindow().getAllowEnterTransitionOverlap(); 593 } 594 595 private void startRejectedAnimations(final ArrayList<View> rejectedSnapshots) { 596 if (rejectedSnapshots == null || rejectedSnapshots.isEmpty()) { 597 return; 598 } 599 final ViewGroup decorView = getDecor(); 600 if (decorView != null) { 601 ViewGroupOverlay overlay = decorView.getOverlay(); 602 ObjectAnimator animator = null; 603 int numRejected = rejectedSnapshots.size(); 604 for (int i = 0; i < numRejected; i++) { 605 View snapshot = rejectedSnapshots.get(i); 606 overlay.add(snapshot); 607 animator = ObjectAnimator.ofFloat(snapshot, View.ALPHA, 1, 0); 608 animator.start(); 609 } 610 animator.addListener(new AnimatorListenerAdapter() { 611 @Override 612 public void onAnimationEnd(Animator animation) { 613 ViewGroupOverlay overlay = decorView.getOverlay(); 614 int numRejected = rejectedSnapshots.size(); 615 for (int i = 0; i < numRejected; i++) { 616 overlay.remove(rejectedSnapshots.get(i)); 617 } 618 } 619 }); 620 } 621 } 622 623 protected void onRemoteExitTransitionComplete() { 624 if (!allowOverlappingTransitions()) { 625 startEnterTransitionOnly(); 626 } 627 } 628 629 private void startEnterTransitionOnly() { 630 startTransition(new Runnable() { 631 @Override 632 public void run() { 633 boolean startEnterTransition = true; 634 boolean startSharedElementTransition = false; 635 ViewGroup decorView = getDecor(); 636 if (decorView != null) { 637 Transition transition = beginTransition(decorView, startEnterTransition, 638 startSharedElementTransition); 639 startEnterTransition(transition); 640 } 641 } 642 }); 643 } 644 645} 646