EnterTransitionCoordinator.java revision 7fa7da221ef10113cda33c1cdc5810fe5ed0e00b
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 decorView.getViewTreeObserver().addOnPreDrawListener( 74 new ViewTreeObserver.OnPreDrawListener() { 75 @Override 76 public boolean onPreDraw() { 77 if (mIsReadyForTransition) { 78 decorView.getViewTreeObserver().removeOnPreDrawListener(this); 79 } 80 return mIsReadyForTransition; 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 setTransitionAlpha(mSharedElements, 0); 116 if (getViewsTransition() != null) { 117 setTransitionAlpha(mTransitioningViews, 0); 118 } 119 if (mIsReturning) { 120 sendSharedElementDestination(); 121 } else { 122 setSharedElementMatrices(); 123 moveSharedElementsToOverlay(); 124 } 125 if (mSharedElementsBundle != null) { 126 onTakeSharedElements(); 127 } 128 } 129 130 private void triggerViewsReady(final ArrayMap<String, View> sharedElements) { 131 if (mAreViewsReady) { 132 return; 133 } 134 mAreViewsReady = true; 135 // Ensure the views have been laid out before capturing the views -- we need the epicenter. 136 if (sharedElements.isEmpty() || !sharedElements.valueAt(0).isLayoutRequested()) { 137 viewsReady(sharedElements); 138 } else { 139 final View sharedElement = sharedElements.valueAt(0); 140 sharedElement.getViewTreeObserver() 141 .addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() { 142 @Override 143 public boolean onPreDraw() { 144 sharedElement.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 getDecor().findNamedViews(sharedElements); 156 if (accepted != null) { 157 for (int i = 0; i < localNames.size(); i++) { 158 String localName = localNames.get(i); 159 String acceptedName = accepted.get(i); 160 if (localName != null && !localName.equals(acceptedName)) { 161 View view = sharedElements.remove(localName); 162 if (view != null) { 163 sharedElements.put(acceptedName, view); 164 } 165 } 166 } 167 } 168 return sharedElements; 169 } 170 171 private void sendSharedElementDestination() { 172 boolean allReady; 173 if (allowOverlappingTransitions() && getEnterViewsTransition() != null) { 174 allReady = false; 175 } else { 176 allReady = !getDecor().isLayoutRequested(); 177 if (allReady) { 178 for (int i = 0; i < mSharedElements.size(); i++) { 179 if (mSharedElements.get(i).isLayoutRequested()) { 180 allReady = false; 181 break; 182 } 183 } 184 } 185 } 186 if (allReady) { 187 Bundle state = captureSharedElementState(); 188 setSharedElementMatrices(); 189 moveSharedElementsToOverlay(); 190 mResultReceiver.send(MSG_SHARED_ELEMENT_DESTINATION, state); 191 } else { 192 final View decorView = getDecor(); 193 decorView.getViewTreeObserver() 194 .addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() { 195 @Override 196 public boolean onPreDraw() { 197 decorView.getViewTreeObserver().removeOnPreDrawListener(this); 198 if (mResultReceiver != null) { 199 Bundle state = captureSharedElementState(); 200 setSharedElementMatrices(); 201 moveSharedElementsToOverlay(); 202 mResultReceiver.send(MSG_SHARED_ELEMENT_DESTINATION, state); 203 } 204 return true; 205 } 206 }); 207 } 208 if (allowOverlappingTransitions()) { 209 startEnterTransitionOnly(); 210 } 211 } 212 213 private static SharedElementCallback getListener(Activity activity, boolean isReturning) { 214 return isReturning ? activity.mExitTransitionListener : activity.mEnterTransitionListener; 215 } 216 217 @Override 218 protected void onReceiveResult(int resultCode, Bundle resultData) { 219 switch (resultCode) { 220 case MSG_TAKE_SHARED_ELEMENTS: 221 if (!mIsCanceled) { 222 mSharedElementsBundle = resultData; 223 onTakeSharedElements(); 224 } 225 break; 226 case MSG_EXIT_TRANSITION_COMPLETE: 227 if (!mIsCanceled) { 228 mIsExitTransitionComplete = true; 229 if (mSharedElementTransitionStarted) { 230 onRemoteExitTransitionComplete(); 231 } 232 } 233 break; 234 case MSG_CANCEL: 235 cancel(); 236 break; 237 } 238 } 239 240 private void cancel() { 241 if (!mIsCanceled) { 242 mIsCanceled = true; 243 if (getViewsTransition() == null || mIsViewsTransitionStarted) { 244 setTransitionAlpha(mSharedElements, 1); 245 } else { 246 mTransitioningViews.addAll(mSharedElements); 247 } 248 mSharedElementNames.clear(); 249 mSharedElements.clear(); 250 mAllSharedElementNames.clear(); 251 startSharedElementTransition(null); 252 onRemoteExitTransitionComplete(); 253 } 254 } 255 256 public boolean isReturning() { 257 return mIsReturning; 258 } 259 260 protected void prepareEnter() { 261 mActivity.overridePendingTransition(0, 0); 262 if (!mIsReturning) { 263 mWasOpaque = mActivity.convertToTranslucent(null, null); 264 Drawable background = getDecor().getBackground(); 265 if (background != null) { 266 getWindow().setBackgroundDrawable(null); 267 background = background.mutate(); 268 background.setAlpha(0); 269 getWindow().setBackgroundDrawable(background); 270 } 271 } else { 272 mActivity = null; // all done with it now. 273 } 274 } 275 276 @Override 277 protected Transition getViewsTransition() { 278 if (mIsReturning) { 279 return getWindow().getReenterTransition(); 280 } else { 281 return getWindow().getEnterTransition(); 282 } 283 } 284 285 protected Transition getSharedElementTransition() { 286 if (mIsReturning) { 287 return getWindow().getSharedElementReenterTransition(); 288 } else { 289 return getWindow().getSharedElementEnterTransition(); 290 } 291 } 292 293 private void startSharedElementTransition(Bundle sharedElementState) { 294 // Remove rejected shared elements 295 ArrayList<String> rejectedNames = new ArrayList<String>(mAllSharedElementNames); 296 rejectedNames.removeAll(mSharedElementNames); 297 ArrayList<View> rejectedSnapshots = createSnapshots(sharedElementState, rejectedNames); 298 mListener.onRejectSharedElements(rejectedSnapshots); 299 startRejectedAnimations(rejectedSnapshots); 300 301 // Now start shared element transition 302 ArrayList<View> sharedElementSnapshots = createSnapshots(sharedElementState, 303 mSharedElementNames); 304 setTransitionAlpha(mSharedElements, 1); 305 scheduleSetSharedElementEnd(sharedElementSnapshots); 306 ArrayList<SharedElementOriginalState> originalImageViewState = 307 setSharedElementState(sharedElementState, sharedElementSnapshots); 308 requestLayoutForSharedElements(); 309 310 boolean startEnterTransition = allowOverlappingTransitions() && !mIsReturning; 311 boolean startSharedElementTransition = true; 312 setGhostVisibility(View.INVISIBLE); 313 scheduleGhostVisibilityChange(View.INVISIBLE); 314 Transition transition = beginTransition(startEnterTransition, startSharedElementTransition); 315 scheduleGhostVisibilityChange(View.VISIBLE); 316 setGhostVisibility(View.VISIBLE); 317 318 if (startEnterTransition) { 319 startEnterTransition(transition); 320 } 321 322 setOriginalSharedElementState(mSharedElements, originalImageViewState); 323 324 if (mResultReceiver != null) { 325 // We can't trust that the view will disappear on the same frame that the shared 326 // element appears here. Assure that we get at least 2 frames for double-buffering. 327 getDecor().postOnAnimation(new Runnable() { 328 int mAnimations; 329 @Override 330 public void run() { 331 if (mAnimations++ < MIN_ANIMATION_FRAMES) { 332 getDecor().postOnAnimation(this); 333 } else if (mResultReceiver != null) { 334 mResultReceiver.send(MSG_HIDE_SHARED_ELEMENTS, null); 335 mResultReceiver = null; // all done sending messages. 336 } 337 } 338 }); 339 } 340 } 341 342 private void onTakeSharedElements() { 343 if (!mIsReadyForTransition || mSharedElementsBundle == null) { 344 return; 345 } 346 final Bundle sharedElementState = mSharedElementsBundle; 347 mSharedElementsBundle = null; 348 final View decorView = getDecor(); 349 decorView.getViewTreeObserver() 350 .addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() { 351 @Override 352 public boolean onPreDraw() { 353 decorView.getViewTreeObserver().removeOnPreDrawListener(this); 354 startTransition(new Runnable() { 355 @Override 356 public void run() { 357 startSharedElementTransition(sharedElementState); 358 } 359 }); 360 return false; 361 } 362 }); 363 decorView.invalidate(); 364 } 365 366 private void requestLayoutForSharedElements() { 367 int numSharedElements = mSharedElements.size(); 368 for (int i = 0; i < numSharedElements; i++) { 369 mSharedElements.get(i).requestLayout(); 370 } 371 } 372 373 private Transition beginTransition(boolean startEnterTransition, 374 boolean startSharedElementTransition) { 375 Transition sharedElementTransition = null; 376 if (startSharedElementTransition) { 377 if (!mSharedElementNames.isEmpty()) { 378 sharedElementTransition = configureTransition(getSharedElementTransition(), false); 379 } 380 if (sharedElementTransition == null) { 381 sharedElementTransitionStarted(); 382 sharedElementTransitionComplete(); 383 } else { 384 sharedElementTransition.addListener(new Transition.TransitionListenerAdapter() { 385 @Override 386 public void onTransitionStart(Transition transition) { 387 sharedElementTransitionStarted(); 388 } 389 390 @Override 391 public void onTransitionEnd(Transition transition) { 392 transition.removeListener(this); 393 sharedElementTransitionComplete(); 394 } 395 }); 396 } 397 } 398 Transition viewsTransition = null; 399 if (startEnterTransition) { 400 mIsViewsTransitionStarted = true; 401 if (!mTransitioningViews.isEmpty()) { 402 viewsTransition = configureTransition(getViewsTransition(), true); 403 if (viewsTransition != null && !mIsReturning) { 404 stripOffscreenViews(); 405 } 406 } 407 if (viewsTransition == null) { 408 viewTransitionComplete(); 409 } else { 410 viewsTransition.forceVisibility(View.INVISIBLE, true); 411 viewsTransition.addListener(new ContinueTransitionListener() { 412 @Override 413 public void onTransitionStart(Transition transition) { 414 mEnterViewsTransition = transition; 415 setTransitionAlpha(mTransitioningViews, 1); 416 super.onTransitionStart(transition); 417 } 418 419 @Override 420 public void onTransitionEnd(Transition transition) { 421 mEnterViewsTransition = null; 422 transition.removeListener(this); 423 viewTransitionComplete(); 424 super.onTransitionEnd(transition); 425 } 426 }); 427 } 428 } 429 430 Transition transition = mergeTransitions(sharedElementTransition, viewsTransition); 431 if (transition != null) { 432 transition.addListener(new ContinueTransitionListener()); 433 TransitionManager.beginDelayedTransition(getDecor(), transition); 434 if (startSharedElementTransition && !mSharedElementNames.isEmpty()) { 435 mSharedElements.get(0).invalidate(); 436 } else if (startEnterTransition && !mTransitioningViews.isEmpty()) { 437 mTransitioningViews.get(0).invalidate(); 438 } 439 } else { 440 transitionStarted(); 441 } 442 return transition; 443 } 444 445 private void viewTransitionComplete() { 446 mIsViewsTransitionComplete = true; 447 if (mIsSharedElementTransitionComplete) { 448 moveSharedElementsFromOverlay(); 449 } 450 } 451 452 private void sharedElementTransitionComplete() { 453 mIsSharedElementTransitionComplete = true; 454 if (mIsViewsTransitionComplete) { 455 moveSharedElementsFromOverlay(); 456 } 457 } 458 459 private void sharedElementTransitionStarted() { 460 mSharedElementTransitionStarted = true; 461 if (mIsExitTransitionComplete) { 462 send(MSG_EXIT_TRANSITION_COMPLETE, null); 463 } 464 } 465 466 private void startEnterTransition(Transition transition) { 467 if (!mIsReturning) { 468 Drawable background = getDecor().getBackground(); 469 if (background != null) { 470 background = background.mutate(); 471 getWindow().setBackgroundDrawable(background); 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