EnterTransitionCoordinator.java revision 82ffc141f50bedd7d0af4352e6b6370f4852f752
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.transition.Transition; 28import android.transition.TransitionManager; 29import android.util.ArrayMap; 30import android.util.Pair; 31import android.view.View; 32import android.view.ViewGroup; 33import android.view.ViewGroupOverlay; 34import android.view.ViewTreeObserver; 35import android.widget.ImageView; 36 37import java.util.ArrayList; 38 39/** 40 * This ActivityTransitionCoordinator is created by the Activity to manage 41 * the enter scene and shared element transfer into the Scene, either during 42 * launch of an Activity or returning from a launched Activity. 43 */ 44class EnterTransitionCoordinator extends ActivityTransitionCoordinator { 45 private static final String TAG = "EnterTransitionCoordinator"; 46 47 private static final long MAX_WAIT_MS = 1000; 48 private static final int MIN_ANIMATION_FRAMES = 2; 49 50 private boolean mSharedElementTransitionStarted; 51 private Activity mActivity; 52 private boolean mHasStopped; 53 private Handler mHandler; 54 private boolean mIsCanceled; 55 private ObjectAnimator mBackgroundAnimator; 56 private boolean mIsExitTransitionComplete; 57 private boolean mIsReadyForTransition; 58 private Bundle mSharedElementsBundle; 59 private boolean mWasOpaque; 60 private boolean mAreViewsReady; 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 getDecor().getViewTreeObserver().addOnPreDrawListener( 73 new ViewTreeObserver.OnPreDrawListener() { 74 @Override 75 public boolean onPreDraw() { 76 if (mIsReadyForTransition) { 77 getDecor().getViewTreeObserver().removeOnPreDrawListener(this); 78 } 79 return mIsReadyForTransition; 80 } 81 }); 82 } 83 84 public void viewInstancesReady(ArrayList<String> accepted, ArrayList<View> localViews) { 85 triggerViewsReady(mapSharedElements(accepted, localViews)); 86 } 87 88 public void namedViewsReady(ArrayList<String> accepted, ArrayList<String> localNames) { 89 triggerViewsReady(mapNamedElements(accepted, localNames)); 90 } 91 92 @Override 93 protected void viewsReady(ArrayMap<String, View> sharedElements) { 94 super.viewsReady(sharedElements); 95 mIsReadyForTransition = true; 96 if (mIsReturning) { 97 mHandler = new Handler() { 98 @Override 99 public void handleMessage(Message msg) { 100 cancel(); 101 } 102 }; 103 mHandler.sendEmptyMessageDelayed(MSG_CANCEL, MAX_WAIT_MS); 104 send(MSG_SEND_SHARED_ELEMENT_DESTINATION, null); 105 } 106 setViewVisibility(mSharedElements, View.INVISIBLE); 107 if (getViewsTransition() != null) { 108 setViewVisibility(mTransitioningViews, View.INVISIBLE); 109 } 110 if (mSharedElementsBundle != null) { 111 onTakeSharedElements(); 112 } 113 } 114 115 private void triggerViewsReady(final ArrayMap<String, View> sharedElements) { 116 if (mAreViewsReady) { 117 return; 118 } 119 mAreViewsReady = true; 120 // Ensure the views have been laid out before capturing the views -- we need the epicenter. 121 if (sharedElements.isEmpty() || !sharedElements.valueAt(0).isLayoutRequested()) { 122 viewsReady(sharedElements); 123 } else { 124 sharedElements.valueAt(0).getViewTreeObserver() 125 .addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() { 126 @Override 127 public boolean onPreDraw() { 128 sharedElements.valueAt(0).getViewTreeObserver().removeOnPreDrawListener(this); 129 viewsReady(sharedElements); 130 return true; 131 } 132 }); 133 } 134 } 135 136 private ArrayMap<String, View> mapNamedElements(ArrayList<String> accepted, 137 ArrayList<String> localNames) { 138 ArrayMap<String, View> sharedElements = new ArrayMap<String, View>(); 139 getDecor().findNamedViews(sharedElements); 140 if (accepted != null) { 141 for (int i = 0; i < localNames.size(); i++) { 142 String localName = localNames.get(i); 143 String acceptedName = accepted.get(i); 144 if (localName != null && !localName.equals(acceptedName)) { 145 View view = sharedElements.remove(localName); 146 if (view != null) { 147 sharedElements.put(acceptedName, view); 148 } 149 } 150 } 151 } 152 return sharedElements; 153 } 154 155 private void sendSharedElementDestination() { 156 ViewGroup decor = getDecor(); 157 boolean allReady = !decor.isLayoutRequested(); 158 if (allReady) { 159 for (int i = 0; i < mSharedElements.size(); i++) { 160 if (mSharedElements.get(i).isLayoutRequested()) { 161 allReady = false; 162 break; 163 } 164 } 165 } 166 if (allReady) { 167 Bundle state = captureSharedElementState(); 168 mResultReceiver.send(MSG_SHARED_ELEMENT_DESTINATION, state); 169 } else { 170 getDecor().getViewTreeObserver() 171 .addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() { 172 @Override 173 public boolean onPreDraw() { 174 getDecor().getViewTreeObserver().removeOnPreDrawListener(this); 175 Bundle state = captureSharedElementState(); 176 mResultReceiver.send(MSG_SHARED_ELEMENT_DESTINATION, state); 177 return true; 178 } 179 }); 180 } 181 if (allowOverlappingTransitions()) { 182 startEnterTransitionOnly(); 183 } 184 } 185 186 private static SharedElementListener getListener(Activity activity, boolean isReturning) { 187 return isReturning ? activity.mExitTransitionListener : activity.mEnterTransitionListener; 188 } 189 190 @Override 191 protected void onReceiveResult(int resultCode, Bundle resultData) { 192 switch (resultCode) { 193 case MSG_TAKE_SHARED_ELEMENTS: 194 if (!mIsCanceled) { 195 if (mHandler != null) { 196 mHandler.removeMessages(MSG_CANCEL); 197 } 198 mSharedElementsBundle = resultData; 199 onTakeSharedElements(); 200 } 201 break; 202 case MSG_EXIT_TRANSITION_COMPLETE: 203 if (!mIsCanceled) { 204 mIsExitTransitionComplete = true; 205 if (mSharedElementTransitionStarted) { 206 onRemoteExitTransitionComplete(); 207 } 208 } 209 break; 210 case MSG_CANCEL: 211 cancel(); 212 break; 213 case MSG_SEND_SHARED_ELEMENT_DESTINATION: 214 sendSharedElementDestination(); 215 break; 216 } 217 } 218 219 private void cancel() { 220 if (!mIsCanceled) { 221 mIsCanceled = true; 222 if (getViewsTransition() == null) { 223 setViewVisibility(mSharedElements, View.VISIBLE); 224 } else { 225 mTransitioningViews.addAll(mSharedElements); 226 } 227 mSharedElementNames.clear(); 228 mSharedElements.clear(); 229 mAllSharedElementNames.clear(); 230 startSharedElementTransition(null); 231 onRemoteExitTransitionComplete(); 232 } 233 } 234 235 public boolean isReturning() { 236 return mIsReturning; 237 } 238 239 protected void prepareEnter() { 240 mActivity.overridePendingTransition(0, 0); 241 if (!mIsReturning) { 242 mWasOpaque = mActivity.convertToTranslucent(null, null); 243 Drawable background = getDecor().getBackground(); 244 if (background != null) { 245 getWindow().setBackgroundDrawable(null); 246 background = background.mutate(); 247 background.setAlpha(0); 248 getWindow().setBackgroundDrawable(background); 249 } 250 } else { 251 mActivity = null; // all done with it now. 252 } 253 } 254 255 @Override 256 protected Transition getViewsTransition() { 257 if (mIsReturning) { 258 return getWindow().getExitTransition(); 259 } else { 260 return getWindow().getEnterTransition(); 261 } 262 } 263 264 protected Transition getSharedElementTransition() { 265 if (mIsReturning) { 266 return getWindow().getSharedElementExitTransition(); 267 } else { 268 return getWindow().getSharedElementEnterTransition(); 269 } 270 } 271 272 private void startSharedElementTransition(Bundle sharedElementState) { 273 // Remove rejected shared elements 274 ArrayList<String> rejectedNames = new ArrayList<String>(mAllSharedElementNames); 275 rejectedNames.removeAll(mSharedElementNames); 276 ArrayList<View> rejectedSnapshots = createSnapshots(sharedElementState, rejectedNames); 277 mListener.handleRejectedSharedElements(rejectedSnapshots); 278 startRejectedAnimations(rejectedSnapshots); 279 280 // Now start shared element transition 281 ArrayList<View> sharedElementSnapshots = createSnapshots(sharedElementState, 282 mSharedElementNames); 283 setViewVisibility(mSharedElements, View.VISIBLE); 284 ArrayMap<ImageView, Pair<ImageView.ScaleType, Matrix>> originalImageViewState = 285 setSharedElementState(sharedElementState, sharedElementSnapshots); 286 requestLayoutForSharedElements(); 287 288 boolean startEnterTransition = allowOverlappingTransitions() && !mIsReturning; 289 boolean startSharedElementTransition = true; 290 Transition transition = beginTransition(startEnterTransition, startSharedElementTransition); 291 292 if (startEnterTransition) { 293 startEnterTransition(transition); 294 } 295 296 setOriginalImageViewState(originalImageViewState); 297 298 if (mResultReceiver != null) { 299 // We can't trust that the view will disappear on the same frame that the shared 300 // element appears here. Assure that we get at least 2 frames for double-buffering. 301 getDecor().postOnAnimation(new Runnable() { 302 int mAnimations; 303 @Override 304 public void run() { 305 if (mAnimations++ < MIN_ANIMATION_FRAMES) { 306 getDecor().postOnAnimation(this); 307 } else { 308 mResultReceiver.send(MSG_HIDE_SHARED_ELEMENTS, null); 309 mResultReceiver = null; // all done sending messages. 310 } 311 } 312 }); 313 } 314 } 315 316 @Override 317 protected void stripOffscreenViews() { 318 setViewVisibility(mTransitioningViews, View.VISIBLE); 319 super.stripOffscreenViews(); 320 setViewVisibility(mTransitioningViews, View.INVISIBLE); 321 } 322 323 private void onTakeSharedElements() { 324 if (!mIsReadyForTransition || mSharedElementsBundle == null) { 325 return; 326 } 327 final Bundle sharedElementState = mSharedElementsBundle; 328 mSharedElementsBundle = null; 329 getDecor().getViewTreeObserver() 330 .addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() { 331 @Override 332 public boolean onPreDraw() { 333 getDecor().getViewTreeObserver().removeOnPreDrawListener(this); 334 startTransition(new Runnable() { 335 @Override 336 public void run() { 337 startSharedElementTransition(sharedElementState); 338 } 339 }); 340 return false; 341 } 342 }); 343 getDecor().invalidate(); 344 } 345 346 private void requestLayoutForSharedElements() { 347 int numSharedElements = mSharedElements.size(); 348 for (int i = 0; i < numSharedElements; i++) { 349 mSharedElements.get(i).requestLayout(); 350 } 351 } 352 353 private Transition beginTransition(boolean startEnterTransition, 354 boolean startSharedElementTransition) { 355 Transition sharedElementTransition = null; 356 if (startSharedElementTransition && !mSharedElementNames.isEmpty()) { 357 sharedElementTransition = configureTransition(getSharedElementTransition(), false); 358 } 359 Transition viewsTransition = null; 360 if (startEnterTransition && !mTransitioningViews.isEmpty()) { 361 viewsTransition = configureTransition(getViewsTransition(), true); 362 if (viewsTransition != null) { 363 stripOffscreenViews(); 364 } 365 } 366 367 Transition transition = mergeTransitions(sharedElementTransition, viewsTransition); 368 if (startSharedElementTransition) { 369 if (transition == null) { 370 sharedElementTransitionStarted(); 371 } else { 372 transition.addListener(new ContinueTransitionListener() { 373 @Override 374 public void onTransitionStart(Transition transition) { 375 super.onTransitionStart(transition); 376 transition.removeListener(this); 377 sharedElementTransitionStarted(); 378 } 379 }); 380 } 381 } 382 if (transition != null) { 383 if (sharedElementTransition == null) { 384 transition.addListener(new ContinueTransitionListener()); 385 } 386 TransitionManager.beginDelayedTransition(getDecor(), transition); 387 if (startSharedElementTransition && !mSharedElementNames.isEmpty()) { 388 mSharedElements.get(0).invalidate(); 389 } else if (startEnterTransition && !mTransitioningViews.isEmpty()) { 390 mTransitioningViews.get(0).invalidate(); 391 } 392 } else { 393 transitionStarted(); 394 } 395 return transition; 396 } 397 398 private void sharedElementTransitionStarted() { 399 mSharedElementTransitionStarted = true; 400 if (mIsExitTransitionComplete) { 401 send(MSG_EXIT_TRANSITION_COMPLETE, null); 402 } 403 } 404 405 private void startEnterTransition(Transition transition) { 406 setViewVisibility(mTransitioningViews, View.VISIBLE); 407 if (!mIsReturning) { 408 Drawable background = getDecor().getBackground(); 409 if (background != null) { 410 background = background.mutate(); 411 mBackgroundAnimator = ObjectAnimator.ofInt(background, "alpha", 255); 412 mBackgroundAnimator.setDuration(getFadeDuration()); 413 mBackgroundAnimator.addListener(new AnimatorListenerAdapter() { 414 @Override 415 public void onAnimationEnd(Animator animation) { 416 makeOpaque(); 417 } 418 }); 419 mBackgroundAnimator.start(); 420 } else if (transition != null) { 421 transition.addListener(new Transition.TransitionListenerAdapter() { 422 @Override 423 public void onTransitionEnd(Transition transition) { 424 transition.removeListener(this); 425 makeOpaque(); 426 } 427 }); 428 } else { 429 makeOpaque(); 430 } 431 } 432 } 433 434 public void stop() { 435 makeOpaque(); 436 mHasStopped = true; 437 mIsCanceled = true; 438 mResultReceiver = null; 439 if (mBackgroundAnimator != null) { 440 mBackgroundAnimator.cancel(); 441 mBackgroundAnimator = null; 442 } 443 } 444 445 private void makeOpaque() { 446 if (!mHasStopped && mActivity != null) { 447 if (mWasOpaque) { 448 mActivity.convertFromTranslucent(); 449 } 450 mActivity = null; 451 } 452 } 453 454 private boolean allowOverlappingTransitions() { 455 return mIsReturning ? getWindow().getAllowExitTransitionOverlap() 456 : getWindow().getAllowEnterTransitionOverlap(); 457 } 458 459 private void startRejectedAnimations(final ArrayList<View> rejectedSnapshots) { 460 if (rejectedSnapshots == null || rejectedSnapshots.isEmpty()) { 461 return; 462 } 463 ViewGroupOverlay overlay = getDecor().getOverlay(); 464 ObjectAnimator animator = null; 465 int numRejected = rejectedSnapshots.size(); 466 for (int i = 0; i < numRejected; i++) { 467 View snapshot = rejectedSnapshots.get(i); 468 overlay.add(snapshot); 469 animator = ObjectAnimator.ofFloat(snapshot, View.ALPHA, 1, 0); 470 animator.start(); 471 } 472 animator.addListener(new AnimatorListenerAdapter() { 473 @Override 474 public void onAnimationEnd(Animator animation) { 475 ViewGroupOverlay overlay = getDecor().getOverlay(); 476 int numRejected = rejectedSnapshots.size(); 477 for (int i = 0; i < numRejected; i++) { 478 overlay.remove(rejectedSnapshots.get(i)); 479 } 480 } 481 }); 482 } 483 484 protected void onRemoteExitTransitionComplete() { 485 if (!allowOverlappingTransitions()) { 486 startEnterTransitionOnly(); 487 } 488 } 489 490 private void startEnterTransitionOnly() { 491 startTransition(new Runnable() { 492 @Override 493 public void run() { 494 boolean startEnterTransition = true; 495 boolean startSharedElementTransition = false; 496 Transition transition = beginTransition(startEnterTransition, 497 startSharedElementTransition); 498 startEnterTransition(transition); 499 } 500 }); 501 } 502} 503