/*
 * Copyright (C) 2014 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package android.app;

import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.animation.ObjectAnimator;
import android.app.SharedElementCallback.OnSharedElementsReadyListener;
import android.graphics.Color;
import android.graphics.drawable.ColorDrawable;
import android.graphics.drawable.Drawable;
import android.os.Bundle;
import android.os.ResultReceiver;
import android.text.TextUtils;
import android.transition.Transition;
import android.transition.TransitionListenerAdapter;
import android.transition.TransitionManager;
import android.util.ArrayMap;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewGroupOverlay;
import android.view.ViewTreeObserver;
import android.view.Window;
import android.view.accessibility.AccessibilityEvent;

import com.android.internal.view.OneShotPreDrawListener;

import java.util.ArrayList;

This ActivityTransitionCoordinator is created by the Activity to manage the enter scene and shared element transfer into the Scene, either during launch of an Activity or returning from a launched Activity.
/** * This ActivityTransitionCoordinator is created by the Activity to manage * the enter scene and shared element transfer into the Scene, either during * launch of an Activity or returning from a launched Activity. */
class EnterTransitionCoordinator extends ActivityTransitionCoordinator { private static final String TAG = "EnterTransitionCoordinator"; private static final int MIN_ANIMATION_FRAMES = 2; private boolean mSharedElementTransitionStarted; private Activity mActivity; private boolean mHasStopped; private boolean mIsCanceled; private ObjectAnimator mBackgroundAnimator; private boolean mIsExitTransitionComplete; private boolean mIsReadyForTransition; private Bundle mSharedElementsBundle; private boolean mWasOpaque; private boolean mAreViewsReady; private boolean mIsViewsTransitionStarted; private Transition mEnterViewsTransition; private OneShotPreDrawListener mViewsReadyListener; private final boolean mIsCrossTask; private Drawable mReplacedBackground; public EnterTransitionCoordinator(Activity activity, ResultReceiver resultReceiver, ArrayList<String> sharedElementNames, boolean isReturning, boolean isCrossTask) { super(activity.getWindow(), sharedElementNames, getListener(activity, isReturning && !isCrossTask), isReturning); mActivity = activity; mIsCrossTask = isCrossTask; setResultReceiver(resultReceiver); prepareEnter(); Bundle resultReceiverBundle = new Bundle(); resultReceiverBundle.putParcelable(KEY_REMOTE_RECEIVER, this); mResultReceiver.send(MSG_SET_REMOTE_RECEIVER, resultReceiverBundle); final View decorView = getDecor(); if (decorView != null) { final ViewTreeObserver viewTreeObserver = decorView.getViewTreeObserver(); viewTreeObserver.addOnPreDrawListener( new ViewTreeObserver.OnPreDrawListener() { @Override public boolean onPreDraw() { if (mIsReadyForTransition) { if (viewTreeObserver.isAlive()) { viewTreeObserver.removeOnPreDrawListener(this); } else { decorView.getViewTreeObserver().removeOnPreDrawListener(this); } } return false; } }); } } boolean isCrossTask() { return mIsCrossTask; } public void viewInstancesReady(ArrayList<String> accepted, ArrayList<String> localNames, ArrayList<View> localViews) { boolean remap = false; for (int i = 0; i < localViews.size(); i++) { View view = localViews.get(i); if (!TextUtils.equals(view.getTransitionName(), localNames.get(i)) || !view.isAttachedToWindow()) { remap = true; break; } } if (remap) { triggerViewsReady(mapNamedElements(accepted, localNames)); } else { triggerViewsReady(mapSharedElements(accepted, localViews)); } } public void namedViewsReady(ArrayList<String> accepted, ArrayList<String> localNames) { triggerViewsReady(mapNamedElements(accepted, localNames)); } public Transition getEnterViewsTransition() { return mEnterViewsTransition; } @Override protected void viewsReady(ArrayMap<String, View> sharedElements) { super.viewsReady(sharedElements); mIsReadyForTransition = true; hideViews(mSharedElements); Transition viewsTransition = getViewsTransition(); if (viewsTransition != null && mTransitioningViews != null) { removeExcludedViews(viewsTransition, mTransitioningViews); stripOffscreenViews(); hideViews(mTransitioningViews); } if (mIsReturning) { sendSharedElementDestination(); } else { moveSharedElementsToOverlay(); } if (mSharedElementsBundle != null) { onTakeSharedElements(); } } private void triggerViewsReady(final ArrayMap<String, View> sharedElements) { if (mAreViewsReady) { return; } mAreViewsReady = true; final ViewGroup decor = getDecor(); // Ensure the views have been laid out before capturing the views -- we need the epicenter. if (decor == null || (decor.isAttachedToWindow() && (sharedElements.isEmpty() || !sharedElements.valueAt(0).isLayoutRequested()))) { viewsReady(sharedElements); } else { mViewsReadyListener = OneShotPreDrawListener.add(decor, () -> { mViewsReadyListener = null; viewsReady(sharedElements); }); decor.invalidate(); } } private ArrayMap<String, View> mapNamedElements(ArrayList<String> accepted, ArrayList<String> localNames) { ArrayMap<String, View> sharedElements = new ArrayMap<String, View>(); ViewGroup decorView = getDecor(); if (decorView != null) { decorView.findNamedViews(sharedElements); } if (accepted != null) { for (int i = 0; i < localNames.size(); i++) { String localName = localNames.get(i); String acceptedName = accepted.get(i); if (localName != null && !localName.equals(acceptedName)) { View view = sharedElements.get(localName); if (view != null) { sharedElements.put(acceptedName, view); } } } } return sharedElements; } private void sendSharedElementDestination() { boolean allReady; final View decorView = getDecor(); if (allowOverlappingTransitions() && getEnterViewsTransition() != null) { allReady = false; } else if (decorView == null) { allReady = true; } else { allReady = !decorView.isLayoutRequested(); if (allReady) { for (int i = 0; i < mSharedElements.size(); i++) { if (mSharedElements.get(i).isLayoutRequested()) { allReady = false; break; } } } } if (allReady) { Bundle state = captureSharedElementState(); moveSharedElementsToOverlay(); mResultReceiver.send(MSG_SHARED_ELEMENT_DESTINATION, state); } else if (decorView != null) { OneShotPreDrawListener.add(decorView, () -> { if (mResultReceiver != null) { Bundle state = captureSharedElementState(); moveSharedElementsToOverlay(); mResultReceiver.send(MSG_SHARED_ELEMENT_DESTINATION, state); } }); } if (allowOverlappingTransitions()) { startEnterTransitionOnly(); } } private static SharedElementCallback getListener(Activity activity, boolean isReturning) { return isReturning ? activity.mExitTransitionListener : activity.mEnterTransitionListener; } @Override protected void onReceiveResult(int resultCode, Bundle resultData) { switch (resultCode) { case MSG_TAKE_SHARED_ELEMENTS: if (!mIsCanceled) { mSharedElementsBundle = resultData; onTakeSharedElements(); } break; case MSG_EXIT_TRANSITION_COMPLETE: if (!mIsCanceled) { mIsExitTransitionComplete = true; if (mSharedElementTransitionStarted) { onRemoteExitTransitionComplete(); } } break; case MSG_CANCEL: cancel(); break; } } public boolean isWaitingForRemoteExit() { return mIsReturning && mResultReceiver != null; }
This is called onResume. If an Activity is resuming and the transitions haven't started yet, force the views to appear. This is likely to be caused by the top Activity finishing before the transitions started. In that case, we can finish any transition that was started, but we should cancel any pending transition and just bring those Views visible.
/** * This is called onResume. If an Activity is resuming and the transitions * haven't started yet, force the views to appear. This is likely to be * caused by the top Activity finishing before the transitions started. * In that case, we can finish any transition that was started, but we * should cancel any pending transition and just bring those Views visible. */
public void forceViewsToAppear() { if (!mIsReturning) { return; } if (!mIsReadyForTransition) { mIsReadyForTransition = true; final ViewGroup decor = getDecor(); if (decor != null && mViewsReadyListener != null) { mViewsReadyListener.removeListener(); mViewsReadyListener = null; } showViews(mTransitioningViews, true); setTransitioningViewsVisiblity(View.VISIBLE, true); mSharedElements.clear(); mAllSharedElementNames.clear(); mTransitioningViews.clear(); mIsReadyForTransition = true; viewsTransitionComplete(); sharedElementTransitionComplete(); } else { if (!mSharedElementTransitionStarted) { moveSharedElementsFromOverlay(); mSharedElementTransitionStarted = true; showViews(mSharedElements, true); mSharedElements.clear(); sharedElementTransitionComplete(); } if (!mIsViewsTransitionStarted) { mIsViewsTransitionStarted = true; showViews(mTransitioningViews, true); setTransitioningViewsVisiblity(View.VISIBLE, true); mTransitioningViews.clear(); viewsTransitionComplete(); } cancelPendingTransitions(); } mAreViewsReady = true; if (mResultReceiver != null) { mResultReceiver.send(MSG_CANCEL, null); mResultReceiver = null; } } private void cancel() { if (!mIsCanceled) { mIsCanceled = true; if (getViewsTransition() == null || mIsViewsTransitionStarted) { showViews(mSharedElements, true); } else if (mTransitioningViews != null) { mTransitioningViews.addAll(mSharedElements); } moveSharedElementsFromOverlay(); mSharedElementNames.clear(); mSharedElements.clear(); mAllSharedElementNames.clear(); startSharedElementTransition(null); onRemoteExitTransitionComplete(); } } public boolean isReturning() { return mIsReturning; } protected void prepareEnter() { ViewGroup decorView = getDecor(); if (mActivity == null || decorView == null) { return; } if (!isCrossTask()) { mActivity.overridePendingTransition(0, 0); } if (!mIsReturning) { mWasOpaque = mActivity.convertToTranslucent(null, null); Drawable background = decorView.getBackground(); if (background == null) { background = new ColorDrawable(Color.TRANSPARENT); mReplacedBackground = background; } else { getWindow().setBackgroundDrawable(null); background = background.mutate(); background.setAlpha(0); } getWindow().setBackgroundDrawable(background); } else { mActivity = null; // all done with it now. } } @Override protected Transition getViewsTransition() { Window window = getWindow(); if (window == null) { return null; } if (mIsReturning) { return window.getReenterTransition(); } else { return window.getEnterTransition(); } } protected Transition getSharedElementTransition() { Window window = getWindow(); if (window == null) { return null; } if (mIsReturning) { return window.getSharedElementReenterTransition(); } else { return window.getSharedElementEnterTransition(); } } private void startSharedElementTransition(Bundle sharedElementState) { ViewGroup decorView = getDecor(); if (decorView == null) { return; } // Remove rejected shared elements ArrayList<String> rejectedNames = new ArrayList<String>(mAllSharedElementNames); rejectedNames.removeAll(mSharedElementNames); ArrayList<View> rejectedSnapshots = createSnapshots(sharedElementState, rejectedNames); if (mListener != null) { mListener.onRejectSharedElements(rejectedSnapshots); } removeNullViews(rejectedSnapshots); startRejectedAnimations(rejectedSnapshots); // Now start shared element transition ArrayList<View> sharedElementSnapshots = createSnapshots(sharedElementState, mSharedElementNames); showViews(mSharedElements, true); scheduleSetSharedElementEnd(sharedElementSnapshots); ArrayList<SharedElementOriginalState> originalImageViewState = setSharedElementState(sharedElementState, sharedElementSnapshots); requestLayoutForSharedElements(); boolean startEnterTransition = allowOverlappingTransitions() && !mIsReturning; boolean startSharedElementTransition = true; setGhostVisibility(View.INVISIBLE); scheduleGhostVisibilityChange(View.INVISIBLE); pauseInput(); Transition transition = beginTransition(decorView, startEnterTransition, startSharedElementTransition); scheduleGhostVisibilityChange(View.VISIBLE); setGhostVisibility(View.VISIBLE); if (startEnterTransition) { startEnterTransition(transition); } setOriginalSharedElementState(mSharedElements, originalImageViewState); if (mResultReceiver != null) { // We can't trust that the view will disappear on the same frame that the shared // element appears here. Assure that we get at least 2 frames for double-buffering. decorView.postOnAnimation(new Runnable() { int mAnimations; @Override public void run() { if (mAnimations++ < MIN_ANIMATION_FRAMES) { View decorView = getDecor(); if (decorView != null) { decorView.postOnAnimation(this); } } else if (mResultReceiver != null) { mResultReceiver.send(MSG_HIDE_SHARED_ELEMENTS, null); mResultReceiver = null; // all done sending messages. } } }); } } private static void removeNullViews(ArrayList<View> views) { if (views != null) { for (int i = views.size() - 1; i >= 0; i--) { if (views.get(i) == null) { views.remove(i); } } } } private void onTakeSharedElements() { if (!mIsReadyForTransition || mSharedElementsBundle == null) { return; } final Bundle sharedElementState = mSharedElementsBundle; mSharedElementsBundle = null; OnSharedElementsReadyListener listener = new OnSharedElementsReadyListener() { @Override public void onSharedElementsReady() { final View decorView = getDecor(); if (decorView != null) { OneShotPreDrawListener.add(decorView, false, () -> { startTransition(() -> { startSharedElementTransition(sharedElementState); }); }); decorView.invalidate(); } } }; if (mListener == null) { listener.onSharedElementsReady(); } else { mListener.onSharedElementsArrived(mSharedElementNames, mSharedElements, listener); } } private void requestLayoutForSharedElements() { int numSharedElements = mSharedElements.size(); for (int i = 0; i < numSharedElements; i++) { mSharedElements.get(i).requestLayout(); } } private Transition beginTransition(ViewGroup decorView, boolean startEnterTransition, boolean startSharedElementTransition) { Transition sharedElementTransition = null; if (startSharedElementTransition) { if (!mSharedElementNames.isEmpty()) { sharedElementTransition = configureTransition(getSharedElementTransition(), false); } if (sharedElementTransition == null) { sharedElementTransitionStarted(); sharedElementTransitionComplete(); } else { sharedElementTransition.addListener(new TransitionListenerAdapter() { @Override public void onTransitionStart(Transition transition) { sharedElementTransitionStarted(); } @Override public void onTransitionEnd(Transition transition) { transition.removeListener(this); sharedElementTransitionComplete(); } }); } } Transition viewsTransition = null; if (startEnterTransition) { mIsViewsTransitionStarted = true; if (mTransitioningViews != null && !mTransitioningViews.isEmpty()) { viewsTransition = configureTransition(getViewsTransition(), true); } if (viewsTransition == null) { viewsTransitionComplete(); } else { final ArrayList<View> transitioningViews = mTransitioningViews; viewsTransition.addListener(new ContinueTransitionListener() { @Override public void onTransitionStart(Transition transition) { mEnterViewsTransition = transition; if (transitioningViews != null) { showViews(transitioningViews, false); } super.onTransitionStart(transition); } @Override public void onTransitionEnd(Transition transition) { mEnterViewsTransition = null; transition.removeListener(this); viewsTransitionComplete(); super.onTransitionEnd(transition); } }); } } Transition transition = mergeTransitions(sharedElementTransition, viewsTransition); if (transition != null) { transition.addListener(new ContinueTransitionListener()); if (startEnterTransition) { setTransitioningViewsVisiblity(View.INVISIBLE, false); } TransitionManager.beginDelayedTransition(decorView, transition); if (startEnterTransition) { setTransitioningViewsVisiblity(View.VISIBLE, false); } decorView.invalidate(); } else { transitionStarted(); } return transition; } @Override protected void onTransitionsComplete() { moveSharedElementsFromOverlay(); final ViewGroup decorView = getDecor(); if (decorView != null) { decorView.sendAccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED); Window window = getWindow(); if (window != null && mReplacedBackground == decorView.getBackground()) { window.setBackgroundDrawable(null); } } } private void sharedElementTransitionStarted() { mSharedElementTransitionStarted = true; if (mIsExitTransitionComplete) { send(MSG_EXIT_TRANSITION_COMPLETE, null); } } private void startEnterTransition(Transition transition) { ViewGroup decorView = getDecor(); if (!mIsReturning && decorView != null) { Drawable background = decorView.getBackground(); if (background != null) { background = background.mutate(); getWindow().setBackgroundDrawable(background); mBackgroundAnimator = ObjectAnimator.ofInt(background, "alpha", 255); mBackgroundAnimator.setDuration(getFadeDuration()); mBackgroundAnimator.addListener(new AnimatorListenerAdapter() { @Override public void onAnimationEnd(Animator animation) { makeOpaque(); backgroundAnimatorComplete(); } }); mBackgroundAnimator.start(); } else if (transition != null) { transition.addListener(new TransitionListenerAdapter() { @Override public void onTransitionEnd(Transition transition) { transition.removeListener(this); makeOpaque(); } }); backgroundAnimatorComplete(); } else { makeOpaque(); backgroundAnimatorComplete(); } } else { backgroundAnimatorComplete(); } } public void stop() { // Restore the background to its previous state since the // Activity is stopping. if (mBackgroundAnimator != null) { mBackgroundAnimator.end(); mBackgroundAnimator = null; } else if (mWasOpaque) { ViewGroup decorView = getDecor(); if (decorView != null) { Drawable drawable = decorView.getBackground(); if (drawable != null) { drawable.setAlpha(1); } } } makeOpaque(); mIsCanceled = true; mResultReceiver = null; mActivity = null; moveSharedElementsFromOverlay(); if (mTransitioningViews != null) { showViews(mTransitioningViews, true); setTransitioningViewsVisiblity(View.VISIBLE, true); } showViews(mSharedElements, true); clearState(); }
Cancels the enter transition.
Returns:True if the enter transition is still pending capturing the target state. If so, any transition started on the decor will do nothing.
/** * Cancels the enter transition. * @return True if the enter transition is still pending capturing the target state. If so, * any transition started on the decor will do nothing. */
public boolean cancelEnter() { setGhostVisibility(View.INVISIBLE); mHasStopped = true; mIsCanceled = true; clearState(); return super.cancelPendingTransitions(); } @Override protected void clearState() { mSharedElementsBundle = null; mEnterViewsTransition = null; mResultReceiver = null; if (mBackgroundAnimator != null) { mBackgroundAnimator.cancel(); mBackgroundAnimator = null; } super.clearState(); } private void makeOpaque() { if (!mHasStopped && mActivity != null) { if (mWasOpaque) { mActivity.convertFromTranslucent(); } mActivity = null; } } private boolean allowOverlappingTransitions() { return mIsReturning ? getWindow().getAllowReturnTransitionOverlap() : getWindow().getAllowEnterTransitionOverlap(); } private void startRejectedAnimations(final ArrayList<View> rejectedSnapshots) { if (rejectedSnapshots == null || rejectedSnapshots.isEmpty()) { return; } final ViewGroup decorView = getDecor(); if (decorView != null) { ViewGroupOverlay overlay = decorView.getOverlay(); ObjectAnimator animator = null; int numRejected = rejectedSnapshots.size(); for (int i = 0; i < numRejected; i++) { View snapshot = rejectedSnapshots.get(i); overlay.add(snapshot); animator = ObjectAnimator.ofFloat(snapshot, View.ALPHA, 1, 0); animator.start(); } animator.addListener(new AnimatorListenerAdapter() { @Override public void onAnimationEnd(Animator animation) { ViewGroupOverlay overlay = decorView.getOverlay(); int numRejected = rejectedSnapshots.size(); for (int i = 0; i < numRejected; i++) { overlay.remove(rejectedSnapshots.get(i)); } } }); } } protected void onRemoteExitTransitionComplete() { if (!allowOverlappingTransitions()) { startEnterTransitionOnly(); } } private void startEnterTransitionOnly() { startTransition(new Runnable() { @Override public void run() { boolean startEnterTransition = true; boolean startSharedElementTransition = false; ViewGroup decorView = getDecor(); if (decorView != null) { Transition transition = beginTransition(decorView, startEnterTransition, startSharedElementTransition); startEnterTransition(transition); } } }); } }