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
This commit is contained in:
Miranda Kephart
2021-06-16 19:15:41 -04:00
parent 0d5d58a15f
commit 37a60b435a
4 changed files with 139 additions and 47 deletions

View File

@@ -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;
}
});

View File

@@ -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<ScrollCaptureController.LongScreenshot> mLongScreenshot;
private final AtomicReference<ScreenshotController.TransitionDestination>
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);
}
}

View File

@@ -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<ActivityOptions, ExitTransitionCoordinator> 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<ActivityOptions, ExitTransitionCoordinator> 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<ExitTransitionCallbacks> {
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() {
}
};
}
}
}

View File

@@ -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();