Capture a new scrim for scrolling screenshots

The capture process for scrolling screenshots is disorienting
(since the app itself is actually scrolled on the display), so we
add a scrim on top. Previously we just used the original bitmap,
but this looks odd if the user scrolled between the original
capture and tapping the scroll button.

This change captures a new screenshot when the scroll button is
tapped, and shades/dims it to indicate to the user that the
scrolling process is happening.

Bug: 183197533
Test: manual; visual verification
Change-Id: Ief2111ecc93f90638c6b2abb8a8120b83e805c07
This commit is contained in:
Miranda Kephart
2021-06-18 10:06:50 -04:00
parent a389edcf51
commit a22342e871
3 changed files with 102 additions and 66 deletions

View File

@@ -19,12 +19,15 @@
android:id="@+id/global_screenshot_frame"
android:theme="@style/Screenshot"
android:layout_width="match_parent"
android:layout_height="match_parent">
android:layout_height="match_parent"
android:importantForAccessibility="no">
<ImageView
android:id="@+id/screenshot_scrolling_scrim"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:visibility="gone"/>
android:visibility="gone"
android:clickable="true"
android:importantForAccessibility="no"/>
<ImageView
android:id="@+id/global_screenshot_actions_background"
android:layout_height="@dimen/screenshot_bg_protection_height"

View File

