From 7b5abdcd7de7b5181700ec0cad6a596a6aa9d67e Mon Sep 17 00:00:00 2001 From: Jon Miranda Date: Tue, 10 Nov 2020 02:01:47 +0000 Subject: [PATCH] Fixing support for shared element transitions from a window Previously animation from window always ran in exit mode causing the animation to play in the calling window surface instead of the launching activity surface. This causes flicker on transition end when the rendering switches to the new activity surface. Test: Verified with demo app Change-Id: I07f5aa4e390fd9dab9883894ba7b780170c0e212 --- core/java/android/app/Activity.java | 2 +- core/java/android/app/ActivityOptions.java | 118 ++++------------- .../android/app/ActivityTransitionState.java | 12 +- .../app/ExitTransitionCoordinator.java | 122 +++++++++++------- 4 files changed, 110 insertions(+), 144 deletions(-) diff --git a/core/java/android/app/Activity.java b/core/java/android/app/Activity.java index fc95718cf0e42..2e8e6aafc6aeb 100644 --- a/core/java/android/app/Activity.java +++ b/core/java/android/app/Activity.java @@ -2590,7 +2590,7 @@ public class Activity extends ContextThemeWrapper protected void onStop() { if (DEBUG_LIFECYCLE) Slog.v(TAG, "onStop " + this); if (mActionBar != null) mActionBar.setShowHideAnimationEnabled(false); - mActivityTransitionState.onStop(); + mActivityTransitionState.onStop(this); dispatchActivityStopped(); mTranslucentCallback = null; mCalled = true; diff --git a/core/java/android/app/ActivityOptions.java b/core/java/android/app/ActivityOptions.java index ef146b118c338..0b0781e917e3b 100644 --- a/core/java/android/app/ActivityOptions.java +++ b/core/java/android/app/ActivityOptions.java @@ -27,6 +27,8 @@ import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.RequiresPermission; import android.annotation.TestApi; +import android.app.ExitTransitionCoordinator.ActivityExitTransitionCallbacks; +import android.app.ExitTransitionCoordinator.ExitTransitionCallbacks; import android.compat.annotation.UnsupportedAppUsage; import android.content.ComponentName; import android.content.Context; @@ -44,8 +46,6 @@ import android.os.Parcelable; import android.os.RemoteException; import android.os.ResultReceiver; import android.os.UserHandle; -import android.transition.Transition; -import android.transition.TransitionListenerAdapter; import android.transition.TransitionManager; import android.util.Pair; import android.util.Slog; @@ -806,8 +806,11 @@ public class ActivityOptions { public static ActivityOptions makeSceneTransitionAnimation(Activity activity, Pair... sharedElements) { ActivityOptions opts = new ActivityOptions(); - makeSceneTransitionAnimation(activity, activity.getWindow(), opts, - activity.mExitTransitionListener, sharedElements); + ExitTransitionCoordinator exit = makeSceneTransitionAnimation( + new ActivityExitTransitionCallbacks(activity), activity.mExitTransitionListener, + activity.getWindow(), opts, sharedElements); + opts.mExitCoordinatorIndex = + activity.mActivityTransitionState.addExitTransitionCoordinator(exit); return opts; } @@ -823,25 +826,19 @@ public class ActivityOptions { * @hide */ @SafeVarargs - public static ActivityOptions startSharedElementAnimation(Window window, + public static Pair startSharedElementAnimation( + Window window, ExitTransitionCallbacks exitCallbacks, SharedElementCallback callback, Pair... sharedElements) { ActivityOptions opts = new ActivityOptions(); - final View decorView = window.getDecorView(); - if (decorView == null) { - return opts; - } - final ExitTransitionCoordinator exit = - makeSceneTransitionAnimation(null, window, opts, null, sharedElements); - if (exit != null) { - HideWindowListener listener = new HideWindowListener(window, exit); - exit.setHideSharedElementsCallback(listener); - exit.startExit(); - } - return opts; + ExitTransitionCoordinator exit = makeSceneTransitionAnimation( + exitCallbacks, callback, window, opts, sharedElements); + opts.mExitCoordinatorIndex = -1; + return Pair.create(opts, exit); } /** - * This method should be called when the {@link #startSharedElementAnimation(Window, Pair[])} + * This method should be called when the + * {@link #startSharedElementAnimation(Window, ExitTransitionCallbacks, Pair[])} * animation must be stopped and the Views reset. This can happen if there was an error * from startActivity or a springboard activity and the animation should stop and reset. * @@ -864,9 +861,9 @@ public class ActivityOptions { } } - static ExitTransitionCoordinator makeSceneTransitionAnimation(Activity activity, Window window, - ActivityOptions opts, SharedElementCallback callback, - Pair[] sharedElements) { + static ExitTransitionCoordinator makeSceneTransitionAnimation( + ExitTransitionCallbacks exitCallbacks, SharedElementCallback callback, Window window, + ActivityOptions opts, Pair[] sharedElements) { if (!window.hasFeature(Window.FEATURE_ACTIVITY_TRANSITIONS)) { opts.mAnimationType = ANIM_DEFAULT; return null; @@ -892,17 +889,11 @@ public class ActivityOptions { } } - ExitTransitionCoordinator exit = new ExitTransitionCoordinator(activity, window, + ExitTransitionCoordinator exit = new ExitTransitionCoordinator(exitCallbacks, window, callback, names, names, views, false); opts.mTransitionReceiver = exit; opts.mSharedElementNames = names; - opts.mIsReturning = (activity == null); - if (activity == null) { - opts.mExitCoordinatorIndex = -1; - } else { - opts.mExitCoordinatorIndex = - activity.mActivityTransitionState.addExitTransitionCoordinator(exit); - } + opts.mIsReturning = false; return exit; } @@ -928,8 +919,12 @@ public class ActivityOptions { opts.mIsReturning = true; opts.mResultCode = resultCode; opts.mResultData = resultData; - opts.mExitCoordinatorIndex = - activity.mActivityTransitionState.addExitTransitionCoordinator(exitCoordinator); + if (activity == null) { + opts.mExitCoordinatorIndex = -1; + } else { + opts.mExitCoordinatorIndex = + activity.mActivityTransitionState.addExitTransitionCoordinator(exitCoordinator); + } return opts; } @@ -1868,67 +1863,6 @@ public class ActivityOptions { + mStartY + ", mWidth=" + mWidth + ", mHeight=" + mHeight; } - private static class HideWindowListener extends TransitionListenerAdapter - implements ExitTransitionCoordinator.HideSharedElementsCallback { - private final Window mWindow; - private final ExitTransitionCoordinator mExit; - private final boolean mWaitingForTransition; - private boolean mTransitionEnded; - private boolean mSharedElementHidden; - private ArrayList mSharedElements; - - public HideWindowListener(Window window, ExitTransitionCoordinator exit) { - mWindow = window; - mExit = exit; - mSharedElements = new ArrayList<>(exit.mSharedElements); - Transition transition = mWindow.getExitTransition(); - if (transition != null) { - transition.addListener(this); - mWaitingForTransition = true; - } else { - mWaitingForTransition = false; - } - View decorView = mWindow.getDecorView(); - if (decorView != null) { - if (decorView.getTag(com.android.internal.R.id.cross_task_transition) != null) { - throw new IllegalStateException( - "Cannot start a transition while one is running"); - } - decorView.setTagInternal(com.android.internal.R.id.cross_task_transition, exit); - } - } - - @Override - public void onTransitionEnd(Transition transition) { - mTransitionEnded = true; - hideWhenDone(); - transition.removeListener(this); - } - - @Override - public void hideSharedElements() { - mSharedElementHidden = true; - hideWhenDone(); - } - - private void hideWhenDone() { - if (mSharedElementHidden && (!mWaitingForTransition || mTransitionEnded)) { - mExit.resetViews(); - int numSharedElements = mSharedElements.size(); - for (int i = 0; i < numSharedElements; i++) { - View view = mSharedElements.get(i); - view.requestLayout(); - } - View decorView = mWindow.getDecorView(); - if (decorView != null) { - decorView.setTagInternal( - com.android.internal.R.id.cross_task_transition, null); - decorView.setVisibility(View.GONE); - } - } - } - } - /** * The information about the source of activity launch. E.g. describe an activity is launched * from launcher by receiving a motion event with a timestamp. diff --git a/core/java/android/app/ActivityTransitionState.java b/core/java/android/app/ActivityTransitionState.java index 5c4125e01dfd7..62619509184a8 100644 --- a/core/java/android/app/ActivityTransitionState.java +++ b/core/java/android/app/ActivityTransitionState.java @@ -247,14 +247,14 @@ class ActivityTransitionState { mEnterActivityOptions = null; } - public void onStop() { + public void onStop(Activity activity) { restoreExitedViews(); if (mEnterTransitionCoordinator != null) { mEnterTransitionCoordinator.stop(); mEnterTransitionCoordinator = null; } if (mReturnExitCoordinator != null) { - mReturnExitCoordinator.stop(); + mReturnExitCoordinator.stop(activity); mReturnExitCoordinator = null; } } @@ -331,7 +331,8 @@ class ActivityTransitionState { } } - mReturnExitCoordinator = new ExitTransitionCoordinator(activity, + mReturnExitCoordinator = new ExitTransitionCoordinator( + new ExitTransitionCoordinator.ActivityExitTransitionCallbacks(activity), activity.getWindow(), activity.mEnterTransitionListener, pendingExitNames, null, null, true); if (enterViewsTransition != null && decor != null) { @@ -341,12 +342,11 @@ class ActivityTransitionState { final ViewGroup finalDecor = decor; OneShotPreDrawListener.add(decor, () -> { if (mReturnExitCoordinator != null) { - mReturnExitCoordinator.startExit(activity.mResultCode, - activity.mResultData); + mReturnExitCoordinator.startExit(activity); } }); } else { - mReturnExitCoordinator.startExit(activity.mResultCode, activity.mResultData); + mReturnExitCoordinator.startExit(activity); } } return true; diff --git a/core/java/android/app/ExitTransitionCoordinator.java b/core/java/android/app/ExitTransitionCoordinator.java index 68824cd26eaaf..9fdff5979cd06 100644 --- a/core/java/android/app/ExitTransitionCoordinator.java +++ b/core/java/android/app/ExitTransitionCoordinator.java @@ -18,6 +18,7 @@ package android.app; import android.animation.Animator; import android.animation.AnimatorListenerAdapter; import android.animation.ObjectAnimator; +import android.annotation.NonNull; import android.app.SharedElementCallback.OnSharedElementsReadyListener; import android.content.Intent; import android.graphics.Color; @@ -45,15 +46,17 @@ import java.util.ArrayList; * This ActivityTransitionCoordinator is created in ActivityOptions#makeSceneTransitionAnimation * to govern the exit of the Scene and the shared elements when calling an Activity as well as * the reentry of the Scene when coming back from the called Activity. + * + * @hide */ -class ExitTransitionCoordinator extends ActivityTransitionCoordinator { +public class ExitTransitionCoordinator extends ActivityTransitionCoordinator { private static final String TAG = "ExitTransitionCoordinator"; static long sMaxWaitMillis = 1000; private Bundle mSharedElementBundle; private boolean mExitNotified; private boolean mSharedElementNotified; - private Activity mActivity; + private ExitTransitionCallbacks mExitCallbacks; private boolean mIsBackgroundReady; private boolean mIsCanceled; private Handler mHandler; @@ -62,20 +65,15 @@ class ExitTransitionCoordinator extends ActivityTransitionCoordinator { private Bundle mExitSharedElementBundle; private boolean mIsExitStarted; private boolean mSharedElementsHidden; - private HideSharedElementsCallback mHideSharedElementsCallback; - public ExitTransitionCoordinator(Activity activity, Window window, - SharedElementCallback listener, ArrayList names, + public ExitTransitionCoordinator(ExitTransitionCallbacks exitCallbacks, + Window window, SharedElementCallback listener, ArrayList names, ArrayList accepted, ArrayList mapped, boolean isReturning) { super(window, names, listener, isReturning); viewsReady(mapSharedElements(accepted, mapped)); stripOffscreenViews(); mIsBackgroundReady = !isReturning; - mActivity = activity; - } - - void setHideSharedElementsCallback(HideSharedElementsCallback callback) { - mHideSharedElementsCallback = callback; + mExitCallbacks = exitCallbacks; } @Override @@ -190,8 +188,8 @@ class ExitTransitionCoordinator extends ActivityTransitionCoordinator { private void hideSharedElements() { moveSharedElementsFromOverlay(); - if (mHideSharedElementsCallback != null) { - mHideSharedElementsCallback.hideSharedElements(); + if (mExitCallbacks != null) { + mExitCallbacks.hideSharedElements(); } if (!mIsHidden) { hideViews(mSharedElements); @@ -210,20 +208,16 @@ class ExitTransitionCoordinator extends ActivityTransitionCoordinator { decorView.suppressLayout(true); } moveSharedElementsToOverlay(); - startTransition(new Runnable() { - @Override - public void run() { - if (mActivity != null) { - beginTransitions(); - } else { - startExitTransition(); - } - } - }); + startTransition(this::beginTransitions); } } - public void startExit(int resultCode, Intent data) { + /** + * Starts the exit animation and sends back the activity result + */ + public void startExit(Activity activity) { + int resultCode = activity.mResultCode; + Intent data = activity.mResultData; if (!mIsExitStarted) { mIsExitStarted = true; pauseInput(); @@ -247,9 +241,9 @@ class ExitTransitionCoordinator extends ActivityTransitionCoordinator { .getApplicationInfo().targetSdkVersion >= VERSION_CODES.M; ArrayList sharedElementNames = targetsM ? mSharedElementNames : mAllSharedElementNames; - ActivityOptions options = ActivityOptions.makeSceneTransitionAnimation(mActivity, this, + ActivityOptions options = ActivityOptions.makeSceneTransitionAnimation(activity, this, sharedElementNames, resultCode, data); - mActivity.convertToTranslucent(new Activity.TranslucentConversionListener() { + activity.convertToTranslucent(new Activity.TranslucentConversionListener() { @Override public void onTranslucentConversionComplete(boolean drawComplete) { if (!mIsCanceled) { @@ -257,21 +251,19 @@ class ExitTransitionCoordinator extends ActivityTransitionCoordinator { } } }, options); - startTransition(new Runnable() { - @Override - public void run() { - startExitTransition(); - } - }); + startTransition(this::startExitTransition); } } - public void stop() { - if (mIsReturning && mActivity != null) { + /** + * Called from {@link Activity#onStop()} + */ + public void stop(Activity activity) { + if (mIsReturning && mExitCallbacks != null) { // Override the previous ActivityOptions. We don't want the // activity to have options since we're essentially canceling the // transition and finishing right now. - mActivity.convertToTranslucent(null, null); + activity.convertToTranslucent(null, null); finish(); } } @@ -434,7 +426,7 @@ class ExitTransitionCoordinator extends ActivityTransitionCoordinator { mSharedElementNotified = true; delayCancel(); - if (!mActivity.isTopOfTask()) { + if (mExitCallbacks.isReturnTransitionAllowed()) { mResultReceiver.send(MSG_ALLOW_RETURN_TRANSITION, null); } @@ -474,22 +466,20 @@ class ExitTransitionCoordinator extends ActivityTransitionCoordinator { } private void finishIfNecessary() { - if (mIsReturning && mExitNotified && mActivity != null && (mSharedElements.isEmpty() || - mSharedElementsHidden)) { + if (mIsReturning && mExitNotified && mExitCallbacks != null && (mSharedElements.isEmpty() + || mSharedElementsHidden)) { finish(); } if (!mIsReturning && mExitNotified) { - mActivity = null; // don't need it anymore + mExitCallbacks = null; // don't need it anymore } } private void finish() { stopCancel(); - if (mActivity != null) { - mActivity.mActivityTransitionState.clear(); - mActivity.finish(); - mActivity.overridePendingTransition(0, 0); - mActivity = null; + if (mExitCallbacks != null) { + mExitCallbacks.onFinish(); + mExitCallbacks = null; } // Clear the state so that we can't hold any references accidentally and leak memory. clearState(); @@ -529,7 +519,49 @@ class ExitTransitionCoordinator extends ActivityTransitionCoordinator { } } - interface HideSharedElementsCallback { - void hideSharedElements(); + /** + * @hide + */ + public interface ExitTransitionCallbacks { + + /** + * Returns true if reverse exit animation is supported + */ + boolean isReturnTransitionAllowed(); + + /** + * Called then the transition finishes + */ + void onFinish(); + + /** + * Optional callback when the transition is hiding elements in the source surface + */ + default void hideSharedElements() { }; + } + + /** + * @hide + */ + public static class ActivityExitTransitionCallbacks implements ExitTransitionCallbacks { + + @NonNull + final Activity mActivity; + + ActivityExitTransitionCallbacks(@NonNull Activity activity) { + mActivity = activity; + } + + @Override + public boolean isReturnTransitionAllowed() { + return !mActivity.isTopOfTask(); + } + + @Override + public void onFinish() { + mActivity.mActivityTransitionState.clear(); + mActivity.finish(); + mActivity.overridePendingTransition(0, 0); + } } }