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