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:
@@ -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;
|
||||
}
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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() {
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user