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