• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2020 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.systemui.screenshot;
18 
19 import static android.content.res.Configuration.ORIENTATION_PORTRAIT;
20 
21 import static com.android.systemui.screenshot.LogConfig.DEBUG_ANIM;
22 import static com.android.systemui.screenshot.LogConfig.DEBUG_DISMISS;
23 import static com.android.systemui.screenshot.LogConfig.DEBUG_INPUT;
24 import static com.android.systemui.screenshot.LogConfig.DEBUG_SCROLL;
25 import static com.android.systemui.screenshot.LogConfig.DEBUG_UI;
26 import static com.android.systemui.screenshot.LogConfig.DEBUG_WINDOW;
27 import static com.android.systemui.screenshot.LogConfig.logTag;
28 
29 import static java.util.Objects.requireNonNull;
30 
31 import android.animation.Animator;
32 import android.animation.AnimatorListenerAdapter;
33 import android.animation.AnimatorSet;
34 import android.animation.ValueAnimator;
35 import android.app.ActivityManager;
36 import android.app.Notification;
37 import android.app.PendingIntent;
38 import android.content.Context;
39 import android.content.res.ColorStateList;
40 import android.content.res.Resources;
41 import android.graphics.Bitmap;
42 import android.graphics.BlendMode;
43 import android.graphics.Color;
44 import android.graphics.Insets;
45 import android.graphics.Matrix;
46 import android.graphics.PointF;
47 import android.graphics.Rect;
48 import android.graphics.Region;
49 import android.graphics.drawable.BitmapDrawable;
50 import android.graphics.drawable.ColorDrawable;
51 import android.graphics.drawable.Drawable;
52 import android.graphics.drawable.Icon;
53 import android.graphics.drawable.InsetDrawable;
54 import android.graphics.drawable.LayerDrawable;
55 import android.os.Looper;
56 import android.os.RemoteException;
57 import android.util.AttributeSet;
58 import android.util.DisplayMetrics;
59 import android.util.Log;
60 import android.util.MathUtils;
61 import android.view.Choreographer;
62 import android.view.Display;
63 import android.view.DisplayCutout;
64 import android.view.GestureDetector;
65 import android.view.LayoutInflater;
66 import android.view.MotionEvent;
67 import android.view.ScrollCaptureResponse;
68 import android.view.TouchDelegate;
69 import android.view.View;
70 import android.view.ViewGroup;
71 import android.view.ViewTreeObserver;
72 import android.view.WindowInsets;
73 import android.view.WindowManager;
74 import android.view.WindowMetrics;
75 import android.view.accessibility.AccessibilityManager;
76 import android.view.animation.AccelerateInterpolator;
77 import android.view.animation.AnimationUtils;
78 import android.view.animation.Interpolator;
79 import android.widget.FrameLayout;
80 import android.widget.HorizontalScrollView;
81 import android.widget.ImageView;
82 import android.widget.LinearLayout;
83 
84 import androidx.constraintlayout.widget.ConstraintLayout;
85 
86 import com.android.internal.logging.UiEventLogger;
87 import com.android.systemui.R;
88 import com.android.systemui.screenshot.ScreenshotController.SavedImageData.ActionTransition;
89 import com.android.systemui.shared.system.InputMonitorCompat;
90 import com.android.systemui.shared.system.QuickStepContract;
91 
92 import java.util.ArrayList;
93 import java.util.function.Consumer;
94 
95 /**
96  * Handles the visual elements and animations for the screenshot flow.
97  */
98 public class ScreenshotView extends FrameLayout implements
99         ViewTreeObserver.OnComputeInternalInsetsListener {
100 
101     interface ScreenshotViewCallback {
onUserInteraction()102         void onUserInteraction();
103 
onDismiss()104         void onDismiss();
105 
106         /** DOWN motion event was observed outside of the touchable areas of this view. */
onTouchOutside()107         void onTouchOutside();
108     }
109 
110     private static final String TAG = logTag(ScreenshotView.class);
111 
112     private static final long SCREENSHOT_FLASH_IN_DURATION_MS = 133;
113     private static final long SCREENSHOT_FLASH_OUT_DURATION_MS = 217;
114     // delay before starting to fade in dismiss button
115     private static final long SCREENSHOT_TO_CORNER_DISMISS_DELAY_MS = 200;
116     private static final long SCREENSHOT_TO_CORNER_X_DURATION_MS = 234;
117     private static final long SCREENSHOT_TO_CORNER_Y_DURATION_MS = 500;
118     private static final long SCREENSHOT_TO_CORNER_SCALE_DURATION_MS = 234;
119     private static final long SCREENSHOT_ACTIONS_EXPANSION_DURATION_MS = 400;
120     private static final long SCREENSHOT_ACTIONS_ALPHA_DURATION_MS = 100;
121     private static final long SCREENSHOT_DISMISS_Y_DURATION_MS = 350;
122     private static final long SCREENSHOT_DISMISS_ALPHA_DURATION_MS = 183;
123     private static final long SCREENSHOT_DISMISS_ALPHA_OFFSET_MS = 50; // delay before starting fade
124     private static final float SCREENSHOT_ACTIONS_START_SCALE_X = .7f;
125     private static final float ROUNDED_CORNER_RADIUS = .25f;
126     private static final int SWIPE_PADDING_DP = 12; // extra padding around views to allow swipe
127 
128     private final Interpolator mAccelerateInterpolator = new AccelerateInterpolator();
129 
130     private final Resources mResources;
131     private final Interpolator mFastOutSlowIn;
132     private final DisplayMetrics mDisplayMetrics;
133     private final float mCornerSizeX;
134     private final float mDismissDeltaY;
135     private final AccessibilityManager mAccessibilityManager;
136 
137     private int mNavMode;
138     private boolean mOrientationPortrait;
139     private boolean mDirectionLTR;
140     private int mStaticLeftMargin;
141 
142     private ScreenshotSelectorView mScreenshotSelectorView;
143     private ImageView mScrollingScrim;
144     private View mScreenshotStatic;
145     private ImageView mScreenshotPreview;
146     private View mTransitionView;
147     private View mScreenshotPreviewBorder;
148     private ImageView mScrollablePreview;
149     private ImageView mScreenshotFlash;
150     private ImageView mActionsContainerBackground;
151     private HorizontalScrollView mActionsContainer;
152     private LinearLayout mActionsView;
153     private ImageView mBackgroundProtection;
154     private FrameLayout mDismissButton;
155     private ScreenshotActionChip mShareChip;
156     private ScreenshotActionChip mEditChip;
157     private ScreenshotActionChip mScrollChip;
158     private ScreenshotActionChip mQuickShareChip;
159 
160     private UiEventLogger mUiEventLogger;
161     private ScreenshotViewCallback mCallbacks;
162     private Animator mDismissAnimation;
163     private boolean mPendingSharedTransition;
164     private GestureDetector mSwipeDetector;
165     private SwipeDismissHandler mSwipeDismissHandler;
166     private InputMonitorCompat mInputMonitor;
167     private boolean mShowScrollablePreview;
168 
169     private final ArrayList<ScreenshotActionChip> mSmartChips = new ArrayList<>();
170     private PendingInteraction mPendingInteraction;
171 
172     private enum PendingInteraction {
173         PREVIEW,
174         EDIT,
175         SHARE,
176         QUICK_SHARE
177     }
178 
ScreenshotView(Context context)179     public ScreenshotView(Context context) {
180         this(context, null);
181     }
182 
ScreenshotView(Context context, AttributeSet attrs)183     public ScreenshotView(Context context, AttributeSet attrs) {
184         this(context, attrs, 0);
185     }
186 
ScreenshotView(Context context, AttributeSet attrs, int defStyleAttr)187     public ScreenshotView(Context context, AttributeSet attrs, int defStyleAttr) {
188         this(context, attrs, defStyleAttr, 0);
189     }
190 
ScreenshotView( Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes)191     public ScreenshotView(
192             Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
193         super(context, attrs, defStyleAttr, defStyleRes);
194         mResources = mContext.getResources();
195 
196         mCornerSizeX = mResources.getDimensionPixelSize(R.dimen.global_screenshot_x_scale);
197         mDismissDeltaY = mResources.getDimensionPixelSize(
198                 R.dimen.screenshot_dismissal_height_delta);
199 
200         // standard material ease
201         mFastOutSlowIn =
202                 AnimationUtils.loadInterpolator(mContext, android.R.interpolator.fast_out_slow_in);
203 
204         mDisplayMetrics = new DisplayMetrics();
205         mContext.getDisplay().getRealMetrics(mDisplayMetrics);
206 
207         mAccessibilityManager = AccessibilityManager.getInstance(mContext);
208 
209         mSwipeDetector = new GestureDetector(mContext,
210                 new GestureDetector.SimpleOnGestureListener() {
211                     final Rect mActionsRect = new Rect();
212 
213                     @Override
214                     public boolean onScroll(
215                             MotionEvent ev1, MotionEvent ev2, float distanceX, float distanceY) {
216                         mActionsContainer.getBoundsOnScreen(mActionsRect);
217                         // return true if we aren't in the actions bar, or if we are but it isn't
218                         // scrollable in the direction of movement
219                         return !mActionsRect.contains((int) ev2.getRawX(), (int) ev2.getRawY())
220                                 || !mActionsContainer.canScrollHorizontally((int) distanceX);
221                     }
222                 });
223         mSwipeDetector.setIsLongpressEnabled(false);
224         mSwipeDismissHandler = new SwipeDismissHandler();
225         addOnAttachStateChangeListener(new OnAttachStateChangeListener() {
226             @Override
227             public void onViewAttachedToWindow(View v) {
228                 startInputListening();
229             }
230 
231             @Override
232             public void onViewDetachedFromWindow(View v) {
233                 stopInputListening();
234             }
235         });
236     }
237 
hideScrollChip()238     public void hideScrollChip() {
239         mScrollChip.setVisibility(View.GONE);
240     }
241 
242     /**
243      * Called to display the scroll action chip when support is detected.
244      *
245      * @param onClick the action to take when the chip is clicked.
246      */
showScrollChip(Runnable onClick)247     public void showScrollChip(Runnable onClick) {
248         if (DEBUG_SCROLL) {
249             Log.d(TAG, "Showing Scroll option");
250         }
251         mUiEventLogger.log(ScreenshotEvent.SCREENSHOT_LONG_SCREENSHOT_IMPRESSION);
252         mScrollChip.setVisibility(VISIBLE);
253         mScrollChip.setOnClickListener((v) -> {
254             if (DEBUG_INPUT) {
255                 Log.d(TAG, "scroll chip tapped");
256             }
257             mUiEventLogger.log(ScreenshotEvent.SCREENSHOT_LONG_SCREENSHOT_REQUESTED);
258             onClick.run();
259         });
260     }
261 
262     @Override // ViewTreeObserver.OnComputeInternalInsetsListener
onComputeInternalInsets(ViewTreeObserver.InternalInsetsInfo inoutInfo)263     public void onComputeInternalInsets(ViewTreeObserver.InternalInsetsInfo inoutInfo) {
264         inoutInfo.setTouchableInsets(ViewTreeObserver.InternalInsetsInfo.TOUCHABLE_INSETS_REGION);
265         inoutInfo.touchableRegion.set(getTouchRegion(true));
266     }
267 
getTouchRegion(boolean includeScrim)268     private Region getTouchRegion(boolean includeScrim) {
269         Region touchRegion = new Region();
270 
271         final Rect tmpRect = new Rect();
272         mScreenshotPreview.getBoundsOnScreen(tmpRect);
273         tmpRect.inset((int) dpToPx(-SWIPE_PADDING_DP), (int) dpToPx(-SWIPE_PADDING_DP));
274         touchRegion.op(tmpRect, Region.Op.UNION);
275         mActionsContainerBackground.getBoundsOnScreen(tmpRect);
276         tmpRect.inset((int) dpToPx(-SWIPE_PADDING_DP), (int) dpToPx(-SWIPE_PADDING_DP));
277         touchRegion.op(tmpRect, Region.Op.UNION);
278         mDismissButton.getBoundsOnScreen(tmpRect);
279         touchRegion.op(tmpRect, Region.Op.UNION);
280 
281         if (includeScrim && mScrollingScrim.getVisibility() == View.VISIBLE) {
282             mScrollingScrim.getBoundsOnScreen(tmpRect);
283             touchRegion.op(tmpRect, Region.Op.UNION);
284         }
285 
286         if (QuickStepContract.isGesturalMode(mNavMode)) {
287             final WindowManager wm = mContext.getSystemService(WindowManager.class);
288             final WindowMetrics windowMetrics = wm.getCurrentWindowMetrics();
289             final Insets gestureInsets = windowMetrics.getWindowInsets().getInsets(
290                     WindowInsets.Type.systemGestures());
291             // Receive touches in gesture insets such that they don't cause TOUCH_OUTSIDE
292             Rect inset = new Rect(0, 0, gestureInsets.left, mDisplayMetrics.heightPixels);
293             touchRegion.op(inset, Region.Op.UNION);
294             inset.set(mDisplayMetrics.widthPixels - gestureInsets.right, 0,
295                     mDisplayMetrics.widthPixels, mDisplayMetrics.heightPixels);
296             touchRegion.op(inset, Region.Op.UNION);
297         }
298         return touchRegion;
299     }
300 
startInputListening()301     private void startInputListening() {
302         stopInputListening();
303         mInputMonitor = new InputMonitorCompat("Screenshot", Display.DEFAULT_DISPLAY);
304         mInputMonitor.getInputReceiver(Looper.getMainLooper(), Choreographer.getInstance(),
305                 ev -> {
306                     if (ev instanceof MotionEvent) {
307                         MotionEvent event = (MotionEvent) ev;
308                         if (event.getActionMasked() == MotionEvent.ACTION_DOWN
309                                 && !getTouchRegion(false).contains(
310                                 (int) event.getRawX(), (int) event.getRawY())) {
311                             mCallbacks.onTouchOutside();
312                         }
313                     }
314                 });
315     }
316 
stopInputListening()317     private void stopInputListening() {
318         if (mInputMonitor != null) {
319             mInputMonitor.dispose();
320             mInputMonitor = null;
321         }
322     }
323 
324     @Override // ViewGroup
onInterceptTouchEvent(MotionEvent ev)325     public boolean onInterceptTouchEvent(MotionEvent ev) {
326         // scrolling scrim should not be swipeable; return early if we're on the scrim
327         if (!getTouchRegion(false).contains((int) ev.getRawX(), (int) ev.getRawY())) {
328             return false;
329         }
330         // always pass through the down event so the swipe handler knows the initial state
331         if (ev.getActionMasked() == MotionEvent.ACTION_DOWN) {
332             mSwipeDismissHandler.onTouch(this, ev);
333         }
334         return mSwipeDetector.onTouchEvent(ev);
335     }
336 
337     @Override // View
onFinishInflate()338     protected void onFinishInflate() {
339         mScrollingScrim = requireNonNull(findViewById(R.id.screenshot_scrolling_scrim));
340         mScreenshotStatic = requireNonNull(findViewById(R.id.global_screenshot_static));
341         mScreenshotPreview = requireNonNull(findViewById(R.id.global_screenshot_preview));
342         mTransitionView = requireNonNull(findViewById(R.id.screenshot_transition_view));
343         mScreenshotPreviewBorder = requireNonNull(
344                 findViewById(R.id.global_screenshot_preview_border));
345         mScreenshotPreview.setClipToOutline(true);
346 
347         mActionsContainerBackground = requireNonNull(findViewById(
348                 R.id.global_screenshot_actions_container_background));
349         mActionsContainer = requireNonNull(findViewById(R.id.global_screenshot_actions_container));
350         mActionsView = requireNonNull(findViewById(R.id.global_screenshot_actions));
351         mBackgroundProtection = requireNonNull(
352                 findViewById(R.id.global_screenshot_actions_background));
353         mDismissButton = requireNonNull(findViewById(R.id.global_screenshot_dismiss_button));
354         mScrollablePreview = requireNonNull(findViewById(R.id.screenshot_scrollable_preview));
355         mScreenshotFlash = requireNonNull(findViewById(R.id.global_screenshot_flash));
356         mScreenshotSelectorView = requireNonNull(findViewById(R.id.global_screenshot_selector));
357         mShareChip = requireNonNull(mActionsContainer.findViewById(R.id.screenshot_share_chip));
358         mEditChip = requireNonNull(mActionsContainer.findViewById(R.id.screenshot_edit_chip));
359         mScrollChip = requireNonNull(mActionsContainer.findViewById(R.id.screenshot_scroll_chip));
360 
361         int swipePaddingPx = (int) dpToPx(SWIPE_PADDING_DP);
362         TouchDelegate previewDelegate = new TouchDelegate(
363                 new Rect(swipePaddingPx, swipePaddingPx, swipePaddingPx, swipePaddingPx),
364                 mScreenshotPreview);
365         mScreenshotPreview.setTouchDelegate(previewDelegate);
366         TouchDelegate actionsDelegate = new TouchDelegate(
367                 new Rect(swipePaddingPx, swipePaddingPx, swipePaddingPx, swipePaddingPx),
368                 mActionsContainerBackground);
369         mActionsContainerBackground.setTouchDelegate(actionsDelegate);
370 
371         setFocusable(true);
372         mScreenshotSelectorView.setFocusable(true);
373         mScreenshotSelectorView.setFocusableInTouchMode(true);
374         mActionsContainer.setScrollX(0);
375 
376         mNavMode = getResources().getInteger(
377                 com.android.internal.R.integer.config_navBarInteractionMode);
378         mOrientationPortrait =
379                 getResources().getConfiguration().orientation == ORIENTATION_PORTRAIT;
380         mDirectionLTR =
381                 getResources().getConfiguration().getLayoutDirection() == View.LAYOUT_DIRECTION_LTR;
382 
383         // Get focus so that the key events go to the layout.
384         setFocusableInTouchMode(true);
385         requestFocus();
386     }
387 
getTransitionView()388     View getTransitionView() {
389         return mTransitionView;
390     }
391 
getStaticLeftMargin()392     int getStaticLeftMargin() {
393         return mStaticLeftMargin;
394     }
395 
396     /**
397      * Set up the logger and callback on dismissal.
398      *
399      * Note: must be called before any other (non-constructor) method or null pointer exceptions
400      * may occur.
401      */
init(UiEventLogger uiEventLogger, ScreenshotViewCallback callbacks)402     void init(UiEventLogger uiEventLogger, ScreenshotViewCallback callbacks) {
403         mUiEventLogger = uiEventLogger;
404         mCallbacks = callbacks;
405     }
406 
takePartialScreenshot(Consumer<Rect> onPartialScreenshotSelected)407     void takePartialScreenshot(Consumer<Rect> onPartialScreenshotSelected) {
408         mScreenshotSelectorView.setOnScreenshotSelected(onPartialScreenshotSelected);
409         mScreenshotSelectorView.setVisibility(View.VISIBLE);
410         mScreenshotSelectorView.requestFocus();
411     }
412 
setScreenshot(Bitmap bitmap, Insets screenInsets)413     void setScreenshot(Bitmap bitmap, Insets screenInsets) {
414         mScreenshotPreview.setImageDrawable(createScreenDrawable(mResources, bitmap, screenInsets));
415     }
416 
updateDisplayCutoutMargins(DisplayCutout cutout)417     void updateDisplayCutoutMargins(DisplayCutout cutout) {
418         int orientation = mContext.getResources().getConfiguration().orientation;
419         mOrientationPortrait = (orientation == ORIENTATION_PORTRAIT);
420         FrameLayout.LayoutParams p =
421                 (FrameLayout.LayoutParams) mScreenshotStatic.getLayoutParams();
422         if (cutout == null) {
423             p.setMargins(0, 0, 0, 0);
424         } else {
425             Insets waterfall = cutout.getWaterfallInsets();
426             if (mOrientationPortrait) {
427                 p.setMargins(waterfall.left, Math.max(cutout.getSafeInsetTop(), waterfall.top),
428                         waterfall.right, Math.max(cutout.getSafeInsetBottom(), waterfall.bottom));
429             } else {
430                 p.setMargins(Math.max(cutout.getSafeInsetLeft(), waterfall.left), waterfall.top,
431                         Math.max(cutout.getSafeInsetRight(), waterfall.right), waterfall.bottom);
432             }
433         }
434         mStaticLeftMargin = p.leftMargin;
435         mScreenshotStatic.setLayoutParams(p);
436         mScreenshotStatic.requestLayout();
437     }
438 
updateOrientation(DisplayCutout cutout)439     void updateOrientation(DisplayCutout cutout) {
440         int orientation = mContext.getResources().getConfiguration().orientation;
441         mOrientationPortrait = (orientation == ORIENTATION_PORTRAIT);
442         updateDisplayCutoutMargins(cutout);
443         int screenshotFixedSize =
444                 mContext.getResources().getDimensionPixelSize(R.dimen.global_screenshot_x_scale);
445         ViewGroup.LayoutParams params = mScreenshotPreview.getLayoutParams();
446         if (mOrientationPortrait) {
447             params.width = screenshotFixedSize;
448             params.height = LayoutParams.WRAP_CONTENT;
449             mScreenshotPreview.setScaleType(ImageView.ScaleType.FIT_START);
450         } else {
451             params.width = LayoutParams.WRAP_CONTENT;
452             params.height = screenshotFixedSize;
453             mScreenshotPreview.setScaleType(ImageView.ScaleType.FIT_END);
454         }
455 
456         mScreenshotPreview.setLayoutParams(params);
457     }
458 
createScreenshotDropInAnimation(Rect bounds, boolean showFlash)459     AnimatorSet createScreenshotDropInAnimation(Rect bounds, boolean showFlash) {
460         if (DEBUG_ANIM) {
461             Log.d(TAG, "createAnim: bounds=" + bounds + " showFlash=" + showFlash);
462         }
463 
464         Rect targetPosition = new Rect();
465         mScreenshotPreview.getHitRect(targetPosition);
466 
467         // ratio of preview width, end vs. start size
468         float cornerScale =
469                 mCornerSizeX / (mOrientationPortrait ? bounds.width() : bounds.height());
470         final float currentScale = 1 / cornerScale;
471 
472         AnimatorSet dropInAnimation = new AnimatorSet();
473         ValueAnimator flashInAnimator = ValueAnimator.ofFloat(0, 1);
474         flashInAnimator.setDuration(SCREENSHOT_FLASH_IN_DURATION_MS);
475         flashInAnimator.setInterpolator(mFastOutSlowIn);
476         flashInAnimator.addUpdateListener(animation ->
477                 mScreenshotFlash.setAlpha((float) animation.getAnimatedValue()));
478 
479         ValueAnimator flashOutAnimator = ValueAnimator.ofFloat(1, 0);
480         flashOutAnimator.setDuration(SCREENSHOT_FLASH_OUT_DURATION_MS);
481         flashOutAnimator.setInterpolator(mFastOutSlowIn);
482         flashOutAnimator.addUpdateListener(animation ->
483                 mScreenshotFlash.setAlpha((float) animation.getAnimatedValue()));
484 
485         // animate from the current location, to the static preview location
486         final PointF startPos = new PointF(bounds.centerX(), bounds.centerY());
487         final PointF finalPos = new PointF(targetPosition.exactCenterX(),
488                 targetPosition.exactCenterY());
489 
490         // Shift to screen coordinates so that the animation runs on top of the entire screen,
491         // including e.g. bars covering the display cutout.
492         int[] locInScreen = mScreenshotPreview.getLocationOnScreen();
493         startPos.offset(targetPosition.left - locInScreen[0], targetPosition.top - locInScreen[1]);
494 
495         if (DEBUG_ANIM) {
496             Log.d(TAG, "toCorner: startPos=" + startPos);
497             Log.d(TAG, "toCorner: finalPos=" + finalPos);
498         }
499 
500         ValueAnimator toCorner = ValueAnimator.ofFloat(0, 1);
501         toCorner.setDuration(SCREENSHOT_TO_CORNER_Y_DURATION_MS);
502 
503         toCorner.addListener(new AnimatorListenerAdapter() {
504             @Override
505             public void onAnimationStart(Animator animation) {
506                 mScreenshotPreview.setScaleX(currentScale);
507                 mScreenshotPreview.setScaleY(currentScale);
508                 mScreenshotPreview.setVisibility(View.VISIBLE);
509                 if (mAccessibilityManager.isEnabled()) {
510                     mDismissButton.setAlpha(0);
511                     mDismissButton.setVisibility(View.VISIBLE);
512                 }
513             }
514         });
515 
516         float xPositionPct =
517                 SCREENSHOT_TO_CORNER_X_DURATION_MS / (float) SCREENSHOT_TO_CORNER_Y_DURATION_MS;
518         float dismissPct =
519                 SCREENSHOT_TO_CORNER_DISMISS_DELAY_MS / (float) SCREENSHOT_TO_CORNER_Y_DURATION_MS;
520         float scalePct =
521                 SCREENSHOT_TO_CORNER_SCALE_DURATION_MS / (float) SCREENSHOT_TO_CORNER_Y_DURATION_MS;
522         toCorner.addUpdateListener(animation -> {
523             float t = animation.getAnimatedFraction();
524             if (t < scalePct) {
525                 float scale = MathUtils.lerp(
526                         currentScale, 1, mFastOutSlowIn.getInterpolation(t / scalePct));
527                 mScreenshotPreview.setScaleX(scale);
528                 mScreenshotPreview.setScaleY(scale);
529             } else {
530                 mScreenshotPreview.setScaleX(1);
531                 mScreenshotPreview.setScaleY(1);
532             }
533 
534             if (t < xPositionPct) {
535                 float xCenter = MathUtils.lerp(startPos.x, finalPos.x,
536                         mFastOutSlowIn.getInterpolation(t / xPositionPct));
537                 mScreenshotPreview.setX(xCenter - mScreenshotPreview.getWidth() / 2f);
538             } else {
539                 mScreenshotPreview.setX(finalPos.x - mScreenshotPreview.getWidth() / 2f);
540             }
541             float yCenter = MathUtils.lerp(
542                     startPos.y, finalPos.y, mFastOutSlowIn.getInterpolation(t));
543             mScreenshotPreview.setY(yCenter - mScreenshotPreview.getHeight() / 2f);
544 
545             if (t >= dismissPct) {
546                 mDismissButton.setAlpha((t - dismissPct) / (1 - dismissPct));
547                 float currentX = mScreenshotPreview.getX();
548                 float currentY = mScreenshotPreview.getY();
549                 mDismissButton.setY(currentY - mDismissButton.getHeight() / 2f);
550                 if (mDirectionLTR) {
551                     mDismissButton.setX(currentX + mScreenshotPreview.getWidth()
552                             - mDismissButton.getWidth() / 2f);
553                 } else {
554                     mDismissButton.setX(currentX - mDismissButton.getWidth() / 2f);
555                 }
556             }
557         });
558 
559         mScreenshotFlash.setAlpha(0f);
560         mScreenshotFlash.setVisibility(View.VISIBLE);
561 
562         ValueAnimator borderFadeIn = ValueAnimator.ofFloat(0, 1);
563         borderFadeIn.setDuration(100);
564         borderFadeIn.addUpdateListener((animation) ->
565                 mScreenshotPreviewBorder.setAlpha(animation.getAnimatedFraction()));
566 
567         if (showFlash) {
568             dropInAnimation.play(flashOutAnimator).after(flashInAnimator);
569             dropInAnimation.play(flashOutAnimator).with(toCorner);
570         } else {
571             dropInAnimation.play(toCorner);
572         }
573         dropInAnimation.play(borderFadeIn).after(toCorner);
574 
575         dropInAnimation.addListener(new AnimatorListenerAdapter() {
576             @Override
577             public void onAnimationEnd(Animator animation) {
578                 if (DEBUG_ANIM) {
579                     Log.d(TAG, "drop-in animation ended");
580                 }
581                 mDismissButton.setOnClickListener(view -> {
582                     if (DEBUG_INPUT) {
583                         Log.d(TAG, "dismiss button clicked");
584                     }
585                     mUiEventLogger.log(ScreenshotEvent.SCREENSHOT_EXPLICIT_DISMISSAL);
586                     animateDismissal();
587                 });
588                 mDismissButton.setAlpha(1);
589                 float dismissOffset = mDismissButton.getWidth() / 2f;
590                 float finalDismissX = mDirectionLTR
591                         ? finalPos.x - dismissOffset + bounds.width() * cornerScale / 2f
592                         : finalPos.x - dismissOffset - bounds.width() * cornerScale / 2f;
593                 mDismissButton.setX(finalDismissX);
594                 mDismissButton.setY(
595                         finalPos.y - dismissOffset - bounds.height() * cornerScale / 2f);
596                 mScreenshotPreview.setScaleX(1);
597                 mScreenshotPreview.setScaleY(1);
598                 mScreenshotPreview.setX(finalPos.x - mScreenshotPreview.getWidth() / 2f);
599                 mScreenshotPreview.setY(finalPos.y - mScreenshotPreview.getHeight() / 2f);
600                 requestLayout();
601 
602                 createScreenshotActionsShadeAnimation().start();
603 
604                 setOnTouchListener(mSwipeDismissHandler);
605             }
606         });
607 
608         return dropInAnimation;
609     }
610 
createScreenshotActionsShadeAnimation()611     ValueAnimator createScreenshotActionsShadeAnimation() {
612         // By default the activities won't be able to start immediately; override this to keep
613         // the same behavior as if started from a notification
614         try {
615             ActivityManager.getService().resumeAppSwitches();
616         } catch (RemoteException e) {
617         }
618 
619         ArrayList<ScreenshotActionChip> chips = new ArrayList<>();
620 
621         mShareChip.setContentDescription(mContext.getString(com.android.internal.R.string.share));
622         mShareChip.setIcon(Icon.createWithResource(mContext, R.drawable.ic_screenshot_share), true);
623         mShareChip.setOnClickListener(v -> {
624             mShareChip.setIsPending(true);
625             mEditChip.setIsPending(false);
626             if (mQuickShareChip != null) {
627                 mQuickShareChip.setIsPending(false);
628             }
629             mPendingInteraction = PendingInteraction.SHARE;
630         });
631         chips.add(mShareChip);
632 
633         mEditChip.setContentDescription(mContext.getString(R.string.screenshot_edit_label));
634         mEditChip.setIcon(Icon.createWithResource(mContext, R.drawable.ic_screenshot_edit), true);
635         mEditChip.setOnClickListener(v -> {
636             mEditChip.setIsPending(true);
637             mShareChip.setIsPending(false);
638             if (mQuickShareChip != null) {
639                 mQuickShareChip.setIsPending(false);
640             }
641             mPendingInteraction = PendingInteraction.EDIT;
642         });
643         chips.add(mEditChip);
644 
645         mScreenshotPreview.setOnClickListener(v -> {
646             mShareChip.setIsPending(false);
647             mEditChip.setIsPending(false);
648             if (mQuickShareChip != null) {
649                 mQuickShareChip.setIsPending(false);
650             }
651             mPendingInteraction = PendingInteraction.PREVIEW;
652         });
653 
654         mScrollChip.setText(mContext.getString(R.string.screenshot_scroll_label));
655         mScrollChip.setIcon(Icon.createWithResource(mContext,
656                 R.drawable.ic_screenshot_scroll), true);
657         chips.add(mScrollChip);
658 
659         // remove the margin from the last chip so that it's correctly aligned with the end
660         LinearLayout.LayoutParams params = (LinearLayout.LayoutParams)
661                 mActionsView.getChildAt(0).getLayoutParams();
662         params.setMarginEnd(0);
663         mActionsView.getChildAt(0).setLayoutParams(params);
664 
665         ValueAnimator animator = ValueAnimator.ofFloat(0, 1);
666         animator.setDuration(SCREENSHOT_ACTIONS_EXPANSION_DURATION_MS);
667         float alphaFraction = (float) SCREENSHOT_ACTIONS_ALPHA_DURATION_MS
668                 / SCREENSHOT_ACTIONS_EXPANSION_DURATION_MS;
669         mActionsContainer.setAlpha(0f);
670         mActionsContainerBackground.setAlpha(0f);
671         mActionsContainer.setVisibility(View.VISIBLE);
672         mActionsContainerBackground.setVisibility(View.VISIBLE);
673 
674         animator.addUpdateListener(animation -> {
675             float t = animation.getAnimatedFraction();
676             mBackgroundProtection.setAlpha(t);
677             float containerAlpha = t < alphaFraction ? t / alphaFraction : 1;
678             mActionsContainer.setAlpha(containerAlpha);
679             mActionsContainerBackground.setAlpha(containerAlpha);
680             float containerScale = SCREENSHOT_ACTIONS_START_SCALE_X
681                     + (t * (1 - SCREENSHOT_ACTIONS_START_SCALE_X));
682             mActionsContainer.setScaleX(containerScale);
683             mActionsContainerBackground.setScaleX(containerScale);
684             for (ScreenshotActionChip chip : chips) {
685                 chip.setAlpha(t);
686                 chip.setScaleX(1 / containerScale); // invert to keep size of children constant
687             }
688             mActionsContainer.setScrollX(mDirectionLTR ? 0 : mActionsContainer.getWidth());
689             mActionsContainer.setPivotX(mDirectionLTR ? 0 : mActionsContainer.getWidth());
690             mActionsContainerBackground.setPivotX(
691                     mDirectionLTR ? 0 : mActionsContainerBackground.getWidth());
692         });
693         return animator;
694     }
695 
setChipIntents(ScreenshotController.SavedImageData imageData)696     void setChipIntents(ScreenshotController.SavedImageData imageData) {
697         mShareChip.setOnClickListener(v -> {
698             mUiEventLogger.log(ScreenshotEvent.SCREENSHOT_SHARE_TAPPED);
699             startSharedTransition(
700                     imageData.shareTransition.get());
701         });
702         mEditChip.setOnClickListener(v -> {
703             mUiEventLogger.log(ScreenshotEvent.SCREENSHOT_EDIT_TAPPED);
704             startSharedTransition(
705                     imageData.editTransition.get());
706         });
707         mScreenshotPreview.setOnClickListener(v -> {
708             mUiEventLogger.log(ScreenshotEvent.SCREENSHOT_PREVIEW_TAPPED);
709             startSharedTransition(
710                     imageData.editTransition.get());
711         });
712         if (mQuickShareChip != null) {
713             mQuickShareChip.setPendingIntent(imageData.quickShareAction.actionIntent,
714                     () -> {
715                         mUiEventLogger.log(ScreenshotEvent.SCREENSHOT_SMART_ACTION_TAPPED);
716                         animateDismissal();
717                     });
718         }
719 
720         if (mPendingInteraction != null) {
721             switch (mPendingInteraction) {
722                 case PREVIEW:
723                     mScreenshotPreview.callOnClick();
724                     break;
725                 case SHARE:
726                     mShareChip.callOnClick();
727                     break;
728                 case EDIT:
729                     mEditChip.callOnClick();
730                     break;
731                 case QUICK_SHARE:
732                     mQuickShareChip.callOnClick();
733                     break;
734             }
735         } else {
736             LayoutInflater inflater = LayoutInflater.from(mContext);
737 
738             for (Notification.Action smartAction : imageData.smartActions) {
739                 ScreenshotActionChip actionChip = (ScreenshotActionChip) inflater.inflate(
740                         R.layout.global_screenshot_action_chip, mActionsView, false);
741                 actionChip.setText(smartAction.title);
742                 actionChip.setIcon(smartAction.getIcon(), false);
743                 actionChip.setPendingIntent(smartAction.actionIntent,
744                         () -> {
745                             mUiEventLogger.log(ScreenshotEvent.SCREENSHOT_SMART_ACTION_TAPPED);
746                             animateDismissal();
747                         });
748                 actionChip.setAlpha(1);
749                 mActionsView.addView(actionChip);
750                 mSmartChips.add(actionChip);
751             }
752         }
753     }
754 
addQuickShareChip(Notification.Action quickShareAction)755     void addQuickShareChip(Notification.Action quickShareAction) {
756         if (mPendingInteraction == null) {
757             LayoutInflater inflater = LayoutInflater.from(mContext);
758             mQuickShareChip = (ScreenshotActionChip) inflater.inflate(
759                     R.layout.global_screenshot_action_chip, mActionsView, false);
760             mQuickShareChip.setText(quickShareAction.title);
761             mQuickShareChip.setIcon(quickShareAction.getIcon(), false);
762             mQuickShareChip.setOnClickListener(v -> {
763                 mShareChip.setIsPending(false);
764                 mEditChip.setIsPending(false);
765                 mQuickShareChip.setIsPending(true);
766                 mPendingInteraction = PendingInteraction.QUICK_SHARE;
767             });
768             mQuickShareChip.setAlpha(1);
769             mActionsView.addView(mQuickShareChip);
770             mSmartChips.add(mQuickShareChip);
771         }
772     }
773 
scrollableAreaOnScreen(ScrollCaptureResponse response)774     private Rect scrollableAreaOnScreen(ScrollCaptureResponse response) {
775         Rect r = new Rect(response.getBoundsInWindow());
776         Rect windowInScreen = response.getWindowBounds();
777         r.offset(windowInScreen.left, windowInScreen.top);
778         r.intersect(new Rect(0, 0, mDisplayMetrics.widthPixels, mDisplayMetrics.heightPixels));
779         return r;
780     }
781 
startLongScreenshotTransition(Rect destination, Runnable onTransitionEnd, ScrollCaptureController.LongScreenshot longScreenshot)782     void startLongScreenshotTransition(Rect destination, Runnable onTransitionEnd,
783             ScrollCaptureController.LongScreenshot longScreenshot) {
784         AnimatorSet animSet = new AnimatorSet();
785 
786         ValueAnimator scrimAnim = ValueAnimator.ofFloat(0, 1);
787         scrimAnim.addUpdateListener(animation ->
788                 mScrollingScrim.setAlpha(1 - animation.getAnimatedFraction()));
789 
790         if (mShowScrollablePreview) {
791             mScrollablePreview.setImageBitmap(longScreenshot.toBitmap());
792             float startX = mScrollablePreview.getX();
793             float startY = mScrollablePreview.getY();
794             int[] locInScreen = mScrollablePreview.getLocationOnScreen();
795             destination.offset((int) startX - locInScreen[0], (int) startY - locInScreen[1]);
796             mScrollablePreview.setPivotX(0);
797             mScrollablePreview.setPivotY(0);
798             mScrollablePreview.setAlpha(1f);
799             float currentScale = mScrollablePreview.getWidth() / (float) longScreenshot.getWidth();
800             Matrix matrix = new Matrix();
801             matrix.setScale(currentScale, currentScale);
802             matrix.postTranslate(
803                     longScreenshot.getLeft() * currentScale,
804                     longScreenshot.getTop() * currentScale);
805             mScrollablePreview.setImageMatrix(matrix);
806             float destinationScale = destination.width() / (float) mScrollablePreview.getWidth();
807 
808             ValueAnimator previewAnim = ValueAnimator.ofFloat(0, 1);
809             previewAnim.addUpdateListener(animation -> {
810                 float t = animation.getAnimatedFraction();
811                 float currScale = MathUtils.lerp(1, destinationScale, t);
812                 mScrollablePreview.setScaleX(currScale);
813                 mScrollablePreview.setScaleY(currScale);
814                 mScrollablePreview.setX(MathUtils.lerp(startX, destination.left, t));
815                 mScrollablePreview.setY(MathUtils.lerp(startY, destination.top, t));
816             });
817             ValueAnimator previewFadeAnim = ValueAnimator.ofFloat(1, 0);
818             previewFadeAnim.addUpdateListener(animation ->
819                     mScrollablePreview.setAlpha(1 - animation.getAnimatedFraction()));
820             animSet.play(previewAnim).with(scrimAnim).before(previewFadeAnim);
821             previewAnim.addListener(new AnimatorListenerAdapter() {
822                 @Override
823                 public void onAnimationEnd(Animator animation) {
824                     super.onAnimationEnd(animation);
825                     onTransitionEnd.run();
826                 }
827             });
828         } else {
829             // if we switched orientations between the original screenshot and the long screenshot
830             // capture, just fade out the scrim instead of running the preview animation
831             animSet.play(scrimAnim);
832             animSet.addListener(new AnimatorListenerAdapter() {
833                 @Override
834                 public void onAnimationEnd(Animator animation) {
835                     super.onAnimationEnd(animation);
836                     onTransitionEnd.run();
837                 }
838             });
839         }
840         animSet.addListener(new AnimatorListenerAdapter() {
841             @Override
842             public void onAnimationEnd(Animator animation) {
843                 super.onAnimationEnd(animation);
844                         mCallbacks.onDismiss();
845                     }
846         });
847         animSet.start();
848     }
849 
prepareScrollingTransition(ScrollCaptureResponse response, Bitmap screenBitmap, Bitmap newBitmap, boolean screenshotTakenInPortrait)850     void prepareScrollingTransition(ScrollCaptureResponse response, Bitmap screenBitmap,
851             Bitmap newBitmap, boolean screenshotTakenInPortrait) {
852         mShowScrollablePreview = (screenshotTakenInPortrait == mOrientationPortrait);
853 
854         mScrollingScrim.setImageBitmap(newBitmap);
855         mScrollingScrim.setVisibility(View.VISIBLE);
856 
857         if (mShowScrollablePreview) {
858             Rect scrollableArea = scrollableAreaOnScreen(response);
859 
860             float scale = mCornerSizeX
861                     / (mOrientationPortrait ? screenBitmap.getWidth() : screenBitmap.getHeight());
862             ConstraintLayout.LayoutParams params =
863                     (ConstraintLayout.LayoutParams) mScrollablePreview.getLayoutParams();
864 
865             params.width = (int) (scale * scrollableArea.width());
866             params.height = (int) (scale * scrollableArea.height());
867             Matrix matrix = new Matrix();
868             matrix.setScale(scale, scale);
869             matrix.postTranslate(-scrollableArea.left * scale, -scrollableArea.top * scale);
870 
871             mScrollablePreview.setTranslationX(scale
872                     * (mDirectionLTR ? scrollableArea.left : scrollableArea.right - getWidth()));
873             mScrollablePreview.setTranslationY(scale * scrollableArea.top);
874             mScrollablePreview.setImageMatrix(matrix);
875             mScrollablePreview.setImageBitmap(screenBitmap);
876             mScrollablePreview.setVisibility(View.VISIBLE);
877         }
878         mDismissButton.setVisibility(View.GONE);
879         mActionsContainer.setVisibility(View.GONE);
880         mBackgroundProtection.setVisibility(View.GONE);
881         // set these invisible, but not gone, so that the views are laid out correctly
882         mActionsContainerBackground.setVisibility(View.INVISIBLE);
883         mScreenshotPreviewBorder.setVisibility(View.INVISIBLE);
884         mScreenshotPreview.setVisibility(View.INVISIBLE);
885         mScrollingScrim.setImageTintBlendMode(BlendMode.SRC_ATOP);
886         ValueAnimator anim = ValueAnimator.ofFloat(0, .3f);
887         anim.addUpdateListener(animation -> mScrollingScrim.setImageTintList(
888                 ColorStateList.valueOf(Color.argb((float) animation.getAnimatedValue(), 0, 0, 0))));
889         anim.setDuration(200);
890         anim.start();
891     }
892 
restoreNonScrollingUi()893     void restoreNonScrollingUi() {
894         mScrollChip.setVisibility(View.GONE);
895         mScrollablePreview.setVisibility(View.GONE);
896         mScrollingScrim.setVisibility(View.GONE);
897 
898         if (mAccessibilityManager.isEnabled()) {
899             mDismissButton.setVisibility(View.VISIBLE);
900         }
901         mActionsContainer.setVisibility(View.VISIBLE);
902         mBackgroundProtection.setVisibility(View.VISIBLE);
903         mActionsContainerBackground.setVisibility(View.VISIBLE);
904         mScreenshotPreviewBorder.setVisibility(View.VISIBLE);
905         mScreenshotPreview.setVisibility(View.VISIBLE);
906         // reset the timeout
907         mCallbacks.onUserInteraction();
908     }
909 
isDismissing()910     boolean isDismissing() {
911         return (mDismissAnimation != null && mDismissAnimation.isRunning());
912     }
913 
isPendingSharedTransition()914     boolean isPendingSharedTransition() {
915         return mPendingSharedTransition;
916     }
917 
animateDismissal()918     void animateDismissal() {
919         animateDismissal(createScreenshotTranslateDismissAnimation());
920     }
921 
animateDismissal(Animator dismissAnimation)922     private void animateDismissal(Animator dismissAnimation) {
923         mDismissAnimation = dismissAnimation;
924         mDismissAnimation.addListener(new AnimatorListenerAdapter() {
925             private boolean mCancelled = false;
926 
927             @Override
928             public void onAnimationCancel(Animator animation) {
929                 super.onAnimationCancel(animation);
930                 if (DEBUG_ANIM) {
931                     Log.d(TAG, "Cancelled dismiss animation");
932                 }
933                 mCancelled = true;
934             }
935 
936             @Override
937             public void onAnimationEnd(Animator animation) {
938                 super.onAnimationEnd(animation);
939                 if (!mCancelled) {
940                     if (DEBUG_ANIM) {
941                         Log.d(TAG, "after dismiss animation, calling onDismissRunnable.run()");
942                     }
943                     mCallbacks.onDismiss();
944                 }
945             }
946         });
947         if (DEBUG_ANIM) {
948             Log.d(TAG, "Starting dismiss animation");
949         }
950         mDismissAnimation.start();
951     }
952 
reset()953     void reset() {
954         if (DEBUG_UI) {
955             Log.d(TAG, "reset screenshot view");
956         }
957 
958         if (mDismissAnimation != null && mDismissAnimation.isRunning()) {
959             if (DEBUG_ANIM) {
960                 Log.d(TAG, "cancelling dismiss animation");
961             }
962             mDismissAnimation.cancel();
963         }
964         if (DEBUG_WINDOW) {
965             Log.d(TAG, "removing OnComputeInternalInsetsListener");
966         }
967         // Make sure we clean up the view tree observer
968         getViewTreeObserver().removeOnComputeInternalInsetsListener(this);
969         // Clear any references to the bitmap
970         mScreenshotPreview.setImageDrawable(null);
971         mScreenshotPreview.setVisibility(View.INVISIBLE);
972         mScreenshotPreviewBorder.setAlpha(0);
973         mPendingSharedTransition = false;
974         mActionsContainerBackground.setVisibility(View.GONE);
975         mActionsContainer.setVisibility(View.GONE);
976         mBackgroundProtection.setAlpha(0f);
977         mDismissButton.setVisibility(View.GONE);
978         mScrollingScrim.setVisibility(View.GONE);
979         mScrollablePreview.setVisibility(View.GONE);
980         mScreenshotStatic.setTranslationX(0);
981         mScreenshotPreview.setTranslationY(0);
982         mScreenshotPreview.setContentDescription(
983                 mContext.getResources().getString(R.string.screenshot_preview_description));
984         mScreenshotPreview.setOnClickListener(null);
985         mShareChip.setOnClickListener(null);
986         mScrollingScrim.setVisibility(View.GONE);
987         mEditChip.setOnClickListener(null);
988         mShareChip.setIsPending(false);
989         mEditChip.setIsPending(false);
990         mPendingInteraction = null;
991         for (ScreenshotActionChip chip : mSmartChips) {
992             mActionsView.removeView(chip);
993         }
994         mSmartChips.clear();
995         mQuickShareChip = null;
996         setAlpha(1);
997         mDismissButton.setTranslationY(0);
998         mActionsContainer.setTranslationY(0);
999         mActionsContainerBackground.setTranslationY(0);
1000         mScreenshotSelectorView.stop();
1001     }
1002 
startSharedTransition(ActionTransition transition)1003     private void startSharedTransition(ActionTransition transition) {
1004         try {
1005             mPendingSharedTransition = true;
1006             transition.action.actionIntent.send();
1007 
1008             // fade out non-preview UI
1009             createScreenshotFadeDismissAnimation().start();
1010         } catch (PendingIntent.CanceledException e) {
1011             mPendingSharedTransition = false;
1012             if (transition.onCancelRunnable != null) {
1013                 transition.onCancelRunnable.run();
1014             }
1015             Log.e(TAG, "Intent cancelled", e);
1016         }
1017     }
1018 
createScreenshotTranslateDismissAnimation()1019     private AnimatorSet createScreenshotTranslateDismissAnimation() {
1020         ValueAnimator alphaAnim = ValueAnimator.ofFloat(0, 1);
1021         alphaAnim.setStartDelay(SCREENSHOT_DISMISS_ALPHA_OFFSET_MS);
1022         alphaAnim.setDuration(SCREENSHOT_DISMISS_ALPHA_DURATION_MS);
1023         alphaAnim.addUpdateListener(animation -> {
1024             setAlpha(1 - animation.getAnimatedFraction());
1025         });
1026 
1027         ValueAnimator yAnim = ValueAnimator.ofFloat(0, 1);
1028         yAnim.setInterpolator(mAccelerateInterpolator);
1029         yAnim.setDuration(SCREENSHOT_DISMISS_Y_DURATION_MS);
1030         float screenshotStartY = mScreenshotPreview.getTranslationY();
1031         float dismissStartY = mDismissButton.getTranslationY();
1032         yAnim.addUpdateListener(animation -> {
1033             float yDelta = MathUtils.lerp(0, mDismissDeltaY, animation.getAnimatedFraction());
1034             mScreenshotPreview.setTranslationY(screenshotStartY + yDelta);
1035             mScreenshotPreviewBorder.setTranslationY(screenshotStartY + yDelta);
1036             mDismissButton.setTranslationY(dismissStartY + yDelta);
1037             mActionsContainer.setTranslationY(yDelta);
1038             mActionsContainerBackground.setTranslationY(yDelta);
1039         });
1040 
1041         AnimatorSet animSet = new AnimatorSet();
1042         animSet.play(yAnim).with(alphaAnim);
1043 
1044         return animSet;
1045     }
1046 
createScreenshotFadeDismissAnimation()1047     ValueAnimator createScreenshotFadeDismissAnimation() {
1048         ValueAnimator alphaAnim = ValueAnimator.ofFloat(0, 1);
1049         alphaAnim.addUpdateListener(animation -> {
1050             float alpha = 1 - animation.getAnimatedFraction();
1051             mDismissButton.setAlpha(alpha);
1052             mActionsContainerBackground.setAlpha(alpha);
1053             mActionsContainer.setAlpha(alpha);
1054             mBackgroundProtection.setAlpha(alpha);
1055             mScreenshotPreviewBorder.setAlpha(alpha);
1056         });
1057         alphaAnim.setDuration(600);
1058         return alphaAnim;
1059     }
1060 
1061     /**
1062      * Create a drawable using the size of the bitmap and insets as the fractional inset parameters.
1063      */
createScreenDrawable(Resources res, Bitmap bitmap, Insets insets)1064     private static Drawable createScreenDrawable(Resources res, Bitmap bitmap, Insets insets) {
1065         int insettedWidth = bitmap.getWidth() - insets.left - insets.right;
1066         int insettedHeight = bitmap.getHeight() - insets.top - insets.bottom;
1067 
1068         BitmapDrawable bitmapDrawable = new BitmapDrawable(res, bitmap);
1069         if (insettedHeight == 0 || insettedWidth == 0 || bitmap.getWidth() == 0
1070                 || bitmap.getHeight() == 0) {
1071             Log.e(TAG, "Can't create inset drawable, using 0 insets bitmap and insets create "
1072                     + "degenerate region: " + bitmap.getWidth() + "x" + bitmap.getHeight() + " "
1073                     + bitmapDrawable);
1074             return bitmapDrawable;
1075         }
1076 
1077         InsetDrawable insetDrawable = new InsetDrawable(bitmapDrawable,
1078                 -1f * insets.left / insettedWidth,
1079                 -1f * insets.top / insettedHeight,
1080                 -1f * insets.right / insettedWidth,
1081                 -1f * insets.bottom / insettedHeight);
1082 
1083         if (insets.left < 0 || insets.top < 0 || insets.right < 0 || insets.bottom < 0) {
1084             // Are any of the insets negative, meaning the bitmap is smaller than the bounds so need
1085             // to fill in the background of the drawable.
1086             return new LayerDrawable(new Drawable[]{
1087                     new ColorDrawable(Color.BLACK), insetDrawable});
1088         } else {
1089             return insetDrawable;
1090         }
1091     }
1092 
dpToPx(float dp)1093     private float dpToPx(float dp) {
1094         return dp * mDisplayMetrics.densityDpi / (float) DisplayMetrics.DENSITY_DEFAULT;
1095     }
1096 
1097     class SwipeDismissHandler implements OnTouchListener {
1098         // distance needed to register a dismissal
1099         private static final float DISMISS_DISTANCE_THRESHOLD_DP = 20;
1100 
1101         private final GestureDetector mGestureDetector;
1102 
1103         private float mStartX;
1104         // Keeps track of the most recent direction (between the last two move events).
1105         // -1 for left; +1 for right.
1106         private int mDirectionX;
1107         private float mPreviousX;
1108 
SwipeDismissHandler()1109         SwipeDismissHandler() {
1110             GestureDetector.OnGestureListener gestureListener = new SwipeDismissGestureListener();
1111             mGestureDetector = new GestureDetector(mContext, gestureListener);
1112         }
1113 
1114         @Override
onTouch(View view, MotionEvent event)1115         public boolean onTouch(View view, MotionEvent event) {
1116             boolean gestureResult = mGestureDetector.onTouchEvent(event);
1117             mCallbacks.onUserInteraction();
1118             if (event.getActionMasked() == MotionEvent.ACTION_DOWN) {
1119                 mStartX = event.getRawX();
1120                 mPreviousX = mStartX;
1121                 return true;
1122             } else if (event.getActionMasked() == MotionEvent.ACTION_UP) {
1123                 if (isPastDismissThreshold()
1124                         && (mDismissAnimation == null || !mDismissAnimation.isRunning())) {
1125                     if (DEBUG_INPUT) {
1126                         Log.d(TAG, "dismiss triggered via swipe gesture");
1127                     }
1128                     mUiEventLogger.log(ScreenshotEvent.SCREENSHOT_SWIPE_DISMISSED);
1129                     animateDismissal(createSwipeDismissAnimation());
1130                 } else {
1131                     // if we've moved, but not past the threshold, start the return animation
1132                     if (DEBUG_DISMISS) {
1133                         Log.d(TAG, "swipe gesture abandoned");
1134                     }
1135                     if ((mDismissAnimation == null || !mDismissAnimation.isRunning())) {
1136                         createSwipeReturnAnimation().start();
1137                     }
1138                 }
1139                 return true;
1140             }
1141             return gestureResult;
1142         }
1143 
1144         class SwipeDismissGestureListener extends GestureDetector.SimpleOnGestureListener {
1145             @Override
onScroll( MotionEvent ev1, MotionEvent ev2, float distanceX, float distanceY)1146             public boolean onScroll(
1147                     MotionEvent ev1, MotionEvent ev2, float distanceX, float distanceY) {
1148                 mScreenshotStatic.setTranslationX(ev2.getRawX() - mStartX);
1149                 mDirectionX = (ev2.getRawX() < mPreviousX) ? -1 : 1;
1150                 mPreviousX = ev2.getRawX();
1151                 return true;
1152             }
1153 
1154             @Override
onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY)1155             public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX,
1156                     float velocityY) {
1157                 if (mScreenshotStatic.getTranslationX() * velocityX > 0
1158                         && (mDismissAnimation == null || !mDismissAnimation.isRunning())) {
1159                     animateDismissal(createSwipeDismissAnimation(velocityX / (float) 1000));
1160                     return true;
1161                 }
1162                 return false;
1163             }
1164         }
1165 
isPastDismissThreshold()1166         private boolean isPastDismissThreshold() {
1167             float translationX = mScreenshotStatic.getTranslationX();
1168             // Determines whether the absolute translation from the start is in the same direction
1169             // as the current movement. For example, if the user moves most of the way to the right,
1170             // but then starts dragging back left, we do not dismiss even though the absolute
1171             // distance is greater than the threshold.
1172             if (translationX * mDirectionX > 0) {
1173                 return Math.abs(translationX) >= dpToPx(DISMISS_DISTANCE_THRESHOLD_DP);
1174             }
1175             return false;
1176         }
1177 
createSwipeDismissAnimation()1178         private ValueAnimator createSwipeDismissAnimation() {
1179             return createSwipeDismissAnimation(1);
1180         }
1181 
createSwipeDismissAnimation(float velocity)1182         private ValueAnimator createSwipeDismissAnimation(float velocity) {
1183             // velocity is measured in pixels per millisecond
1184             velocity = Math.min(3, Math.max(1, velocity));
1185             ValueAnimator anim = ValueAnimator.ofFloat(0, 1);
1186             float startX = mScreenshotStatic.getTranslationX();
1187             // make sure the UI gets all the way off the screen in the direction of movement
1188             // (the actions container background is guaranteed to be both the leftmost and
1189             // rightmost UI element in LTR and RTL)
1190             float finalX = startX < 0
1191                     ? -1 * mActionsContainerBackground.getRight()
1192                     : mDisplayMetrics.widthPixels;
1193             float distance = Math.abs(finalX - startX);
1194 
1195             anim.addUpdateListener(animation -> {
1196                 float translation = MathUtils.lerp(startX, finalX, animation.getAnimatedFraction());
1197                 mScreenshotStatic.setTranslationX(translation);
1198                 setAlpha(1 - animation.getAnimatedFraction());
1199             });
1200             anim.setDuration((long) (distance / Math.abs(velocity)));
1201             return anim;
1202         }
1203 
createSwipeReturnAnimation()1204         private ValueAnimator createSwipeReturnAnimation() {
1205             ValueAnimator anim = ValueAnimator.ofFloat(0, 1);
1206             float startX = mScreenshotStatic.getTranslationX();
1207             float finalX = 0;
1208 
1209             anim.addUpdateListener(animation -> {
1210                 float translation = MathUtils.lerp(
1211                         startX, finalX, animation.getAnimatedFraction());
1212                 mScreenshotStatic.setTranslationX(translation);
1213             });
1214 
1215             return anim;
1216         }
1217     }
1218 }
1219