/* * 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.drawable.Drawable; import android.os.Bundle; import android.os.ResultReceiver; import android.text.TextUtils; import android.transition.Transition; 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.ViewTreeObserver.OnPreDrawListener; import android.view.Window; import android.view.accessibility.AccessibilityEvent; 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. */ 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 OnPreDrawListener mViewsReadyListener; private final boolean mIsCrossTask; public EnterTransitionCoordinator(Activity activity, ResultReceiver resultReceiver, ArrayList 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) { decorView.getViewTreeObserver().addOnPreDrawListener( new ViewTreeObserver.OnPreDrawListener() { @Override public boolean onPreDraw() { if (mIsReadyForTransition) { decorView.getViewTreeObserver().removeOnPreDrawListener(this); } return mIsReadyForTransition; } }); } } boolean isCrossTask() { return mIsCrossTask; } public void viewInstancesReady(ArrayList accepted, ArrayList localNames, ArrayList 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 accepted, ArrayList localNames) { triggerViewsReady(mapNamedElements(accepted, localNames)); } public Transition getEnterViewsTransition() { return mEnterViewsTransition; } @Override protected void viewsReady(ArrayMap sharedElements) { super.viewsReady(sharedElements); mIsReadyForTransition = true; hideViews(mSharedElements); if (getViewsTransition() != null && mTransitioningViews != null) { hideViews(mTransitioningViews); } if (mIsReturning) { sendSharedElementDestination(); } else { moveSharedElementsToOverlay(); } if (mSharedElementsBundle != null) { onTakeSharedElements(); } } private void triggerViewsReady(final ArrayMap 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 = new ViewTreeObserver.OnPreDrawListener() { @Override public boolean onPreDraw() { mViewsReadyListener = null; decor.getViewTreeObserver().removeOnPreDrawListener(this); viewsReady(sharedElements); return true; } }; decor.getViewTreeObserver().addOnPreDrawListener(mViewsReadyListener); decor.invalidate(); } } private ArrayMap mapNamedElements(ArrayList accepted, ArrayList localNames) { ArrayMap sharedElements = new ArrayMap(); 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.remove(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) { decorView.getViewTreeObserver() .addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() { @Override public boolean onPreDraw() { decorView.getViewTreeObserver().removeOnPreDrawListener(this); if (mResultReceiver != null) { Bundle state = captureSharedElementState(); moveSharedElementsToOverlay(); mResultReceiver.send(MSG_SHARED_ELEMENT_DESTINATION, state); } return true; } }); } 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. */ public void forceViewsToAppear() { if (!mIsReturning) { return; } if (!mIsReadyForTransition) { mIsReadyForTransition = true; final ViewGroup decor = getDecor(); if (decor != null && mViewsReadyListener != null) { decor.getViewTreeObserver().removeOnPreDrawListener(mViewsReadyListener); 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) { 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 rejectedNames = new ArrayList(mAllSharedElementNames); rejectedNames.removeAll(mSharedElementNames); ArrayList rejectedSnapshots = createSnapshots(sharedElementState, rejectedNames); if (mListener != null) { mListener.onRejectSharedElements(rejectedSnapshots); } removeNullViews(rejectedSnapshots); startRejectedAnimations(rejectedSnapshots); // Now start shared element transition ArrayList sharedElementSnapshots = createSnapshots(sharedElementState, mSharedElementNames); showViews(mSharedElements, true); scheduleSetSharedElementEnd(sharedElementSnapshots); ArrayList 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 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) { decorView.getViewTreeObserver() .addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() { @Override public boolean onPreDraw() { decorView.getViewTreeObserver().removeOnPreDrawListener(this); startTransition(new Runnable() { @Override public void run() { startSharedElementTransition(sharedElementState); } }); return false; } }); 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 Transition.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 && !mIsReturning) { stripOffscreenViews(); } } if (viewsTransition == null) { viewsTransitionComplete(); } else { final ArrayList 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); } } 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(); } }); mBackgroundAnimator.start(); } else if (transition != null) { transition.addListener(new Transition.TransitionListenerAdapter() { @Override public void onTransitionEnd(Transition transition) { transition.removeListener(this); makeOpaque(); } }); } else { makeOpaque(); } } } 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. * @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 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); } } }); } }