EnterTransitionCoordinator.java revision 6e7fb60b93d3449d256282a9cc34ba3356add90e
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.Handler; 25import android.os.Message; 26import android.os.ResultReceiver; 27import android.text.TextUtils; 28import android.transition.Transition; 29import android.transition.TransitionManager; 30import android.util.ArrayMap; 31import android.view.View; 32import android.view.ViewGroup; 33import android.view.ViewGroupOverlay; 34import android.view.ViewTreeObserver; 35 36import java.util.ArrayList; 37 38/** 39 * This ActivityTransitionCoordinator is created by the Activity to manage 40 * the enter scene and shared element transfer into the Scene, either during 41 * launch of an Activity or returning from a launched Activity. 42 */ 43class EnterTransitionCoordinator extends ActivityTransitionCoordinator { 44 private static final String TAG = "EnterTransitionCoordinator"; 45 46 private static final int MIN_ANIMATION_FRAMES = 2; 47 48 private boolean mSharedElementTransitionStarted; 49 private Activity mActivity; 50 private boolean mHasStopped; 51 private boolean mIsCanceled; 52 private ObjectAnimator mBackgroundAnimator; 53 private boolean mIsExitTransitionComplete; 54 private boolean mIsReadyForTransition; 55 private Bundle mSharedElementsBundle; 56 private boolean mWasOpaque; 57 private boolean mAreViewsReady; 58 private boolean mIsViewsTransitionStarted; 59 private boolean mIsViewsTransitionComplete; 60 private boolean mIsSharedElementTransitionComplete; 61 private ArrayList<Matrix> mSharedElementParentMatrices; 62 private Transition mEnterViewsTransition; 63 64 public EnterTransitionCoordinator(Activity activity, ResultReceiver resultReceiver, 65 ArrayList<String> sharedElementNames, boolean isReturning) { 66 super(activity.getWindow(), sharedElementNames, 67 getListener(activity, isReturning), isReturning); 68 mActivity = activity; 69 setResultReceiver(resultReceiver); 70 prepareEnter(); 71 Bundle resultReceiverBundle = new Bundle(); 72 resultReceiverBundle.putParcelable(KEY_REMOTE_RECEIVER, this); 73 mResultReceiver.send(MSG_SET_REMOTE_RECEIVER, resultReceiverBundle); 74 final View decorView = getDecor(); 75 decorView.getViewTreeObserver().addOnPreDrawListener( 76 new ViewTreeObserver.OnPreDrawListener() { 77 @Override 78 public boolean onPreDraw() { 79 if (mIsReadyForTransition) { 80 decorView.getViewTreeObserver().removeOnPreDrawListener(this); 81 } 82 return mIsReadyForTransition; 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 setTransitionAlpha(mSharedElements, 0); 118 if (getViewsTransition() != null) { 119 setTransitionAlpha(mTransitioningViews, 0); 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 sharedElements.valueAt(0).getViewTreeObserver() 142 .addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() { 143 @Override 144 public boolean onPreDraw() { 145 sharedElements.valueAt(0).getViewTreeObserver().removeOnPreDrawListener(this); 146 viewsReady(sharedElements); 147 return true; 148 } 149 }); 150 } 151 } 152 153 private ArrayMap<String, View> mapNamedElements(ArrayList<String> accepted, 154 ArrayList<String> localNames) { 155 ArrayMap<String, View> sharedElements = new ArrayMap<String, View>(); 156 getDecor().findNamedViews(sharedElements); 157 if (accepted != null) { 158 for (int i = 0; i < localNames.size(); i++) { 159 String localName = localNames.get(i); 160 String acceptedName = accepted.get(i); 161 if (localName != null && !localName.equals(acceptedName)) { 162 View view = sharedElements.remove(localName); 163 if (view != null) { 164 sharedElements.put(acceptedName, view); 165 } 166 } 167 } 168 } 169 return sharedElements; 170 } 171 172 private void sendSharedElementDestination() { 173 boolean allReady; 174 if (allowOverlappingTransitions()) { 175 allReady = false; 176 } else { 177 allReady = !getDecor().isLayoutRequested(); 178 if (allReady) { 179 for (int i = 0; i < mSharedElements.size(); i++) { 180 if (mSharedElements.get(i).isLayoutRequested()) { 181 allReady = false; 182 break; 183 } 184 } 185 } 186 } 187 if (allReady) { 188 Bundle state = captureSharedElementState(); 189 setSharedElementMatrices(); 190 moveSharedElementsToOverlay(); 191 mResultReceiver.send(MSG_SHARED_ELEMENT_DESTINATION, state); 192 } else { 193 final View decorView = getDecor(); 194 decorView.getViewTreeObserver() 195 .addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() { 196 @Override 197 public boolean onPreDraw() { 198 decorView.getViewTreeObserver().removeOnPreDrawListener(this); 199 if (mResultReceiver != null) { 200 Bundle state = captureSharedElementState(); 201 setSharedElementMatrices(); 202 moveSharedElementsToOverlay(); 203 mResultReceiver.send(MSG_SHARED_ELEMENT_DESTINATION, state); 204 } 205 return true; 206 } 207 }); 208 } 209 if (allowOverlappingTransitions()) { 210 startEnterTransitionOnly(); 211 } 212 } 213 214 private static SharedElementListener getListener(Activity activity, boolean isReturning) { 215 return isReturning ? activity.mExitTransitionListener : activity.mEnterTransitionListener; 216 } 217 218 @Override 219 protected void onReceiveResult(int resultCode, Bundle resultData) { 220 switch (resultCode) { 221 case MSG_TAKE_SHARED_ELEMENTS: 222 if (!mIsCanceled) { 223 mSharedElementsBundle = resultData; 224 onTakeSharedElements(); 225 } 226 break; 227 case MSG_EXIT_TRANSITION_COMPLETE: 228 if (!mIsCanceled) { 229 mIsExitTransitionComplete = true; 230 if (mSharedElementTransitionStarted) { 231 onRemoteExitTransitionComplete(); 232 } 233 } 234 break; 235 case MSG_CANCEL: 236 cancel(); 237 break; 238 } 239 } 240 241 private void cancel() { 242 if (!mIsCanceled) { 243 mIsCanceled = true; 244 if (getViewsTransition() == null || mIsViewsTransitionStarted) { 245 setTransitionAlpha(mSharedElements, 1); 246 } else { 247 mTransitioningViews.addAll(mSharedElements); 248 } 249 mSharedElementNames.clear(); 250 mSharedElements.clear(); 251 mAllSharedElementNames.clear(); 252 startSharedElementTransition(null); 253 onRemoteExitTransitionComplete(); 254 } 255 } 256 257 public boolean isReturning() { 258 return mIsReturning; 259 } 260 261 protected void prepareEnter() { 262 mActivity.overridePendingTransition(0, 0); 263 if (!mIsReturning) { 264 mWasOpaque = mActivity.convertToTranslucent(null, null); 265 Drawable background = getDecor().getBackground(); 266 if (background != null) { 267 getWindow().setBackgroundDrawable(null); 268 background = background.mutate(); 269 background.setAlpha(0); 270 getWindow().setBackgroundDrawable(background); 271 } 272 } else { 273 mActivity = null; // all done with it now. 274 } 275 } 276 277 @Override 278 protected Transition getViewsTransition() { 279 if (mIsReturning) { 280 return getWindow().getReenterTransition(); 281 } else { 282 return getWindow().getEnterTransition(); 283 } 284 } 285 286 protected Transition getSharedElementTransition() { 287 if (mIsReturning) { 288 return getWindow().getSharedElementReenterTransition(); 289 } else { 290 return getWindow().getSharedElementEnterTransition(); 291 } 292 } 293 294 private void startSharedElementTransition(Bundle sharedElementState) { 295 // Remove rejected shared elements 296 ArrayList<String> rejectedNames = new ArrayList<String>(mAllSharedElementNames); 297 rejectedNames.removeAll(mSharedElementNames); 298 ArrayList<View> rejectedSnapshots = createSnapshots(sharedElementState, rejectedNames); 299 mListener.handleRejectedSharedElements(rejectedSnapshots); 300 startRejectedAnimations(rejectedSnapshots); 301 302 // Now start shared element transition 303 ArrayList<View> sharedElementSnapshots = createSnapshots(sharedElementState, 304 mSharedElementNames); 305 setTransitionAlpha(mSharedElements, 1); 306 scheduleSetSharedElementEnd(sharedElementSnapshots); 307 ArrayList<SharedElementOriginalState> originalImageViewState = 308 setSharedElementState(sharedElementState, sharedElementSnapshots); 309 requestLayoutForSharedElements(); 310 311 boolean startEnterTransition = allowOverlappingTransitions() && !mIsReturning; 312 boolean startSharedElementTransition = true; 313 setGhostVisibility(View.INVISIBLE); 314 scheduleGhostVisibilityChange(View.INVISIBLE); 315 Transition transition = beginTransition(startEnterTransition, startSharedElementTransition); 316 scheduleGhostVisibilityChange(View.VISIBLE); 317 setGhostVisibility(View.VISIBLE); 318 319 if (startEnterTransition) { 320 startEnterTransition(transition); 321 } 322 323 setOriginalSharedElementState(mSharedElements, originalImageViewState); 324 325 if (mResultReceiver != null) { 326 // We can't trust that the view will disappear on the same frame that the shared 327 // element appears here. Assure that we get at least 2 frames for double-buffering. 328 getDecor().postOnAnimation(new Runnable() { 329 int mAnimations; 330 @Override 331 public void run() { 332 if (mAnimations++ < MIN_ANIMATION_FRAMES) { 333 getDecor().postOnAnimation(this); 334 } else if (mResultReceiver != null) { 335 mResultReceiver.send(MSG_HIDE_SHARED_ELEMENTS, null); 336 mResultReceiver = null; // all done sending messages. 337 } 338 } 339 }); 340 } 341 } 342 343 private void onTakeSharedElements() { 344 if (!mIsReadyForTransition || mSharedElementsBundle == null) { 345 return; 346 } 347 final Bundle sharedElementState = mSharedElementsBundle; 348 mSharedElementsBundle = null; 349 final View decorView = getDecor(); 350 decorView.getViewTreeObserver() 351 .addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() { 352 @Override 353 public boolean onPreDraw() { 354 decorView.getViewTreeObserver().removeOnPreDrawListener(this); 355 startTransition(new Runnable() { 356 @Override 357 public void run() { 358 startSharedElementTransition(sharedElementState); 359 } 360 }); 361 return false; 362 } 363 }); 364 decorView.invalidate(); 365 } 366 367 private void requestLayoutForSharedElements() { 368 int numSharedElements = mSharedElements.size(); 369 for (int i = 0; i < numSharedElements; i++) { 370 mSharedElements.get(i).requestLayout(); 371 } 372 } 373 374 private Transition beginTransition(boolean startEnterTransition, 375 boolean startSharedElementTransition) { 376 Transition sharedElementTransition = null; 377 if (startSharedElementTransition) { 378 if (!mSharedElementNames.isEmpty()) { 379 sharedElementTransition = configureTransition(getSharedElementTransition(), false); 380 } 381 if (sharedElementTransition == null) { 382 sharedElementTransitionStarted(); 383 sharedElementTransitionComplete(); 384 } else { 385 sharedElementTransition.addListener(new Transition.TransitionListenerAdapter() { 386 @Override 387 public void onTransitionStart(Transition transition) { 388 sharedElementTransitionStarted(); 389 } 390 391 @Override 392 public void onTransitionEnd(Transition transition) { 393 transition.removeListener(this); 394 sharedElementTransitionComplete(); 395 } 396 }); 397 } 398 } 399 Transition viewsTransition = null; 400 if (startEnterTransition) { 401 mIsViewsTransitionStarted = true; 402 if (!mTransitioningViews.isEmpty()) { 403 viewsTransition = configureTransition(getViewsTransition(), true); 404 if (viewsTransition != null && !mIsReturning) { 405 stripOffscreenViews(); 406 } 407 } 408 if (viewsTransition == null) { 409 viewTransitionComplete(); 410 } else { 411 viewsTransition.forceVisibility(View.INVISIBLE, true); 412 viewsTransition.addListener(new ContinueTransitionListener() { 413 @Override 414 public void onTransitionStart(Transition transition) { 415 mEnterViewsTransition = transition; 416 setTransitionAlpha(mTransitioningViews, 1); 417 super.onTransitionStart(transition); 418 } 419 420 @Override 421 public void onTransitionEnd(Transition transition) { 422 mEnterViewsTransition = null; 423 transition.removeListener(this); 424 viewTransitionComplete(); 425 super.onTransitionEnd(transition); 426 } 427 }); 428 } 429 } 430 431 Transition transition = mergeTransitions(sharedElementTransition, viewsTransition); 432 if (transition != null) { 433 transition.addListener(new ContinueTransitionListener()); 434 TransitionManager.beginDelayedTransition(getDecor(), transition); 435 if (startSharedElementTransition && !mSharedElementNames.isEmpty()) { 436 mSharedElements.get(0).invalidate(); 437 } else if (startEnterTransition && !mTransitioningViews.isEmpty()) { 438 mTransitioningViews.get(0).invalidate(); 439 } 440 } else { 441 transitionStarted(); 442 } 443 return transition; 444 } 445 446 private void viewTransitionComplete() { 447 mIsViewsTransitionComplete = true; 448 if (mIsSharedElementTransitionComplete) { 449 moveSharedElementsFromOverlay(); 450 } 451 } 452 453 private void sharedElementTransitionComplete() { 454 mIsSharedElementTransitionComplete = true; 455 if (mIsViewsTransitionComplete) { 456 moveSharedElementsFromOverlay(); 457 } 458 } 459 460 private void sharedElementTransitionStarted() { 461 mSharedElementTransitionStarted = true; 462 if (mIsExitTransitionComplete) { 463 send(MSG_EXIT_TRANSITION_COMPLETE, null); 464 } 465 } 466 467 private void startEnterTransition(Transition transition) { 468 if (!mIsReturning) { 469 Drawable background = getDecor().getBackground(); 470 if (background != null) { 471 background = background.mutate(); 472 mBackgroundAnimator = ObjectAnimator.ofInt(background, "alpha", 255); 473 mBackgroundAnimator.setDuration(getFadeDuration()); 474 mBackgroundAnimator.addListener(new AnimatorListenerAdapter() { 475 @Override 476 public void onAnimationEnd(Animator animation) { 477 makeOpaque(); 478 } 479 }); 480 mBackgroundAnimator.start(); 481 } else if (transition != null) { 482 transition.addListener(new Transition.TransitionListenerAdapter() { 483 @Override 484 public void onTransitionEnd(Transition transition) { 485 transition.removeListener(this); 486 makeOpaque(); 487 } 488 }); 489 } else { 490 makeOpaque(); 491 } 492 } 493 } 494 495 public void stop() { 496 makeOpaque(); 497 mIsCanceled = true; 498 mResultReceiver = null; 499 if (mBackgroundAnimator != null) { 500 mBackgroundAnimator.end(); 501 mBackgroundAnimator = null; 502 } 503 mActivity = null; 504 moveSharedElementsFromOverlay(); 505 clearState(); 506 } 507 508 public void cancelEnter() { 509 setGhostVisibility(View.INVISIBLE); 510 mHasStopped = true; 511 mIsCanceled = true; 512 mResultReceiver = null; 513 if (mBackgroundAnimator != null) { 514 mBackgroundAnimator.cancel(); 515 mBackgroundAnimator = null; 516 } 517 mActivity = null; 518 clearState(); 519 } 520 521 private void makeOpaque() { 522 if (!mHasStopped && mActivity != null) { 523 if (mWasOpaque) { 524 mActivity.convertFromTranslucent(); 525 } 526 mActivity = null; 527 } 528 } 529 530 private boolean allowOverlappingTransitions() { 531 return mIsReturning ? getWindow().getAllowExitTransitionOverlap() 532 : getWindow().getAllowEnterTransitionOverlap(); 533 } 534 535 private void startRejectedAnimations(final ArrayList<View> rejectedSnapshots) { 536 if (rejectedSnapshots == null || rejectedSnapshots.isEmpty()) { 537 return; 538 } 539 ViewGroupOverlay overlay = getDecor().getOverlay(); 540 ObjectAnimator animator = null; 541 int numRejected = rejectedSnapshots.size(); 542 for (int i = 0; i < numRejected; i++) { 543 View snapshot = rejectedSnapshots.get(i); 544 overlay.add(snapshot); 545 animator = ObjectAnimator.ofFloat(snapshot, View.ALPHA, 1, 0); 546 animator.start(); 547 } 548 animator.addListener(new AnimatorListenerAdapter() { 549 @Override 550 public void onAnimationEnd(Animator animation) { 551 ViewGroupOverlay overlay = getDecor().getOverlay(); 552 int numRejected = rejectedSnapshots.size(); 553 for (int i = 0; i < numRejected; i++) { 554 overlay.remove(rejectedSnapshots.get(i)); 555 } 556 } 557 }); 558 } 559 560 protected void onRemoteExitTransitionComplete() { 561 if (!allowOverlappingTransitions()) { 562 startEnterTransitionOnly(); 563 } 564 } 565 566 private void startEnterTransitionOnly() { 567 startTransition(new Runnable() { 568 @Override 569 public void run() { 570 boolean startEnterTransition = true; 571 boolean startSharedElementTransition = false; 572 Transition transition = beginTransition(startEnterTransition, 573 startSharedElementTransition); 574 startEnterTransition(transition); 575 } 576 }); 577 } 578 579 private void setSharedElementMatrices() { 580 int numSharedElements = mSharedElements.size(); 581 if (numSharedElements > 0) { 582 mSharedElementParentMatrices = new ArrayList<Matrix>(numSharedElements); 583 } 584 for (int i = 0; i < numSharedElements; i++) { 585 View view = mSharedElements.get(i); 586 587 // Find the location in the view's parent 588 ViewGroup parent = (ViewGroup) view.getParent(); 589 Matrix matrix = new Matrix(); 590 parent.transformMatrixToLocal(matrix); 591 592 mSharedElementParentMatrices.add(matrix); 593 } 594 } 595 596 @Override 597 protected void getSharedElementParentMatrix(View view, Matrix matrix) { 598 int index = mSharedElementParentMatrices == null ? -1 : mSharedElements.indexOf(view); 599 if (index < 0) { 600 super.getSharedElementParentMatrix(view, matrix); 601 } else { 602 matrix.set(mSharedElementParentMatrices.get(index)); 603 } 604 } 605} 606