2 * Copyright (C) 2014 The Android Open Source Project
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
8 * http://www.apache.org/licenses/LICENSE-2.0
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.
18 import android.animation.Animator;
19 import android.animation.AnimatorListenerAdapter;
20 import android.animation.ObjectAnimator;
21 import android.app.SharedElementCallback.OnSharedElementsReadyListener;
22 import android.graphics.drawable.Drawable;
23 import android.os.Bundle;
24 import android.os.ResultReceiver;
25 import android.text.TextUtils;
26 import android.transition.Transition;
27 import android.transition.TransitionManager;
28 import android.util.ArrayMap;
29 import android.view.View;
30 import android.view.ViewGroup;
31 import android.view.ViewGroupOverlay;
32 import android.view.ViewTreeObserver;
33 import android.view.ViewTreeObserver.OnPreDrawListener;
34 import android.view.Window;
35 import android.view.accessibility.AccessibilityEvent;
37 import java.util.ArrayList;
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.
44 class EnterTransitionCoordinator extends ActivityTransitionCoordinator {
45 private static final String TAG = "EnterTransitionCoordinator";
47 private static final int MIN_ANIMATION_FRAMES = 2;
49 private boolean mSharedElementTransitionStarted;
50 private Activity mActivity;
51 private boolean mHasStopped;
52 private boolean mIsCanceled;
53 private ObjectAnimator mBackgroundAnimator;
54 private boolean mIsExitTransitionComplete;
55 private boolean mIsReadyForTransition;
56 private Bundle mSharedElementsBundle;
57 private boolean mWasOpaque;
58 private boolean mAreViewsReady;
59 private boolean mIsViewsTransitionStarted;
60 private Transition mEnterViewsTransition;
61 private OnPreDrawListener mViewsReadyListener;
63 public EnterTransitionCoordinator(Activity activity, ResultReceiver resultReceiver,
64 ArrayList<String> sharedElementNames, boolean isReturning) {
65 super(activity.getWindow(), sharedElementNames,
66 getListener(activity, isReturning), isReturning);
68 setResultReceiver(resultReceiver);
70 Bundle resultReceiverBundle = new Bundle();
71 resultReceiverBundle.putParcelable(KEY_REMOTE_RECEIVER, this);
72 mResultReceiver.send(MSG_SET_REMOTE_RECEIVER, resultReceiverBundle);
73 final View decorView = getDecor();
74 if (decorView != null) {
75 decorView.getViewTreeObserver().addOnPreDrawListener(
76 new ViewTreeObserver.OnPreDrawListener() {
78 public boolean onPreDraw() {
79 if (mIsReadyForTransition) {
80 decorView.getViewTreeObserver().removeOnPreDrawListener(this);
82 return mIsReadyForTransition;
88 public void viewInstancesReady(ArrayList<String> accepted, ArrayList<String> localNames,
89 ArrayList<View> localViews) {
90 boolean remap = false;
91 for (int i = 0; i < localViews.size(); i++) {
92 View view = localViews.get(i);
93 if (!TextUtils.equals(view.getTransitionName(), localNames.get(i))
94 || !view.isAttachedToWindow()) {
100 triggerViewsReady(mapNamedElements(accepted, localNames));
102 triggerViewsReady(mapSharedElements(accepted, localViews));
106 public void namedViewsReady(ArrayList<String> accepted, ArrayList<String> localNames) {
107 triggerViewsReady(mapNamedElements(accepted, localNames));
110 public Transition getEnterViewsTransition() {
111 return mEnterViewsTransition;
115 protected void viewsReady(ArrayMap<String, View> sharedElements) {
116 super.viewsReady(sharedElements);
117 mIsReadyForTransition = true;
118 hideViews(mSharedElements);
119 if (getViewsTransition() != null && mTransitioningViews != null) {
120 hideViews(mTransitioningViews);
123 sendSharedElementDestination();
125 moveSharedElementsToOverlay();
127 if (mSharedElementsBundle != null) {
128 onTakeSharedElements();
132 private void triggerViewsReady(final ArrayMap<String, View> sharedElements) {
133 if (mAreViewsReady) {
136 mAreViewsReady = true;
137 final ViewGroup decor = getDecor();
138 // Ensure the views have been laid out before capturing the views -- we need the epicenter.
139 if (decor == null || (decor.isAttachedToWindow() &&
140 (sharedElements.isEmpty() || !sharedElements.valueAt(0).isLayoutRequested()))) {
141 viewsReady(sharedElements);
143 mViewsReadyListener = new ViewTreeObserver.OnPreDrawListener() {
145 public boolean onPreDraw() {
146 mViewsReadyListener = null;
147 decor.getViewTreeObserver().removeOnPreDrawListener(this);
148 viewsReady(sharedElements);
152 decor.getViewTreeObserver().addOnPreDrawListener(mViewsReadyListener);
157 private ArrayMap<String, View> mapNamedElements(ArrayList<String> accepted,
158 ArrayList<String> localNames) {
159 ArrayMap<String, View> sharedElements = new ArrayMap<String, View>();
160 ViewGroup decorView = getDecor();
161 if (decorView != null) {
162 decorView.findNamedViews(sharedElements);
164 if (accepted != null) {
165 for (int i = 0; i < localNames.size(); i++) {
166 String localName = localNames.get(i);
167 String acceptedName = accepted.get(i);
168 if (localName != null && !localName.equals(acceptedName)) {
169 View view = sharedElements.remove(localName);
171 sharedElements.put(acceptedName, view);
176 return sharedElements;
179 private void sendSharedElementDestination() {
181 final View decorView = getDecor();
182 if (allowOverlappingTransitions() && getEnterViewsTransition() != null) {
184 } else if (decorView == null) {
187 allReady = !decorView.isLayoutRequested();
189 for (int i = 0; i < mSharedElements.size(); i++) {
190 if (mSharedElements.get(i).isLayoutRequested()) {
198 Bundle state = captureSharedElementState();
199 moveSharedElementsToOverlay();
200 mResultReceiver.send(MSG_SHARED_ELEMENT_DESTINATION, state);
201 } else if (decorView != null) {
202 decorView.getViewTreeObserver()
203 .addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() {
205 public boolean onPreDraw() {
206 decorView.getViewTreeObserver().removeOnPreDrawListener(this);
207 if (mResultReceiver != null) {
208 Bundle state = captureSharedElementState();
209 moveSharedElementsToOverlay();
210 mResultReceiver.send(MSG_SHARED_ELEMENT_DESTINATION, state);
216 if (allowOverlappingTransitions()) {
217 startEnterTransitionOnly();
221 private static SharedElementCallback getListener(Activity activity, boolean isReturning) {
222 return isReturning ? activity.mExitTransitionListener : activity.mEnterTransitionListener;
226 protected void onReceiveResult(int resultCode, Bundle resultData) {
227 switch (resultCode) {
228 case MSG_TAKE_SHARED_ELEMENTS:
230 mSharedElementsBundle = resultData;
231 onTakeSharedElements();
234 case MSG_EXIT_TRANSITION_COMPLETE:
236 mIsExitTransitionComplete = true;
237 if (mSharedElementTransitionStarted) {
238 onRemoteExitTransitionComplete();
248 public boolean isWaitingForRemoteExit() {
249 return mIsReturning && mResultReceiver != null;
253 * This is called onResume. If an Activity is resuming and the transitions
254 * haven't started yet, force the views to appear. This is likely to be
255 * caused by the top Activity finishing before the transitions started.
256 * In that case, we can finish any transition that was started, but we
257 * should cancel any pending transition and just bring those Views visible.
259 public void forceViewsToAppear() {
263 if (!mIsReadyForTransition) {
264 mIsReadyForTransition = true;
265 final ViewGroup decor = getDecor();
266 if (decor != null && mViewsReadyListener != null) {
267 decor.getViewTreeObserver().removeOnPreDrawListener(mViewsReadyListener);
268 mViewsReadyListener = null;
270 showViews(mTransitioningViews, true);
271 setTransitioningViewsVisiblity(View.VISIBLE, true);
272 mSharedElements.clear();
273 mAllSharedElementNames.clear();
274 mTransitioningViews.clear();
275 mIsReadyForTransition = true;
276 viewsTransitionComplete();
277 sharedElementTransitionComplete();
279 if (!mSharedElementTransitionStarted) {
280 moveSharedElementsFromOverlay();
281 mSharedElementTransitionStarted = true;
282 showViews(mSharedElements, true);
283 mSharedElements.clear();
284 sharedElementTransitionComplete();
286 if (!mIsViewsTransitionStarted) {
287 mIsViewsTransitionStarted = true;
288 showViews(mTransitioningViews, true);
289 setTransitioningViewsVisiblity(View.VISIBLE, true);
290 mTransitioningViews.clear();
291 viewsTransitionComplete();
293 cancelPendingTransitions();
295 mAreViewsReady = true;
296 if (mResultReceiver != null) {
297 mResultReceiver.send(MSG_CANCEL, null);
298 mResultReceiver = null;
302 private void cancel() {
305 if (getViewsTransition() == null || mIsViewsTransitionStarted) {
306 showViews(mSharedElements, true);
307 } else if (mTransitioningViews != null) {
308 mTransitioningViews.addAll(mSharedElements);
310 moveSharedElementsFromOverlay();
311 mSharedElementNames.clear();
312 mSharedElements.clear();
313 mAllSharedElementNames.clear();
314 startSharedElementTransition(null);
315 onRemoteExitTransitionComplete();
319 public boolean isReturning() {
323 protected void prepareEnter() {
324 ViewGroup decorView = getDecor();
325 if (mActivity == null || decorView == null) {
328 mActivity.overridePendingTransition(0, 0);
330 mWasOpaque = mActivity.convertToTranslucent(null, null);
331 Drawable background = decorView.getBackground();
332 if (background != null) {
333 getWindow().setBackgroundDrawable(null);
334 background = background.mutate();
335 background.setAlpha(0);
336 getWindow().setBackgroundDrawable(background);
339 mActivity = null; // all done with it now.
344 protected Transition getViewsTransition() {
345 Window window = getWindow();
346 if (window == null) {
350 return window.getReenterTransition();
352 return window.getEnterTransition();
356 protected Transition getSharedElementTransition() {
357 Window window = getWindow();
358 if (window == null) {
362 return window.getSharedElementReenterTransition();
364 return window.getSharedElementEnterTransition();
368 private void startSharedElementTransition(Bundle sharedElementState) {
369 ViewGroup decorView = getDecor();
370 if (decorView == null) {
373 // Remove rejected shared elements
374 ArrayList<String> rejectedNames = new ArrayList<String>(mAllSharedElementNames);
375 rejectedNames.removeAll(mSharedElementNames);
376 ArrayList<View> rejectedSnapshots = createSnapshots(sharedElementState, rejectedNames);
377 if (mListener != null) {
378 mListener.onRejectSharedElements(rejectedSnapshots);
380 removeNullViews(rejectedSnapshots);
381 startRejectedAnimations(rejectedSnapshots);
383 // Now start shared element transition
384 ArrayList<View> sharedElementSnapshots = createSnapshots(sharedElementState,
385 mSharedElementNames);
386 showViews(mSharedElements, true);
387 scheduleSetSharedElementEnd(sharedElementSnapshots);
388 ArrayList<SharedElementOriginalState> originalImageViewState =
389 setSharedElementState(sharedElementState, sharedElementSnapshots);
390 requestLayoutForSharedElements();
392 boolean startEnterTransition = allowOverlappingTransitions() && !mIsReturning;
393 boolean startSharedElementTransition = true;
394 setGhostVisibility(View.INVISIBLE);
395 scheduleGhostVisibilityChange(View.INVISIBLE);
397 Transition transition = beginTransition(decorView, startEnterTransition,
398 startSharedElementTransition);
399 scheduleGhostVisibilityChange(View.VISIBLE);
400 setGhostVisibility(View.VISIBLE);
402 if (startEnterTransition) {
403 startEnterTransition(transition);
406 setOriginalSharedElementState(mSharedElements, originalImageViewState);
408 if (mResultReceiver != null) {
409 // We can't trust that the view will disappear on the same frame that the shared
410 // element appears here. Assure that we get at least 2 frames for double-buffering.
411 decorView.postOnAnimation(new Runnable() {
416 if (mAnimations++ < MIN_ANIMATION_FRAMES) {
417 View decorView = getDecor();
418 if (decorView != null) {
419 decorView.postOnAnimation(this);
421 } else if (mResultReceiver != null) {
422 mResultReceiver.send(MSG_HIDE_SHARED_ELEMENTS, null);
423 mResultReceiver = null; // all done sending messages.
430 private static void removeNullViews(ArrayList<View> views) {
432 for (int i = views.size() - 1; i >= 0; i--) {
433 if (views.get(i) == null) {
440 private void onTakeSharedElements() {
441 if (!mIsReadyForTransition || mSharedElementsBundle == null) {
444 final Bundle sharedElementState = mSharedElementsBundle;
445 mSharedElementsBundle = null;
446 OnSharedElementsReadyListener listener = new OnSharedElementsReadyListener() {
448 public void onSharedElementsReady() {
449 final View decorView = getDecor();
450 if (decorView != null) {
451 decorView.getViewTreeObserver()
452 .addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() {
454 public boolean onPreDraw() {
455 decorView.getViewTreeObserver().removeOnPreDrawListener(this);
456 startTransition(new Runnable() {
459 startSharedElementTransition(sharedElementState);
465 decorView.invalidate();
469 if (mListener == null) {
470 listener.onSharedElementsReady();
472 mListener.onSharedElementsArrived(mSharedElementNames, mSharedElements, listener);
476 private void requestLayoutForSharedElements() {
477 int numSharedElements = mSharedElements.size();
478 for (int i = 0; i < numSharedElements; i++) {
479 mSharedElements.get(i).requestLayout();
483 private Transition beginTransition(ViewGroup decorView, boolean startEnterTransition,
484 boolean startSharedElementTransition) {
485 Transition sharedElementTransition = null;
486 if (startSharedElementTransition) {
487 if (!mSharedElementNames.isEmpty()) {
488 sharedElementTransition = configureTransition(getSharedElementTransition(), false);
490 if (sharedElementTransition == null) {
491 sharedElementTransitionStarted();
492 sharedElementTransitionComplete();
494 sharedElementTransition.addListener(new Transition.TransitionListenerAdapter() {
496 public void onTransitionStart(Transition transition) {
497 sharedElementTransitionStarted();
501 public void onTransitionEnd(Transition transition) {
502 transition.removeListener(this);
503 sharedElementTransitionComplete();
508 Transition viewsTransition = null;
509 if (startEnterTransition) {
510 mIsViewsTransitionStarted = true;
511 if (mTransitioningViews != null && !mTransitioningViews.isEmpty()) {
512 viewsTransition = configureTransition(getViewsTransition(), true);
513 if (viewsTransition != null && !mIsReturning) {
514 stripOffscreenViews();
517 if (viewsTransition == null) {
518 viewsTransitionComplete();
520 final ArrayList<View> transitioningViews = mTransitioningViews;
521 viewsTransition.addListener(new ContinueTransitionListener() {
523 public void onTransitionStart(Transition transition) {
524 mEnterViewsTransition = transition;
525 if (transitioningViews != null) {
526 showViews(transitioningViews, false);
528 super.onTransitionStart(transition);
532 public void onTransitionEnd(Transition transition) {
533 mEnterViewsTransition = null;
534 transition.removeListener(this);
535 viewsTransitionComplete();
536 super.onTransitionEnd(transition);
542 Transition transition = mergeTransitions(sharedElementTransition, viewsTransition);
543 if (transition != null) {
544 transition.addListener(new ContinueTransitionListener());
545 if (startEnterTransition) {
546 setTransitioningViewsVisiblity(View.INVISIBLE, false);
548 TransitionManager.beginDelayedTransition(decorView, transition);
549 if (startEnterTransition) {
550 setTransitioningViewsVisiblity(View.VISIBLE, false);
552 decorView.invalidate();
560 protected void onTransitionsComplete() {
561 moveSharedElementsFromOverlay();
562 final ViewGroup decorView = getDecor();
563 if (decorView != null) {
564 decorView.sendAccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED);
568 private void sharedElementTransitionStarted() {
569 mSharedElementTransitionStarted = true;
570 if (mIsExitTransitionComplete) {
571 send(MSG_EXIT_TRANSITION_COMPLETE, null);
575 private void startEnterTransition(Transition transition) {
576 ViewGroup decorView = getDecor();
577 if (!mIsReturning && decorView != null) {
578 Drawable background = decorView.getBackground();
579 if (background != null) {
580 background = background.mutate();
581 getWindow().setBackgroundDrawable(background);
582 mBackgroundAnimator = ObjectAnimator.ofInt(background, "alpha", 255);
583 mBackgroundAnimator.setDuration(getFadeDuration());
584 mBackgroundAnimator.addListener(new AnimatorListenerAdapter() {
586 public void onAnimationEnd(Animator animation) {
590 mBackgroundAnimator.start();
591 } else if (transition != null) {
592 transition.addListener(new Transition.TransitionListenerAdapter() {
594 public void onTransitionEnd(Transition transition) {
595 transition.removeListener(this);
606 // Restore the background to its previous state since the
607 // Activity is stopping.
608 if (mBackgroundAnimator != null) {
609 mBackgroundAnimator.end();
610 mBackgroundAnimator = null;
611 } else if (mWasOpaque) {
612 ViewGroup decorView = getDecor();
613 if (decorView != null) {
614 Drawable drawable = decorView.getBackground();
615 if (drawable != null) {
616 drawable.setAlpha(1);
622 mResultReceiver = null;
624 moveSharedElementsFromOverlay();
625 if (mTransitioningViews != null) {
626 showViews(mTransitioningViews, true);
627 setTransitioningViewsVisiblity(View.VISIBLE, true);
629 showViews(mSharedElements, true);
634 * Cancels the enter transition.
635 * @return True if the enter transition is still pending capturing the target state. If so,
636 * any transition started on the decor will do nothing.
638 public boolean cancelEnter() {
639 setGhostVisibility(View.INVISIBLE);
643 return super.cancelPendingTransitions();
647 protected void clearState() {
648 mSharedElementsBundle = null;
649 mEnterViewsTransition = null;
650 mResultReceiver = null;
651 if (mBackgroundAnimator != null) {
652 mBackgroundAnimator.cancel();
653 mBackgroundAnimator = null;
658 private void makeOpaque() {
659 if (!mHasStopped && mActivity != null) {
661 mActivity.convertFromTranslucent();
667 private boolean allowOverlappingTransitions() {
668 return mIsReturning ? getWindow().getAllowReturnTransitionOverlap()
669 : getWindow().getAllowEnterTransitionOverlap();
672 private void startRejectedAnimations(final ArrayList<View> rejectedSnapshots) {
673 if (rejectedSnapshots == null || rejectedSnapshots.isEmpty()) {
676 final ViewGroup decorView = getDecor();
677 if (decorView != null) {
678 ViewGroupOverlay overlay = decorView.getOverlay();
679 ObjectAnimator animator = null;
680 int numRejected = rejectedSnapshots.size();
681 for (int i = 0; i < numRejected; i++) {
682 View snapshot = rejectedSnapshots.get(i);
683 overlay.add(snapshot);
684 animator = ObjectAnimator.ofFloat(snapshot, View.ALPHA, 1, 0);
687 animator.addListener(new AnimatorListenerAdapter() {
689 public void onAnimationEnd(Animator animation) {
690 ViewGroupOverlay overlay = decorView.getOverlay();
691 int numRejected = rejectedSnapshots.size();
692 for (int i = 0; i < numRejected; i++) {
693 overlay.remove(rejectedSnapshots.get(i));
700 protected void onRemoteExitTransitionComplete() {
701 if (!allowOverlappingTransitions()) {
702 startEnterTransitionOnly();
706 private void startEnterTransitionOnly() {
707 startTransition(new Runnable() {
710 boolean startEnterTransition = true;
711 boolean startSharedElementTransition = false;
712 ViewGroup decorView = getDecor();
713 if (decorView != null) {
714 Transition transition = beginTransition(decorView, startEnterTransition,
715 startSharedElementTransition);
716 startEnterTransition(transition);