From 37a60b435a3f43c53c17b2b9f6f52f77755c1ae3 Mon Sep 17 00:00:00 2001 From: Miranda Kephart Date: Wed, 16 Jun 2021 19:15:41 -0400 Subject: [PATCH] Implement "shared element" SS->LSS transition in screenshots Workaround for b/190806800; in order to get the transition from screenshots to the long screenshots activity to look the way we want, we fake the shared element transition by determining the destination for the preview in long screenshots, sending that back, and running the actual animation in screenshots. Bug: 183197533 Test: manual; visual verification in portrait and landscape Change-Id: I16d462789ec0fa9422cc1924e6df9649b89b9055 --- .../screenshot/LongScreenshotActivity.java | 40 +++++------ ...hotHolder.java => LongScreenshotData.java} | 25 +++++-- .../screenshot/ScreenshotController.java | 71 ++++++++++++++----- .../systemui/screenshot/ScreenshotView.java | 50 ++++++++++++- 4 files changed, 139 insertions(+), 47 deletions(-) rename packages/SystemUI/src/com/android/systemui/screenshot/{LongScreenshotHolder.java => LongScreenshotData.java} (63%) diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/LongScreenshotActivity.java b/packages/SystemUI/src/com/android/systemui/screenshot/LongScreenshotActivity.java index 25ec1d74008e9..f571e417c6367 100644 --- a/packages/SystemUI/src/com/android/systemui/screenshot/LongScreenshotActivity.java +++ b/packages/SystemUI/src/com/android/systemui/screenshot/LongScreenshotActivity.java @@ -32,8 +32,6 @@ import android.net.Uri; import android.os.Bundle; import android.os.UserHandle; import android.text.TextUtils; -import android.transition.Transition; -import android.transition.TransitionListenerAdapter; import android.util.Log; import android.view.ScrollCaptureResponse; import android.view.View; @@ -74,7 +72,7 @@ public class LongScreenshotActivity extends Activity { private final Executor mUiExecutor; private final Executor mBackgroundExecutor; private final ImageExporter mImageExporter; - private final LongScreenshotHolder mLongScreenshotHolder; + private final LongScreenshotData mLongScreenshotHolder; private ImageView mPreview; private ImageView mTransitionView; @@ -103,7 +101,7 @@ public class LongScreenshotActivity extends Activity { @Inject public LongScreenshotActivity(UiEventLogger uiEventLogger, ImageExporter imageExporter, @Main Executor mainExecutor, @Background Executor bgExecutor, - LongScreenshotHolder longScreenshotHolder) { + LongScreenshotData longScreenshotHolder) { mUiEventLogger = uiEventLogger; mUiExecutor = mainExecutor; mBackgroundExecutor = bgExecutor; @@ -116,7 +114,6 @@ public class LongScreenshotActivity extends Activity { public void onCreate(Bundle savedInstanceState) { Log.d(TAG, "onCreate(savedInstanceState = " + savedInstanceState + ")"); super.onCreate(savedInstanceState); - postponeEnterTransition(); setContentView(R.layout.long_screenshot); mPreview = requireViewById(R.id.preview); @@ -210,25 +207,22 @@ public class LongScreenshotActivity extends Activity { public boolean onPreDraw() { mEnterTransitionView.getViewTreeObserver().removeOnPreDrawListener(this); updateImageDimensions(); - startPostponedEnterTransition(); - if (isActivityTransitionRunning()) { - getWindow().getSharedElementEnterTransition().addListener( - new TransitionListenerAdapter() { - @Override - public void onTransitionEnd(Transition transition) { - super.onTransitionEnd(transition); - mPreview.animate().alpha(1f); - mCropView.setBoundaryPosition( - CropView.CropBoundary.TOP, topFraction); - mCropView.setBoundaryPosition( - CropView.CropBoundary.BOTTOM, bottomFraction); - mCropView.animateEntrance(); - mCropView.setVisibility(View.VISIBLE); - setButtonsEnabled(true); - mEnterTransitionView.setVisibility(View.GONE); - } + mEnterTransitionView.post(() -> { + Rect dest = new Rect(); + mEnterTransitionView.getBoundsOnScreen(dest); + mLongScreenshotHolder.takeTransitionDestinationCallback() + .setTransitionDestination(dest, () -> { + mPreview.animate().alpha(1f); + mCropView.setBoundaryPosition( + CropView.CropBoundary.TOP, topFraction); + mCropView.setBoundaryPosition( + CropView.CropBoundary.BOTTOM, bottomFraction); + mCropView.animateEntrance(); + mCropView.setVisibility(View.VISIBLE); + setButtonsEnabled(true); + mEnterTransitionView.setVisibility(View.GONE); }); - } + }); return true; } }); diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/LongScreenshotHolder.java b/packages/SystemUI/src/com/android/systemui/screenshot/LongScreenshotData.java similarity index 63% rename from packages/SystemUI/src/com/android/systemui/screenshot/LongScreenshotHolder.java rename to packages/SystemUI/src/com/android/systemui/screenshot/LongScreenshotData.java index 39c6f07989885..f549faf2414aa 100644 --- a/packages/SystemUI/src/com/android/systemui/screenshot/LongScreenshotHolder.java +++ b/packages/SystemUI/src/com/android/systemui/screenshot/LongScreenshotData.java @@ -23,16 +23,19 @@ import java.util.concurrent.atomic.AtomicReference; import javax.inject.Inject; /** - * LongScreenshotHolder holds on to 1 LongScreenshot reference to facilitate indirect in-process - * passing. + * LongScreenshotData holds on to 1 LongScreenshot reference and 1 TransitionDestination + * reference, to facilitate indirect in-process passing. */ @SysUISingleton -public class LongScreenshotHolder { +public class LongScreenshotData { private final AtomicReference mLongScreenshot; + private final AtomicReference + mTransitionDestinationCallback; @Inject - public LongScreenshotHolder() { + public LongScreenshotData() { mLongScreenshot = new AtomicReference<>(); + mTransitionDestinationCallback = new AtomicReference<>(); } /** @@ -56,4 +59,18 @@ public class LongScreenshotHolder { return mLongScreenshot.getAndSet(null); } + /** + * Set the holder's TransitionDestination callback. + */ + public void setTransitionDestinationCallback( + ScreenshotController.TransitionDestination destination) { + mTransitionDestinationCallback.set(destination); + } + + /** + * Return the current TransitionDestination callback and clear. + */ + public ScreenshotController.TransitionDestination takeTransitionDestinationCallback() { + return mTransitionDestinationCallback.getAndSet(null); + } } diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotController.java b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotController.java index 5efa1b2992b2b..aee88cd7cf7c9 100644 --- a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotController.java +++ b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotController.java @@ -64,6 +64,7 @@ import android.view.IRemoteAnimationFinishedCallback; import android.view.IRemoteAnimationRunner; import android.view.KeyEvent; import android.view.LayoutInflater; +import android.view.RemoteAnimationAdapter; import android.view.RemoteAnimationTarget; import android.view.ScrollCaptureResponse; import android.view.SurfaceControl; @@ -72,6 +73,7 @@ import android.view.ViewTreeObserver; import android.view.Window; import android.view.WindowInsets; import android.view.WindowManager; +import android.view.WindowManagerGlobal; import android.view.accessibility.AccessibilityEvent; import android.view.accessibility.AccessibilityManager; import android.widget.Toast; @@ -203,6 +205,15 @@ public class ScreenshotController { void onActionsReady(ScreenshotController.QuickShareData quickShareData); } + interface TransitionDestination { + /** + * Allows the long screenshot activity to call back with a destination location (the bounds + * on screen of the destination for the transitioning view) and a Runnable to be run once + * the transition animation is complete. + */ + void setTransitionDestination(Rect transitionDestination, Runnable onTransitionEnd); + } + // These strings are used for communicating the action invoked to // ScreenshotNotificationSmartActionsProvider. static final String EXTRA_ACTION_TYPE = "android:screenshot_action_type"; @@ -241,7 +252,7 @@ public class ScreenshotController { private final PhoneWindow mWindow; private final DisplayManager mDisplayManager; private final ScrollCaptureController mScrollCaptureController; - private final LongScreenshotHolder mLongScreenshotHolder; + private final LongScreenshotData mLongScreenshotHolder; private ScreenshotView mScreenshotView; private Bitmap mScreenBitmap; @@ -286,7 +297,7 @@ public class ScreenshotController { ImageExporter imageExporter, @Main Executor mainExecutor, ScrollCaptureController scrollCaptureController, - LongScreenshotHolder longScreenshotHolder) { + LongScreenshotData longScreenshotHolder) { mScreenshotSmartActions = screenshotSmartActions; mNotificationsController = screenshotNotificationsController; mScrollCaptureClient = scrollCaptureClient; @@ -644,17 +655,29 @@ public class ScreenshotController { } mLongScreenshotHolder.setLongScreenshot(longScreenshot); + mLongScreenshotHolder.setTransitionDestinationCallback( + (transitionDestination, onTransitionEnd) -> + mScreenshotView.startLongScreenshotTransition( + transitionDestination, onTransitionEnd, + longScreenshot)); final Intent intent = new Intent(mContext, LongScreenshotActivity.class); intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TOP); Pair transition = - ActivityOptions.startSharedElementAnimation( - mWindow, new ScreenshotExitTransitionCallbacks(), null, - Pair.create(mScreenshotView.getScrollablePreview(), - ChooserActivity.FIRST_IMAGE_PREVIEW_TRANSITION_NAME)); + ActivityOptions.startSharedElementAnimation(mWindow, + new ScreenshotExitTransitionCallbacksSupplier(false).get(), + null); transition.second.startExit(); mContext.startActivity(intent, transition.first.toBundle()); + RemoteAnimationAdapter runner = new RemoteAnimationAdapter( + SCREENSHOT_REMOTE_RUNNER, 0, 0); + try { + WindowManagerGlobal.getWindowManagerService() + .overridePendingAppTransitionRemote(runner, DEFAULT_DISPLAY); + } catch (Exception e) { + Log.e(TAG, "Error overriding screenshot app transition", e); + } }, mMainExecutor); }); } catch (CancellationException e) { @@ -879,8 +902,8 @@ public class ScreenshotController { return () -> { Pair transition = ActivityOptions.startSharedElementAnimation( - mWindow, new ScreenshotExitTransitionCallbacks(), null, - Pair.create(mScreenshotView.getScreenshotPreview(), + mWindow, new ScreenshotExitTransitionCallbacksSupplier(true).get(), + null, Pair.create(mScreenshotView.getScreenshotPreview(), ChooserActivity.FIRST_IMAGE_PREVIEW_TRANSITION_NAME)); transition.second.startExit(); @@ -966,19 +989,33 @@ public class ScreenshotController { return matchWithinTolerance; } - private class ScreenshotExitTransitionCallbacks implements ExitTransitionCallbacks { - @Override - public boolean isReturnTransitionAllowed() { - return false; + private class ScreenshotExitTransitionCallbacksSupplier implements + Supplier { + final boolean mDismissOnHideSharedElements; + + ScreenshotExitTransitionCallbacksSupplier(boolean dismissOnHideSharedElements) { + mDismissOnHideSharedElements = dismissOnHideSharedElements; } @Override - public void hideSharedElements() { - finishDismiss(); - } + public ExitTransitionCallbacks get() { + return new ExitTransitionCallbacks() { + @Override + public boolean isReturnTransitionAllowed() { + return false; + } - @Override - public void onFinish() { + @Override + public void hideSharedElements() { + if (mDismissOnHideSharedElements) { + finishDismiss(); + } + } + + @Override + public void onFinish() { + } + }; } } } diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotView.java b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotView.java index 2a2217912b584..985a5b8e8493d 100644 --- a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotView.java +++ b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotView.java @@ -759,7 +759,53 @@ public class ScreenshotView extends FrameLayout implements return r; } + void startLongScreenshotTransition(Rect destination, Runnable onTransitionEnd, + ScrollCaptureController.LongScreenshot longScreenshot) { + mScrollablePreview.setImageBitmap(longScreenshot.toBitmap()); + ValueAnimator anim = ValueAnimator.ofFloat(0, 1); + float startX = mScrollablePreview.getX(); + float startY = mScrollablePreview.getY(); + int[] locInScreen = mScrollablePreview.getLocationOnScreen(); + destination.offset((int) startX - locInScreen[0], (int) startY - locInScreen[1]); + mScrollablePreview.setPivotX(0); + mScrollablePreview.setPivotY(0); + mScrollablePreview.setAlpha(1f); + float currentScale = mScrollablePreview.getWidth() / (float) longScreenshot.getWidth(); + Matrix matrix = new Matrix(); + matrix.setScale(currentScale, currentScale); + matrix.postTranslate( + longScreenshot.getLeft() * currentScale, longScreenshot.getTop() * currentScale); + mScrollablePreview.setImageMatrix(matrix); + float destinationScale = destination.width() / (float) mScrollablePreview.getWidth(); + anim.addUpdateListener(animation -> { + float t = animation.getAnimatedFraction(); + mScrollingScrim.setAlpha(1 - t); + float currScale = MathUtils.lerp(1, destinationScale, t); + mScrollablePreview.setScaleX(currScale); + mScrollablePreview.setScaleY(currScale); + mScrollablePreview.setX(MathUtils.lerp(startX, destination.left, t)); + mScrollablePreview.setY(MathUtils.lerp(startY, destination.top, t)); + }); + anim.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + super.onAnimationEnd(animation); + onTransitionEnd.run(); + mScrollablePreview.animate().alpha(0).setListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + super.onAnimationEnd(animation); + mCallbacks.onDismiss(); + } + }); + } + }); + anim.start(); + } + void prepareScrollingTransition(ScrollCaptureResponse response, Bitmap screenBitmap) { + mScrollingScrim.setImageBitmap(screenBitmap); + mScrollingScrim.setVisibility(View.VISIBLE); Rect scrollableArea = scrollableAreaOnScreen(response); float scale = mCornerSizeX / (mOrientationPortrait ? screenBitmap.getWidth() : screenBitmap.getHeight()); @@ -770,14 +816,12 @@ public class ScreenshotView extends FrameLayout implements params.height = (int) (scale * scrollableArea.height()); Matrix matrix = new Matrix(); matrix.setScale(scale, scale); - matrix.postTranslate(0, -scrollableArea.top * scale); + matrix.postTranslate(-scrollableArea.left * scale, -scrollableArea.top * scale); mScrollablePreview.setTranslationX(scale * scrollableArea.left); mScrollablePreview.setTranslationY(scale * scrollableArea.top); mScrollablePreview.setImageMatrix(matrix); - mScrollingScrim.setImageBitmap(screenBitmap); - mScrollingScrim.setVisibility(View.VISIBLE); mScrollablePreview.setImageBitmap(screenBitmap); mScrollablePreview.setVisibility(View.VISIBLE); createScreenshotFadeDismissAnimation(true).start();