EnterTransitionCoordinator.java revision b694e080546316a27d22eba759027f6cb0a24705
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; 33 34import 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 */ 41class 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 ArrayList<Matrix> mSharedElementParentMatrices; 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 setSharedElementMatrices(); 125 moveSharedElementsToOverlay(); 126 } 127 if (mSharedElementsBundle != null) { 128 onTakeSharedElements(); 129 } 130 } 131 132 private void triggerViewsReady(final ArrayMap<String, View> sharedElements) { 133 if (mAreViewsReady) { 134 return; 135 } 136 mAreViewsReady = true; 137 // Ensure the views have been laid out before capturing the views -- we need the epicenter. 138 if (sharedElements.isEmpty() || !sharedElements.valueAt(0).isLayoutRequested()) { 139 viewsReady(sharedElements); 140 } else { 141 final View sharedElement = sharedElements.valueAt(0); 142 sharedElement.getViewTreeObserver() 143 .addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() { 144 @Override 145 public boolean onPreDraw() { 146 sharedElement.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 setSharedElementMatrices(); 197 moveSharedElementsToOverlay(); 198 mResultReceiver.send(MSG_SHARED_ELEMENT_DESTINATION, state); 199 } else if (decorView != null) { 200 decorView.getViewTreeObserver() 201 .addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() { 202 @Override 203 public boolean onPreDraw() { 204 decorView.getViewTreeObserver().removeOnPreDrawListener(this); 205 if (mResultReceiver != null) { 206 Bundle state = captureSharedElementState(); 207 setSharedElementMatrices(); 208 moveSharedElementsToOverlay(); 209 mResultReceiver.send(MSG_SHARED_ELEMENT_DESTINATION, state); 210 } 211 return true; 212 } 213 }); 214 } 215 if (allowOverlappingTransitions()) { 216 startEnterTransitionOnly(); 217 } 218 } 219 220 private static SharedElementCallback getListener(Activity activity, boolean isReturning) { 221 return isReturning ? activity.mExitTransitionListener : activity.mEnterTransitionListener; 222 } 223 224 @Override 225 protected void onReceiveResult(int resultCode, Bundle resultData) { 226 switch (resultCode) { 227 case MSG_TAKE_SHARED_ELEMENTS: 228 if (!mIsCanceled) { 229 mSharedElementsBundle = resultData; 230 onTakeSharedElements(); 231 } 232 break; 233 case MSG_EXIT_TRANSITION_COMPLETE: 234 if (!mIsCanceled) { 235 mIsExitTransitionComplete = true; 236 if (mSharedElementTransitionStarted) { 237 onRemoteExitTransitionComplete(); 238 } 239 } 240 break; 241 case MSG_CANCEL: 242 cancel(); 243 break; 244 } 245 } 246 247 private void cancel() { 248 if (!mIsCanceled) { 249 mIsCanceled = true; 250 if (getViewsTransition() == null || mIsViewsTransitionStarted) { 251 showViews(mSharedElements, true); 252 } else if (mTransitioningViews != null) { 253 mTransitioningViews.addAll(mSharedElements); 254 } 255 mSharedElementNames.clear(); 256 mSharedElements.clear(); 257 mAllSharedElementNames.clear(); 258 startSharedElementTransition(null); 259 onRemoteExitTransitionComplete(); 260 } 261 } 262 263 public boolean isReturning() { 264 return mIsReturning; 265 } 266 267 protected void prepareEnter() { 268 mActivity.overridePendingTransition(0, 0); 269 if (!mIsReturning) { 270 mWasOpaque = mActivity.convertToTranslucent(null, null); 271 Drawable background = getDecor().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 if (mIsReturning) { 286 return getWindow().getReenterTransition(); 287 } else { 288 return getWindow().getEnterTransition(); 289 } 290 } 291 292 protected Transition getSharedElementTransition() { 293 if (mIsReturning) { 294 return getWindow().getSharedElementReenterTransition(); 295 } else { 296 return getWindow().getSharedElementEnterTransition(); 297 } 298 } 299 300 private void startSharedElementTransition(Bundle sharedElementState) { 301 ViewGroup decorView = getDecor(); 302 if (decorView == null) { 303 return; 304 } 305 // Remove rejected shared elements 306 ArrayList<String> rejectedNames = new ArrayList<String>(mAllSharedElementNames); 307 rejectedNames.removeAll(mSharedElementNames); 308 ArrayList<View> rejectedSnapshots = createSnapshots(sharedElementState, rejectedNames); 309 mListener.onRejectSharedElements(rejectedSnapshots); 310 startRejectedAnimations(rejectedSnapshots); 311 312 // Now start shared element transition 313 ArrayList<View> sharedElementSnapshots = createSnapshots(sharedElementState, 314 mSharedElementNames); 315 showViews(mSharedElements, true); 316 scheduleSetSharedElementEnd(sharedElementSnapshots); 317 ArrayList<SharedElementOriginalState> originalImageViewState = 318 setSharedElementState(sharedElementState, sharedElementSnapshots); 319 requestLayoutForSharedElements(); 320 321 boolean startEnterTransition = allowOverlappingTransitions() && !mIsReturning; 322 boolean startSharedElementTransition = true; 323 setGhostVisibility(View.INVISIBLE); 324 scheduleGhostVisibilityChange(View.INVISIBLE); 325 Transition transition = beginTransition(decorView, startEnterTransition, 326 startSharedElementTransition); 327 scheduleGhostVisibilityChange(View.VISIBLE); 328 setGhostVisibility(View.VISIBLE); 329 330 if (startEnterTransition) { 331 startEnterTransition(transition); 332 } 333 334 setOriginalSharedElementState(mSharedElements, originalImageViewState); 335 336 if (mResultReceiver != null) { 337 // We can't trust that the view will disappear on the same frame that the shared 338 // element appears here. Assure that we get at least 2 frames for double-buffering. 339 decorView.postOnAnimation(new Runnable() { 340 int mAnimations; 341 342 @Override 343 public void run() { 344 if (mAnimations++ < MIN_ANIMATION_FRAMES) { 345 View decorView = getDecor(); 346 if (decorView != null) { 347 decorView.postOnAnimation(this); 348 } 349 } else if (mResultReceiver != null) { 350 mResultReceiver.send(MSG_HIDE_SHARED_ELEMENTS, null); 351 mResultReceiver = null; // all done sending messages. 352 } 353 } 354 }); 355 } 356 } 357 358 private void onTakeSharedElements() { 359 if (!mIsReadyForTransition || mSharedElementsBundle == null) { 360 return; 361 } 362 final Bundle sharedElementState = mSharedElementsBundle; 363 mSharedElementsBundle = null; 364 final View decorView = getDecor(); 365 if (decorView != null) { 366 decorView.getViewTreeObserver() 367 .addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() { 368 @Override 369 public boolean onPreDraw() { 370 decorView.getViewTreeObserver().removeOnPreDrawListener(this); 371 startTransition(new Runnable() { 372 @Override 373 public void run() { 374 startSharedElementTransition(sharedElementState); 375 } 376 }); 377 return false; 378 } 379 }); 380 decorView.invalidate(); 381 } 382 } 383 384 private void requestLayoutForSharedElements() { 385 int numSharedElements = mSharedElements.size(); 386 for (int i = 0; i < numSharedElements; i++) { 387 mSharedElements.get(i).requestLayout(); 388 } 389 } 390 391 private Transition beginTransition(ViewGroup decorView, boolean startEnterTransition, 392 boolean startSharedElementTransition) { 393 Transition sharedElementTransition = null; 394 if (startSharedElementTransition) { 395 if (!mSharedElementNames.isEmpty()) { 396 sharedElementTransition = configureTransition(getSharedElementTransition(), false); 397 } 398 if (sharedElementTransition == null) { 399 sharedElementTransitionStarted(); 400 sharedElementTransitionComplete(); 401 } else { 402 sharedElementTransition.addListener(new Transition.TransitionListenerAdapter() { 403 @Override 404 public void onTransitionStart(Transition transition) { 405 sharedElementTransitionStarted(); 406 } 407 408 @Override 409 public void onTransitionEnd(Transition transition) { 410 transition.removeListener(this); 411 sharedElementTransitionComplete(); 412 } 413 }); 414 } 415 } 416 Transition viewsTransition = null; 417 if (startEnterTransition) { 418 mIsViewsTransitionStarted = true; 419 if (mTransitioningViews != null && !mTransitioningViews.isEmpty()) { 420 viewsTransition = configureTransition(getViewsTransition(), true); 421 if (viewsTransition != null && !mIsReturning) { 422 stripOffscreenViews(); 423 } 424 } 425 if (viewsTransition == null) { 426 viewTransitionComplete(); 427 } else { 428 viewsTransition.forceVisibility(View.INVISIBLE, true); 429 final ArrayList<View> transitioningViews = mTransitioningViews; 430 viewsTransition.addListener(new ContinueTransitionListener() { 431 @Override 432 public void onTransitionStart(Transition transition) { 433 mEnterViewsTransition = transition; 434 if (transitioningViews != null) { 435 showViews(transitioningViews, false); 436 } 437 super.onTransitionStart(transition); 438 } 439 440 @Override 441 public void onTransitionEnd(Transition transition) { 442 mEnterViewsTransition = null; 443 transition.removeListener(this); 444 viewTransitionComplete(); 445 super.onTransitionEnd(transition); 446 } 447 }); 448 } 449 } 450 451 Transition transition = mergeTransitions(sharedElementTransition, viewsTransition); 452 if (transition != null) { 453 transition.addListener(new ContinueTransitionListener()); 454 TransitionManager.beginDelayedTransition(decorView, transition); 455 if (startSharedElementTransition && !mSharedElementNames.isEmpty()) { 456 mSharedElements.get(0).invalidate(); 457 } else if (startEnterTransition && mTransitioningViews != null && 458 !mTransitioningViews.isEmpty()) { 459 mTransitioningViews.get(0).invalidate(); 460 } 461 } else { 462 transitionStarted(); 463 } 464 return transition; 465 } 466 467 private void viewTransitionComplete() { 468 mIsViewsTransitionComplete = true; 469 if (mIsSharedElementTransitionComplete) { 470 moveSharedElementsFromOverlay(); 471 } 472 } 473 474 private void sharedElementTransitionComplete() { 475 mIsSharedElementTransitionComplete = true; 476 if (mIsViewsTransitionComplete) { 477 moveSharedElementsFromOverlay(); 478 } 479 } 480 481 private void sharedElementTransitionStarted() { 482 mSharedElementTransitionStarted = true; 483 if (mIsExitTransitionComplete) { 484 send(MSG_EXIT_TRANSITION_COMPLETE, null); 485 } 486 } 487 488 private void startEnterTransition(Transition transition) { 489 ViewGroup decorView = getDecor(); 490 if (!mIsReturning && decorView != null) { 491 Drawable background = decorView.getBackground(); 492 if (background != null) { 493 background = background.mutate(); 494 getWindow().setBackgroundDrawable(background); 495 mBackgroundAnimator = ObjectAnimator.ofInt(background, "alpha", 255); 496 mBackgroundAnimator.setDuration(getFadeDuration()); 497 mBackgroundAnimator.addListener(new AnimatorListenerAdapter() { 498 @Override 499 public void onAnimationEnd(Animator animation) { 500 makeOpaque(); 501 } 502 }); 503 mBackgroundAnimator.start(); 504 } else if (transition != null) { 505 transition.addListener(new Transition.TransitionListenerAdapter() { 506 @Override 507 public void onTransitionEnd(Transition transition) { 508 transition.removeListener(this); 509 makeOpaque(); 510 } 511 }); 512 } else { 513 makeOpaque(); 514 } 515 } 516 } 517 518 public void stop() { 519 makeOpaque(); 520 mIsCanceled = true; 521 mResultReceiver = null; 522 if (mBackgroundAnimator != null) { 523 mBackgroundAnimator.end(); 524 mBackgroundAnimator = null; 525 } 526 mActivity = null; 527 moveSharedElementsFromOverlay(); 528 clearState(); 529 } 530 531 public void cancelEnter() { 532 setGhostVisibility(View.INVISIBLE); 533 mHasStopped = true; 534 mIsCanceled = true; 535 mResultReceiver = null; 536 if (mBackgroundAnimator != null) { 537 mBackgroundAnimator.cancel(); 538 mBackgroundAnimator = null; 539 } 540 mActivity = null; 541 clearState(); 542 } 543 544 private void makeOpaque() { 545 if (!mHasStopped && mActivity != null) { 546 if (mWasOpaque) { 547 mActivity.convertFromTranslucent(); 548 } 549 mActivity = null; 550 } 551 } 552 553 private boolean allowOverlappingTransitions() { 554 return mIsReturning ? getWindow().getAllowExitTransitionOverlap() 555 : getWindow().getAllowEnterTransitionOverlap(); 556 } 557 558 private void startRejectedAnimations(final ArrayList<View> rejectedSnapshots) { 559 if (rejectedSnapshots == null || rejectedSnapshots.isEmpty()) { 560 return; 561 } 562 final ViewGroup decorView = getDecor(); 563 if (decorView != null) { 564 ViewGroupOverlay overlay = decorView.getOverlay(); 565 ObjectAnimator animator = null; 566 int numRejected = rejectedSnapshots.size(); 567 for (int i = 0; i < numRejected; i++) { 568 View snapshot = rejectedSnapshots.get(i); 569 overlay.add(snapshot); 570 animator = ObjectAnimator.ofFloat(snapshot, View.ALPHA, 1, 0); 571 animator.start(); 572 } 573 animator.addListener(new AnimatorListenerAdapter() { 574 @Override 575 public void onAnimationEnd(Animator animation) { 576 ViewGroupOverlay overlay = decorView.getOverlay(); 577 int numRejected = rejectedSnapshots.size(); 578 for (int i = 0; i < numRejected; i++) { 579 overlay.remove(rejectedSnapshots.get(i)); 580 } 581 } 582 }); 583 } 584 } 585 586 protected void onRemoteExitTransitionComplete() { 587 if (!allowOverlappingTransitions()) { 588 startEnterTransitionOnly(); 589 } 590 } 591 592 private void startEnterTransitionOnly() { 593 startTransition(new Runnable() { 594 @Override 595 public void run() { 596 boolean startEnterTransition = true; 597 boolean startSharedElementTransition = false; 598 ViewGroup decorView = getDecor(); 599 if (decorView != null) { 600 Transition transition = beginTransition(decorView, startEnterTransition, 601 startSharedElementTransition); 602 startEnterTransition(transition); 603 } 604 } 605 }); 606 } 607 608 private void setSharedElementMatrices() { 609 int numSharedElements = mSharedElements.size(); 610 if (numSharedElements > 0) { 611 mSharedElementParentMatrices = new ArrayList<Matrix>(numSharedElements); 612 } 613 for (int i = 0; i < numSharedElements; i++) { 614 View view = mSharedElements.get(i); 615 616 // Find the location in the view's parent 617 ViewGroup parent = (ViewGroup) view.getParent(); 618 Matrix matrix = new Matrix(); 619 parent.transformMatrixToLocal(matrix); 620 621 mSharedElementParentMatrices.add(matrix); 622 } 623 } 624 625 @Override 626 protected void getSharedElementParentMatrix(View view, Matrix matrix) { 627 int index = mSharedElementParentMatrices == null ? -1 : mSharedElements.indexOf(view); 628 if (index < 0) { 629 super.getSharedElementParentMatrix(view, matrix); 630 } else { 631 matrix.set(mSharedElementParentMatrices.get(index)); 632 } 633 } 634} 635