@@ -486,9 +486,24 @@ public class ScreenshotController {
private void takeScreenshotInternal(Consumer<Uri> finisher, Rect crop) {
// copy the input Rect, since SurfaceControl.screenshot can mutate it
Rect screenRect = new Rect(crop);
Bitmap screenshot = captureScreenshot(crop);
if (screenshot == null) {
Log.e(TAG, "takeScreenshotInternal: Screenshot bitmap was null");
mNotificationsController.notifyScreenshotError(
R.string.screenshot_failed_to_capture_text);
if (mCurrentRequestCallback != null) {
mCurrentRequestCallback.reportError();
}
return;
}
saveScreenshot(screenshot, finisher, screenRect, Insets.NONE, true);
}
private Bitmap captureScreenshot(Rect crop) {
int width = crop.width();
int height = crop.height();
Bitmap screenshot = null;
final Display display = getDefaultDisplay();
final DisplayAddress address = display.getAddress();
@@ -509,18 +524,7 @@ public class ScreenshotController {
SurfaceControl.captureDisplay(captureArgs);
screenshot = screenshotBuffer == null ? null : screenshotBuffer.asBitmap();
}
if (screenshot == null) {
Log.e(TAG, "takeScreenshotInternal: Screenshot bitmap was null");
mNotificationsController.notifyScreenshotError(
R.string.screenshot_failed_to_capture_text);
if (mCurrentRequestCallback != null) {
mCurrentRequestCallback.reportError();
}
return;
}
saveScreenshot(screenshot, finisher, screenRect, Insets.NONE, true);
return screenshot;
}
private void saveScreenshot(Bitmap screenshot, Consumer<Uri> finisher, Rect screenRect,
@@ -644,45 +648,56 @@ public class ScreenshotController {
final ScrollCaptureResponse response = mLastScrollCaptureResponse;
mScreenshotView.showScrollChip(/* onClick */ () -> {
mScreenshotView.prepareScrollingTransition(response, mScreenBitmap);
// Clear the reference to prevent close() in dismissScreenshot
mLastScrollCaptureResponse = null;
final ListenableFuture<ScrollCaptureController.LongScreenshot> future =
mScrollCaptureController.run(response);
future.addListener(() -> {
ScrollCaptureController.LongScreenshot longScreenshot;
try {
longScreenshot = future.get();
} catch (CancellationException | InterruptedException | ExecutionException e) {
Log.e(TAG, "Exception", e);
return;
}
DisplayMetrics displayMetrics = new DisplayMetrics();
getDefaultDisplay().getRealMetrics(displayMetrics);
Bitmap newScreenshot = captureScreenshot(
new Rect(0, 0, displayMetrics.widthPixels, displayMetrics.heightPixels));
mLongScreenshotHolder.setLongScreenshot(longScreenshot);
mLongScreenshotHolder.setTransitionDestinationCallback(
(transitionDestination, onTransitionEnd) ->
mScreenshotView.startLongScreenshotTransition(
transitionDestination, onTransitionEnd,
longScreenshot));
mScreenshotView.prepareScrollingTransition(response, mScreenBitmap, newScreenshot);
// delay starting scroll capture to make sure the scrim is up before the app moves
mScreenshotView.post(() -> {
// Clear the reference to prevent close() in dismissScreenshot
mLastScrollCaptureResponse = null;
final ListenableFuture<ScrollCaptureController.LongScreenshot> future =
mScrollCaptureController.run(response);
future.addListener(() -> {
ScrollCaptureController.LongScreenshot longScreenshot;
try {
longScreenshot = future.get();
} catch (CancellationException
| InterruptedException
| ExecutionException e) {
Log.e(TAG, "Exception", e);
return;
}
final Intent intent = new Intent(mContext, LongScreenshotActivity.class);
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TOP);
mLongScreenshotHolder.setLongScreenshot(longScreenshot);
mLongScreenshotHolder.setTransitionDestinationCallback(
(transitionDestination, onTransitionEnd) ->
mScreenshotView.startLongScreenshotTransition(
transitionDestination, onTransitionEnd,
longScreenshot));
Pair<ActivityOptions, ExitTransitionCoordinator> transition =
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);
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 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) {
// Ignore

View File

@@ -36,8 +36,10 @@ import android.app.ActivityManager;
import android.app.Notification;
import android.app.PendingIntent;
import android.content.Context;
import android.content.res.ColorStateList;
import android.content.res.Resources;
import android.graphics.Bitmap;
import android.graphics.BlendMode;
import android.graphics.Color;
import android.graphics.Insets;
import android.graphics.Matrix;
@@ -257,10 +259,10 @@ public class ScreenshotView extends FrameLayout implements
@Override // ViewTreeObserver.OnComputeInternalInsetsListener
public void onComputeInternalInsets(ViewTreeObserver.InternalInsetsInfo inoutInfo) {
inoutInfo.setTouchableInsets(ViewTreeObserver.InternalInsetsInfo.TOUCHABLE_INSETS_REGION);
inoutInfo.touchableRegion.set(getTouchRegion());
inoutInfo.touchableRegion.set(getTouchRegion(true));
}
private Region getTouchRegion() {
private Region getTouchRegion(boolean includeScrim) {
Region touchRegion = new Region();
final Rect tmpRect = new Rect();
@@ -273,6 +275,11 @@ public class ScreenshotView extends FrameLayout implements
mDismissButton.getBoundsOnScreen(tmpRect);
touchRegion.op(tmpRect, Region.Op.UNION);
if (includeScrim && mScrollingScrim.getVisibility() == View.VISIBLE) {
mScrollingScrim.getBoundsOnScreen(tmpRect);
touchRegion.op(tmpRect, Region.Op.UNION);
}
if (QuickStepContract.isGesturalMode(mNavMode)) {
final WindowManager wm = mContext.getSystemService(WindowManager.class);
final WindowMetrics windowMetrics = wm.getCurrentWindowMetrics();
@@ -296,7 +303,7 @@ public class ScreenshotView extends FrameLayout implements
if (ev instanceof MotionEvent) {
MotionEvent event = (MotionEvent) ev;
if (event.getActionMasked() == MotionEvent.ACTION_DOWN
&& !getTouchRegion().contains(
&& !getTouchRegion(false).contains(
(int) event.getRawX(), (int) event.getRawY())) {
mCallbacks.onTouchOutside();
}
@@ -313,6 +320,10 @@ public class ScreenshotView extends FrameLayout implements
@Override // ViewGroup
public boolean onInterceptTouchEvent(MotionEvent ev) {
// scrolling scrim should not be swipeable; return early if we're on the scrim
if (!getTouchRegion(false).contains((int) ev.getRawX(), (int) ev.getRawY())) {
return false;
}
// always pass through the down event so the swipe handler knows the initial state
if (ev.getActionMasked() == MotionEvent.ACTION_DOWN) {
mSwipeDismissHandler.onTouch(this, ev);
@@ -374,10 +385,6 @@ public class ScreenshotView extends FrameLayout implements
return mScreenshotPreview;
}
View getScrollablePreview() {
return mScrollablePreview;
}
/**
* Set up the logger and callback on dismissal.
*
@@ -808,8 +815,9 @@ public class ScreenshotView extends FrameLayout implements
anim.start();
}
void prepareScrollingTransition(ScrollCaptureResponse response, Bitmap screenBitmap) {
mScrollingScrim.setImageBitmap(screenBitmap);
void prepareScrollingTransition(ScrollCaptureResponse response, Bitmap screenBitmap,
Bitmap newBitmap) {
mScrollingScrim.setImageBitmap(newBitmap);
mScrollingScrim.setVisibility(View.VISIBLE);
Rect scrollableArea = scrollableAreaOnScreen(response);
float scale = mCornerSizeX
@@ -829,7 +837,19 @@ public class ScreenshotView extends FrameLayout implements
mScrollablePreview.setImageBitmap(screenBitmap);
mScrollablePreview.setVisibility(View.VISIBLE);
createScreenshotFadeDismissAnimation(true).start();
mDismissButton.setVisibility(View.GONE);
mActionsContainer.setVisibility(View.GONE);
mBackgroundProtection.setVisibility(View.GONE);
// set these invisible, but not gone, so that the views are laid out correctly
mActionsContainerBackground.setVisibility(View.INVISIBLE);
mScreenshotPreviewBorder.setVisibility(View.INVISIBLE);
mScreenshotPreview.setVisibility(View.INVISIBLE);
mScrollingScrim.setImageTintBlendMode(BlendMode.SRC_ATOP);
ValueAnimator anim = ValueAnimator.ofFloat(0, .3f);
anim.addUpdateListener(animation -> mScrollingScrim.setImageTintList(
ColorStateList.valueOf(Color.argb((float) animation.getAnimatedValue(), 0, 0, 0))));
anim.setDuration(200);
anim.start();
}
boolean isDismissing() {
@@ -910,6 +930,7 @@ public class ScreenshotView extends FrameLayout implements
mContext.getResources().getString(R.string.screenshot_preview_description));
mScreenshotPreview.setOnClickListener(null);
mShareChip.setOnClickListener(null);
mScrollingScrim.setVisibility(View.GONE);
mEditChip.setOnClickListener(null);
mShareChip.setIsPending(false);
mEditChip.setIsPending(false);
@@ -932,7 +953,7 @@ public class ScreenshotView extends FrameLayout implements
transition.action.actionIntent.send();
// fade out non-preview UI
createScreenshotFadeDismissAnimation(false).start();
createScreenshotFadeDismissAnimation().start();
} catch (PendingIntent.CanceledException e) {
mPendingSharedTransition = false;
if (transition.onCancelRunnable != null) {
@@ -970,7 +991,7 @@ public class ScreenshotView extends FrameLayout implements
return animSet;
}
ValueAnimator createScreenshotFadeDismissAnimation(boolean fadePreview) {
ValueAnimator createScreenshotFadeDismissAnimation() {
ValueAnimator alphaAnim = ValueAnimator.ofFloat(0, 1);
alphaAnim.addUpdateListener(animation -> {
float alpha = 1 - animation.getAnimatedFraction();
@@ -979,9 +1000,6 @@ public class ScreenshotView extends FrameLayout implements
mActionsContainer.setAlpha(alpha);
mBackgroundProtection.setAlpha(alpha);
mScreenshotPreviewBorder.setAlpha(alpha);
if (fadePreview) {
mScreenshotPreview.setAlpha(alpha);
}
});
alphaAnim.setDuration(600);
return alphaAnim;