1 /* 2 * Copyright (C) 2011 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; 18 19 import static androidx.dynamicanimation.animation.DynamicAnimation.TRANSLATION_X; 20 import static androidx.dynamicanimation.animation.FloatPropertyCompat.createFloatPropertyCompat; 21 22 import static com.android.systemui.classifier.Classifier.NOTIFICATION_DISMISS; 23 import static com.android.systemui.statusbar.notification.NotificationUtils.logKey; 24 25 import android.animation.Animator; 26 import android.animation.AnimatorListenerAdapter; 27 import android.animation.ObjectAnimator; 28 import android.animation.ValueAnimator; 29 import android.animation.ValueAnimator.AnimatorUpdateListener; 30 import android.annotation.NonNull; 31 import android.annotation.Nullable; 32 import android.app.Notification; 33 import android.app.PendingIntent; 34 import android.content.res.Resources; 35 import android.graphics.RectF; 36 import android.os.Handler; 37 import android.os.Trace; 38 import android.util.ArrayMap; 39 import android.util.Log; 40 import android.view.MotionEvent; 41 import android.view.VelocityTracker; 42 import android.view.View; 43 import android.view.ViewConfiguration; 44 import android.view.accessibility.AccessibilityEvent; 45 46 import androidx.annotation.VisibleForTesting; 47 48 import com.android.app.animation.Interpolators; 49 import com.android.internal.dynamicanimation.animation.SpringForce; 50 import com.android.systemui.flags.FeatureFlags; 51 import com.android.systemui.flags.Flags; 52 import com.android.systemui.plugins.FalsingManager; 53 import com.android.systemui.plugins.statusbar.NotificationMenuRowPlugin; 54 import com.android.systemui.res.R; 55 import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow; 56 import com.android.wm.shell.animation.FlingAnimationUtils; 57 import com.android.wm.shell.shared.animation.PhysicsAnimator; 58 import com.android.wm.shell.shared.animation.PhysicsAnimator.SpringConfig; 59 60 import java.io.PrintWriter; 61 import java.util.function.Consumer; 62 63 public class SwipeHelper implements Gefingerpoken, Dumpable { 64 static final String TAG = "com.android.systemui.SwipeHelper"; 65 private static final boolean DEBUG_INVALIDATE = false; 66 private static final boolean CONSTRAIN_SWIPE = true; 67 private static final boolean FADE_OUT_DURING_SWIPE = true; 68 private static final boolean DISMISS_IF_SWIPED_FAR_ENOUGH = true; 69 70 public static final int X = 0; 71 public static final int Y = 1; 72 73 private static final float SWIPE_ESCAPE_VELOCITY = 500f; // dp/sec 74 private static final int DEFAULT_ESCAPE_ANIMATION_DURATION = 200; // ms 75 private static final int MAX_ESCAPE_ANIMATION_DURATION = 400; // ms 76 private static final int MAX_DISMISS_VELOCITY = 4000; // dp/sec 77 78 public static final float SWIPE_PROGRESS_FADE_END = 0.6f; // fraction of thumbnail width 79 // beyond which swipe progress->0 80 public static final float SWIPED_FAR_ENOUGH_SIZE_FRACTION = 0.6f; 81 static final float MAX_SCROLL_SIZE_FRACTION = 0.3f; 82 83 protected final Handler mHandler; 84 85 private final SpringConfig mSnapBackSpringConfig = 86 new SpringConfig(SpringForce.STIFFNESS_LOW, SpringForce.DAMPING_RATIO_LOW_BOUNCY); 87 88 private final FlingAnimationUtils mFlingAnimationUtils; 89 private float mPagingTouchSlop; 90 private final float mSlopMultiplier; 91 private int mTouchSlop; 92 private float mTouchSlopMultiplier; 93 94 private final Callback mCallback; 95 private final VelocityTracker mVelocityTracker; 96 private final FalsingManager mFalsingManager; 97 private final FeatureFlags mFeatureFlags; 98 99 private float mInitialTouchPos; 100 private float mPerpendicularInitialTouchPos; 101 private boolean mIsSwiping; 102 private boolean mSnappingChild; 103 private View mTouchedView; 104 private boolean mCanCurrViewBeDimissed; 105 private float mDensityScale; 106 private float mTranslation = 0; 107 108 private boolean mMenuRowIntercepting; 109 private final long mLongPressTimeout; 110 private boolean mLongPressSent; 111 private final float[] mDownLocation = new float[2]; 112 private final Runnable mPerformLongPress = new Runnable() { 113 114 private final int[] mViewOffset = new int[2]; 115 116 @Override 117 public void run() { 118 if (mTouchedView != null && !mLongPressSent) { 119 mLongPressSent = true; 120 if (mTouchedView instanceof ExpandableNotificationRow) { 121 mTouchedView.getLocationOnScreen(mViewOffset); 122 final int x = (int) mDownLocation[0] - mViewOffset[0]; 123 final int y = (int) mDownLocation[1] - mViewOffset[1]; 124 mTouchedView.sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_LONG_CLICKED); 125 ((ExpandableNotificationRow) mTouchedView).doLongClickCallback(x, y); 126 127 if (isAvailableToDragAndDrop(mTouchedView)) { 128 mCallback.onLongPressSent(mTouchedView); 129 } 130 } 131 } 132 } 133 }; 134 135 private final int mFalsingThreshold; 136 private boolean mTouchAboveFalsingThreshold; 137 private boolean mDisableHwLayers; 138 private final boolean mFadeDependingOnAmountSwiped; 139 140 private final ArrayMap<View, Animator> mDismissPendingMap = new ArrayMap<>(); 141 SwipeHelper( Callback callback, Resources resources, ViewConfiguration viewConfiguration, FalsingManager falsingManager, FeatureFlags featureFlags)142 public SwipeHelper( 143 Callback callback, Resources resources, ViewConfiguration viewConfiguration, 144 FalsingManager falsingManager, FeatureFlags featureFlags) { 145 mCallback = callback; 146 mHandler = new Handler(); 147 mVelocityTracker = VelocityTracker.obtain(); 148 mPagingTouchSlop = viewConfiguration.getScaledPagingTouchSlop(); 149 mSlopMultiplier = viewConfiguration.getScaledAmbiguousGestureMultiplier(); 150 mTouchSlop = viewConfiguration.getScaledTouchSlop(); 151 mTouchSlopMultiplier = viewConfiguration.getAmbiguousGestureMultiplier(); 152 153 // Extra long-press! 154 mLongPressTimeout = (long) (ViewConfiguration.getLongPressTimeout() * 1.5f); 155 156 mDensityScale = resources.getDisplayMetrics().density; 157 mFalsingThreshold = resources.getDimensionPixelSize(R.dimen.swipe_helper_falsing_threshold); 158 mFadeDependingOnAmountSwiped = resources.getBoolean( 159 R.bool.config_fadeDependingOnAmountSwiped); 160 mFalsingManager = falsingManager; 161 mFeatureFlags = featureFlags; 162 mFlingAnimationUtils = new FlingAnimationUtils(resources.getDisplayMetrics(), 163 getMaxEscapeAnimDuration() / 1000f); 164 } 165 setDensityScale(float densityScale)166 public void setDensityScale(float densityScale) { 167 mDensityScale = densityScale; 168 } 169 setPagingTouchSlop(float pagingTouchSlop)170 public void setPagingTouchSlop(float pagingTouchSlop) { 171 mPagingTouchSlop = pagingTouchSlop; 172 } 173 setDisableHardwareLayers(boolean disableHwLayers)174 public void setDisableHardwareLayers(boolean disableHwLayers) { 175 mDisableHwLayers = disableHwLayers; 176 } 177 getPos(MotionEvent ev)178 private float getPos(MotionEvent ev) { 179 return ev.getX(); 180 } 181 getPerpendicularPos(MotionEvent ev)182 private float getPerpendicularPos(MotionEvent ev) { 183 return ev.getY(); 184 } 185 getTranslation(View v)186 protected float getTranslation(View v) { 187 return v.getTranslationX(); 188 } 189 getVelocity(VelocityTracker vt)190 private float getVelocity(VelocityTracker vt) { 191 return vt.getXVelocity(); 192 } 193 194 getViewTranslationAnimator(View view, float target, AnimatorUpdateListener listener)195 protected Animator getViewTranslationAnimator(View view, float target, 196 AnimatorUpdateListener listener) { 197 198 cancelSnapbackAnimation(view); 199 200 if (view instanceof ExpandableNotificationRow) { 201 return ((ExpandableNotificationRow) view).getTranslateViewAnimator(target, listener); 202 } 203 204 return createTranslationAnimation(view, target, listener); 205 } 206 createTranslationAnimation(View view, float newPos, AnimatorUpdateListener listener)207 protected Animator createTranslationAnimation(View view, float newPos, 208 AnimatorUpdateListener listener) { 209 ObjectAnimator anim = ObjectAnimator.ofFloat(view, View.TRANSLATION_X, newPos); 210 211 if (listener != null) { 212 anim.addUpdateListener(listener); 213 } 214 215 return anim; 216 } 217 setTranslation(View v, float translate)218 protected void setTranslation(View v, float translate) { 219 if (v != null) { 220 v.setTranslationX(translate); 221 } 222 } 223 getSize(View v)224 protected float getSize(View v) { 225 return v.getMeasuredWidth(); 226 } 227 getSwipeProgressForOffset(View view, float translation)228 private float getSwipeProgressForOffset(View view, float translation) { 229 if (translation == 0) return 0; 230 float viewSize = getSize(view); 231 float result = Math.abs(translation / viewSize); 232 return Math.min(Math.max(0, result), 1); 233 } 234 235 /** 236 * Returns the alpha value depending on the progress of the swipe. 237 */ 238 @VisibleForTesting getSwipeAlpha(float progress)239 public float getSwipeAlpha(float progress) { 240 if (mFadeDependingOnAmountSwiped) { 241 // The more progress has been fade, the lower the alpha value so that the view fades. 242 return Math.max(1 - progress, 0); 243 } 244 245 return 1f - Math.max(0, Math.min(1, progress / SWIPE_PROGRESS_FADE_END)); 246 } 247 updateSwipeProgressFromOffset(View animView, boolean dismissable)248 private void updateSwipeProgressFromOffset(View animView, boolean dismissable) { 249 updateSwipeProgressFromOffset(animView, dismissable, getTranslation(animView)); 250 } 251 updateSwipeProgressFromOffset(View animView, boolean dismissable, float translation)252 private void updateSwipeProgressFromOffset(View animView, boolean dismissable, 253 float translation) { 254 float swipeProgress = getSwipeProgressForOffset(animView, translation); 255 if (!mCallback.updateSwipeProgress(animView, dismissable, swipeProgress)) { 256 if (FADE_OUT_DURING_SWIPE && dismissable) { 257 if (!mDisableHwLayers) { 258 if (swipeProgress != 0f && swipeProgress != 1f) { 259 animView.setLayerType(View.LAYER_TYPE_HARDWARE, null); 260 } else { 261 animView.setLayerType(View.LAYER_TYPE_NONE, null); 262 } 263 } 264 updateSwipeProgressAlpha(animView, getSwipeAlpha(swipeProgress)); 265 } 266 } 267 invalidateGlobalRegion(animView); 268 } 269 270 // invalidate the view's own bounds all the way up the view hierarchy invalidateGlobalRegion(View view)271 public static void invalidateGlobalRegion(View view) { 272 Trace.beginSection("SwipeHelper.invalidateGlobalRegion"); 273 invalidateGlobalRegion( 274 view, 275 new RectF(view.getLeft(), view.getTop(), view.getRight(), view.getBottom())); 276 Trace.endSection(); 277 } 278 279 // invalidate a rectangle relative to the view's coordinate system all the way up the view 280 // hierarchy invalidateGlobalRegion(View view, RectF childBounds)281 public static void invalidateGlobalRegion(View view, RectF childBounds) { 282 //childBounds.offset(view.getTranslationX(), view.getTranslationY()); 283 if (DEBUG_INVALIDATE) 284 Log.v(TAG, "-------------"); 285 while (view.getParent() != null && view.getParent() instanceof View) { 286 view = (View) view.getParent(); 287 view.getMatrix().mapRect(childBounds); 288 view.invalidate((int) Math.floor(childBounds.left), 289 (int) Math.floor(childBounds.top), 290 (int) Math.ceil(childBounds.right), 291 (int) Math.ceil(childBounds.bottom)); 292 if (DEBUG_INVALIDATE) { 293 Log.v(TAG, "INVALIDATE(" + (int) Math.floor(childBounds.left) 294 + "," + (int) Math.floor(childBounds.top) 295 + "," + (int) Math.ceil(childBounds.right) 296 + "," + (int) Math.ceil(childBounds.bottom)); 297 } 298 } 299 } 300 cancelLongPress()301 public void cancelLongPress() { 302 mHandler.removeCallbacks(mPerformLongPress); 303 } 304 305 @Override onInterceptTouchEvent(final MotionEvent ev)306 public boolean onInterceptTouchEvent(final MotionEvent ev) { 307 if (mTouchedView instanceof ExpandableNotificationRow) { 308 NotificationMenuRowPlugin nmr = ((ExpandableNotificationRow) mTouchedView).getProvider(); 309 if (nmr != null) { 310 mMenuRowIntercepting = nmr.onInterceptTouchEvent(mTouchedView, ev); 311 } 312 } 313 final int action = ev.getAction(); 314 315 switch (action) { 316 case MotionEvent.ACTION_DOWN: 317 mTouchAboveFalsingThreshold = false; 318 mIsSwiping = false; 319 mSnappingChild = false; 320 mLongPressSent = false; 321 mCallback.onLongPressSent(null); 322 mVelocityTracker.clear(); 323 cancelLongPress(); 324 mTouchedView = mCallback.getChildAtPosition(ev); 325 326 if (mTouchedView != null) { 327 cancelSnapbackAnimation(mTouchedView); 328 onDownUpdate(mTouchedView, ev); 329 mCanCurrViewBeDimissed = mCallback.canChildBeDismissed(mTouchedView); 330 mVelocityTracker.addMovement(ev); 331 mInitialTouchPos = getPos(ev); 332 mPerpendicularInitialTouchPos = getPerpendicularPos(ev); 333 mTranslation = getTranslation(mTouchedView); 334 mDownLocation[0] = ev.getRawX(); 335 mDownLocation[1] = ev.getRawY(); 336 mHandler.postDelayed(mPerformLongPress, mLongPressTimeout); 337 } 338 break; 339 340 case MotionEvent.ACTION_MOVE: 341 if (mTouchedView != null && !mLongPressSent) { 342 mVelocityTracker.addMovement(ev); 343 float pos = getPos(ev); 344 float perpendicularPos = getPerpendicularPos(ev); 345 float delta = pos - mInitialTouchPos; 346 float deltaPerpendicular = perpendicularPos - mPerpendicularInitialTouchPos; 347 // Adjust the touch slop if another gesture may be being performed. 348 final float pagingTouchSlop = 349 ev.getClassification() == MotionEvent.CLASSIFICATION_AMBIGUOUS_GESTURE 350 ? mPagingTouchSlop * mSlopMultiplier 351 : mPagingTouchSlop; 352 if (Math.abs(delta) > pagingTouchSlop 353 && Math.abs(delta) > Math.abs(deltaPerpendicular)) { 354 if (mCallback.canChildBeDragged(mTouchedView)) { 355 mIsSwiping = true; 356 mCallback.onBeginDrag(mTouchedView); 357 mInitialTouchPos = getPos(ev); 358 mTranslation = getTranslation(mTouchedView); 359 } 360 cancelLongPress(); 361 } else if (ev.getClassification() == MotionEvent.CLASSIFICATION_DEEP_PRESS 362 && mHandler.hasCallbacks(mPerformLongPress)) { 363 // Accelerate the long press signal. 364 cancelLongPress(); 365 mPerformLongPress.run(); 366 } 367 } 368 break; 369 370 case MotionEvent.ACTION_UP: 371 case MotionEvent.ACTION_CANCEL: 372 final boolean captured = (mIsSwiping || mLongPressSent || mMenuRowIntercepting); 373 mLongPressSent = false; 374 mCallback.onLongPressSent(null); 375 mMenuRowIntercepting = false; 376 resetSwipeState(); 377 cancelLongPress(); 378 if (captured) return true; 379 break; 380 } 381 return mIsSwiping || mLongPressSent || mMenuRowIntercepting; 382 } 383 384 /** 385 * After dismissChild() and related animation finished, this function will be called. 386 */ onDismissChildWithAnimationFinished()387 protected void onDismissChildWithAnimationFinished() {} 388 389 /** 390 * @param view The view to be dismissed 391 * @param velocity The desired pixels/second speed at which the view should move 392 * @param useAccelerateInterpolator Should an accelerating Interpolator be used 393 */ dismissChild(final View view, float velocity, boolean useAccelerateInterpolator)394 public void dismissChild(final View view, float velocity, boolean useAccelerateInterpolator) { 395 dismissChild(view, velocity, null /* endAction */, 0 /* delay */, 396 useAccelerateInterpolator, 0 /* fixedDuration */, false /* isDismissAll */); 397 } 398 399 /** 400 * @param animView The view to be dismissed 401 * @param velocity The desired pixels/second speed at which the view should move 402 * @param endAction The action to perform at the end 403 * @param delay The delay after which we should start 404 * @param useAccelerateInterpolator Should an accelerating Interpolator be used 405 * @param fixedDuration If not 0, this exact duration will be taken 406 */ dismissChild(final View animView, float velocity, final Consumer<Boolean> endAction, long delay, boolean useAccelerateInterpolator, long fixedDuration, boolean isDismissAll)407 public void dismissChild(final View animView, float velocity, final Consumer<Boolean> endAction, 408 long delay, boolean useAccelerateInterpolator, long fixedDuration, 409 boolean isDismissAll) { 410 final boolean canBeDismissed = mCallback.canChildBeDismissed(animView); 411 float newPos; 412 boolean isLayoutRtl = animView.getLayoutDirection() == View.LAYOUT_DIRECTION_RTL; 413 414 // if the language is rtl we prefer swiping to the left 415 boolean animateLeftForRtl = velocity == 0 && (getTranslation(animView) == 0 || isDismissAll) 416 && isLayoutRtl; 417 boolean animateLeft = (Math.abs(velocity) > getEscapeVelocity() && velocity < 0) || 418 (getTranslation(animView) < 0 && !isDismissAll); 419 if (animateLeft || animateLeftForRtl) { 420 newPos = -getTotalTranslationLength(animView); 421 } else { 422 newPos = getTotalTranslationLength(animView); 423 } 424 long duration; 425 if (fixedDuration == 0) { 426 duration = MAX_ESCAPE_ANIMATION_DURATION; 427 if (velocity != 0) { 428 duration = Math.min(duration, 429 (int) (Math.abs(newPos - getTranslation(animView)) * 1000f / Math 430 .abs(velocity)) 431 ); 432 } else { 433 duration = DEFAULT_ESCAPE_ANIMATION_DURATION; 434 } 435 } else { 436 duration = fixedDuration; 437 } 438 439 if (!mDisableHwLayers) { 440 animView.setLayerType(View.LAYER_TYPE_HARDWARE, null); 441 } 442 AnimatorUpdateListener updateListener = new AnimatorUpdateListener() { 443 @Override 444 public void onAnimationUpdate(ValueAnimator animation) { 445 onTranslationUpdate(animView, (float) animation.getAnimatedValue(), canBeDismissed); 446 } 447 }; 448 449 Animator anim = getViewTranslationAnimator(animView, newPos, updateListener); 450 if (anim == null) { 451 onDismissChildWithAnimationFinished(); 452 return; 453 } 454 if (useAccelerateInterpolator) { 455 anim.setInterpolator(Interpolators.FAST_OUT_LINEAR_IN); 456 anim.setDuration(duration); 457 } else { 458 mFlingAnimationUtils.applyDismissing(anim, getTranslation(animView), 459 newPos, velocity, getSize(animView)); 460 } 461 if (delay > 0) { 462 anim.setStartDelay(delay); 463 } 464 anim.addListener(new AnimatorListenerAdapter() { 465 private boolean mCancelled; 466 467 @Override 468 public void onAnimationStart(Animator animation) { 469 super.onAnimationStart(animation); 470 mCallback.onBeginDrag(animView); 471 } 472 473 @Override 474 public void onAnimationCancel(Animator animation) { 475 mCancelled = true; 476 } 477 478 @Override 479 public void onAnimationEnd(Animator animation) { 480 updateSwipeProgressFromOffset(animView, canBeDismissed); 481 mDismissPendingMap.remove(animView); 482 boolean wasRemoved = false; 483 if (animView instanceof ExpandableNotificationRow row) { 484 // If the view is already removed from its parent and added as Transient, 485 // we need to clean the transient view upon animation end 486 wasRemoved = row.getTransientContainer() != null 487 || row.getParent() == null || row.isRemoved(); 488 } 489 if (!mCancelled || wasRemoved) { 490 mCallback.onChildDismissed(animView); 491 resetViewIfSwiping(animView); 492 } 493 if (endAction != null) { 494 endAction.accept(mCancelled); 495 } 496 if (!mDisableHwLayers) { 497 animView.setLayerType(View.LAYER_TYPE_NONE, null); 498 } 499 onDismissChildWithAnimationFinished(); 500 } 501 }); 502 503 prepareDismissAnimation(animView, anim); 504 mDismissPendingMap.put(animView, anim); 505 anim.start(); 506 } 507 508 /** 509 * Get the total translation length where we want to swipe to when dismissing the view. By 510 * default this is the size of the view, but can also be larger. 511 * @param animView the view to ask about 512 */ getTotalTranslationLength(View animView)513 protected float getTotalTranslationLength(View animView) { 514 return getSize(animView); 515 } 516 517 /** 518 * Called to update the dismiss animation. 519 */ prepareDismissAnimation(View view, Animator anim)520 protected void prepareDismissAnimation(View view, Animator anim) { 521 // Do nothing 522 } 523 524 /** 525 * Starts a snapback animation and cancels any previous translate animations on the given view. 526 * 527 * @param animView view to animate 528 * @param targetLeft the end position of the translation 529 * @param velocity the initial velocity of the animation 530 */ snapChild(final View animView, final float targetLeft, float velocity)531 protected void snapChild(final View animView, final float targetLeft, float velocity) { 532 final boolean canBeDismissed = mCallback.canChildBeDismissed(animView); 533 534 cancelTranslateAnimation(animView); 535 536 PhysicsAnimator<? extends View> anim = 537 createSnapBackAnimation(animView, targetLeft, velocity); 538 anim.addUpdateListener((target, values) -> { 539 onTranslationUpdate(target, getTranslation(target), canBeDismissed); 540 }); 541 anim.addEndListener((t, p, wasFling, cancelled, finalValue, finalVelocity, allEnded) -> { 542 mSnappingChild = false; 543 544 if (!cancelled) { 545 updateSwipeProgressFromOffset(animView, canBeDismissed); 546 resetViewIfSwiping(animView); 547 // Clear the snapped view after success, assuming it's not being swiped now 548 if (animView == mTouchedView && !mIsSwiping) { 549 mTouchedView = null; 550 } 551 } 552 onChildSnappedBack(animView, targetLeft); 553 }); 554 mSnappingChild = true; 555 anim.start(); 556 } 557 createSnapBackAnimation(View target, float toPosition, float startVelocity)558 private PhysicsAnimator<? extends View> createSnapBackAnimation(View target, float toPosition, 559 float startVelocity) { 560 if (target instanceof ExpandableNotificationRow) { 561 return PhysicsAnimator.getInstance((ExpandableNotificationRow) target).spring( 562 createFloatPropertyCompat(ExpandableNotificationRow.TRANSLATE_CONTENT), 563 toPosition, 564 startVelocity, 565 mSnapBackSpringConfig); 566 } 567 return PhysicsAnimator.getInstance(target).spring(TRANSLATION_X, toPosition, startVelocity, 568 mSnapBackSpringConfig); 569 } 570 cancelTranslateAnimation(View animView)571 private void cancelTranslateAnimation(View animView) { 572 if (animView instanceof ExpandableNotificationRow) { 573 ((ExpandableNotificationRow) animView).cancelTranslateAnimation(); 574 } 575 cancelSnapbackAnimation(animView); 576 } 577 cancelSnapbackAnimation(View target)578 private void cancelSnapbackAnimation(View target) { 579 PhysicsAnimator.getInstance(target).cancel(); 580 } 581 582 /** 583 * Called to update the content alpha while the view is swiped 584 */ updateSwipeProgressAlpha(View animView, float alpha)585 protected void updateSwipeProgressAlpha(View animView, float alpha) { 586 animView.setAlpha(alpha); 587 } 588 589 /** 590 * Called after {@link #snapChild(View, float, float)} and its related animation has finished. 591 */ onChildSnappedBack(View animView, float targetLeft)592 protected void onChildSnappedBack(View animView, float targetLeft) { 593 mCallback.onChildSnappedBack(animView, targetLeft); 594 } 595 596 /** 597 * Called when there's a down event. 598 */ onDownUpdate(View currView, MotionEvent ev)599 public void onDownUpdate(View currView, MotionEvent ev) { 600 // Do nothing 601 } 602 603 /** 604 * Called on a move event. 605 */ onMoveUpdate(View view, MotionEvent ev, float totalTranslation, float delta)606 protected void onMoveUpdate(View view, MotionEvent ev, float totalTranslation, float delta) { 607 // Do nothing 608 } 609 610 /** 611 * Called in {@link AnimatorUpdateListener#onAnimationUpdate(ValueAnimator)} when the current 612 * view is being animated to dismiss or snap. 613 */ onTranslationUpdate(View animView, float value, boolean canBeDismissed)614 public void onTranslationUpdate(View animView, float value, boolean canBeDismissed) { 615 updateSwipeProgressFromOffset(animView, canBeDismissed, value); 616 } 617 snapChildInstantly(final View view)618 private void snapChildInstantly(final View view) { 619 final boolean canAnimViewBeDismissed = mCallback.canChildBeDismissed(view); 620 setTranslation(view, 0); 621 updateSwipeProgressFromOffset(view, canAnimViewBeDismissed); 622 } 623 624 /** 625 * Called when a view is updated to be non-dismissable, if the view was being dismissed before 626 * the update this will handle snapping it back into place. 627 * 628 * @param view the view to snap if necessary. 629 * @param animate whether to animate the snap or not. 630 * @param targetLeft the target to snap to. 631 */ snapChildIfNeeded(final View view, boolean animate, float targetLeft)632 public void snapChildIfNeeded(final View view, boolean animate, float targetLeft) { 633 if ((mIsSwiping && mTouchedView == view) || mSnappingChild) { 634 return; 635 } 636 boolean needToSnap = false; 637 Animator dismissPendingAnim = mDismissPendingMap.get(view); 638 if (dismissPendingAnim != null) { 639 needToSnap = true; 640 dismissPendingAnim.cancel(); 641 } else if (getTranslation(view) != 0) { 642 needToSnap = true; 643 } 644 if (needToSnap) { 645 if (animate) { 646 snapChild(view, targetLeft, 0.0f /* velocity */); 647 } else { 648 snapChildInstantly(view); 649 } 650 } 651 } 652 653 @Override onTouchEvent(MotionEvent ev)654 public boolean onTouchEvent(MotionEvent ev) { 655 if (!mIsSwiping && !mMenuRowIntercepting && !mLongPressSent) { 656 if (mCallback.getChildAtPosition(ev) != null) { 657 // We are dragging directly over a card, make sure that we also catch the gesture 658 // even if nobody else wants the touch event. 659 mTouchedView = mCallback.getChildAtPosition(ev); 660 onInterceptTouchEvent(ev); 661 return true; 662 } else { 663 // We are not doing anything, make sure the long press callback 664 // is not still ticking like a bomb waiting to go off. 665 cancelLongPress(); 666 return false; 667 } 668 } 669 670 mVelocityTracker.addMovement(ev); 671 final int action = ev.getAction(); 672 switch (action) { 673 case MotionEvent.ACTION_OUTSIDE: 674 case MotionEvent.ACTION_MOVE: 675 if (mTouchedView != null) { 676 float delta = getPos(ev) - mInitialTouchPos; 677 float absDelta = Math.abs(delta); 678 if (absDelta >= getFalsingThreshold()) { 679 mTouchAboveFalsingThreshold = true; 680 } 681 682 if (mLongPressSent) { 683 if (absDelta >= getTouchSlop(ev)) { 684 if (mTouchedView instanceof ExpandableNotificationRow) { 685 ((ExpandableNotificationRow) mTouchedView) 686 .doDragCallback(ev.getX(), ev.getY()); 687 } 688 } 689 } else { 690 // don't let items that can't be dismissed be dragged more than 691 // maxScrollDistance 692 if (CONSTRAIN_SWIPE && !mCallback.canChildBeDismissedInDirection( 693 mTouchedView, 694 delta > 0)) { 695 float size = getSize(mTouchedView); 696 float maxScrollDistance = MAX_SCROLL_SIZE_FRACTION * size; 697 if (absDelta >= size) { 698 delta = delta > 0 ? maxScrollDistance : -maxScrollDistance; 699 } else { 700 int startPosition = mCallback.getConstrainSwipeStartPosition(); 701 if (absDelta > startPosition) { 702 int signedStartPosition = 703 (int) (startPosition * Math.signum(delta)); 704 delta = signedStartPosition 705 + maxScrollDistance * (float) Math.sin( 706 ((delta - signedStartPosition) / size) * (Math.PI / 2)); 707 } 708 } 709 } 710 711 setTranslation(mTouchedView, mTranslation + delta); 712 updateSwipeProgressFromOffset(mTouchedView, mCanCurrViewBeDimissed); 713 onMoveUpdate(mTouchedView, ev, mTranslation + delta, delta); 714 } 715 } 716 break; 717 case MotionEvent.ACTION_UP: 718 case MotionEvent.ACTION_CANCEL: 719 if (mTouchedView == null) { 720 break; 721 } 722 mVelocityTracker.computeCurrentVelocity(1000 /* px/sec */, getMaxVelocity()); 723 float velocity = getVelocity(mVelocityTracker); 724 725 if (!handleUpEvent(ev, mTouchedView, velocity, getTranslation(mTouchedView))) { 726 if (isDismissGesture(ev)) { 727 dismissChild(mTouchedView, velocity, 728 !swipedFastEnough() /* useAccelerateInterpolator */); 729 } else { 730 mCallback.onDragCancelled(mTouchedView); 731 snapChild(mTouchedView, 0 /* leftTarget */, velocity); 732 } 733 mTouchedView = null; 734 } 735 mIsSwiping = false; 736 break; 737 } 738 return true; 739 } 740 getFalsingThreshold()741 private int getFalsingThreshold() { 742 float factor = mCallback.getFalsingThresholdFactor(); 743 return (int) (mFalsingThreshold * factor); 744 } 745 getMaxVelocity()746 private float getMaxVelocity() { 747 return MAX_DISMISS_VELOCITY * mDensityScale; 748 } 749 getEscapeVelocity()750 protected float getEscapeVelocity() { 751 return getUnscaledEscapeVelocity() * mDensityScale; 752 } 753 getUnscaledEscapeVelocity()754 protected float getUnscaledEscapeVelocity() { 755 return SWIPE_ESCAPE_VELOCITY; 756 } 757 getMaxEscapeAnimDuration()758 protected long getMaxEscapeAnimDuration() { 759 return MAX_ESCAPE_ANIMATION_DURATION; 760 } 761 swipedFarEnough()762 protected boolean swipedFarEnough() { 763 float translation = getTranslation(mTouchedView); 764 return DISMISS_IF_SWIPED_FAR_ENOUGH 765 && Math.abs(translation) > SWIPED_FAR_ENOUGH_SIZE_FRACTION * getSize( 766 mTouchedView); 767 } 768 isDismissGesture(MotionEvent ev)769 public boolean isDismissGesture(MotionEvent ev) { 770 float translation = getTranslation(mTouchedView); 771 return ev.getActionMasked() == MotionEvent.ACTION_UP 772 && !mFalsingManager.isUnlockingDisabled() 773 && !isFalseGesture() && (swipedFastEnough() || swipedFarEnough()) 774 && mCallback.canChildBeDismissedInDirection(mTouchedView, translation > 0); 775 } 776 777 /** Returns true if the gesture should be rejected. */ isFalseGesture()778 public boolean isFalseGesture() { 779 boolean falsingDetected = mCallback.isAntiFalsingNeeded(); 780 if (mFalsingManager.isClassifierEnabled()) { 781 falsingDetected = falsingDetected && mFalsingManager.isFalseTouch(NOTIFICATION_DISMISS); 782 } else { 783 falsingDetected = falsingDetected && !mTouchAboveFalsingThreshold; 784 } 785 return falsingDetected; 786 } 787 swipedFastEnough()788 protected boolean swipedFastEnough() { 789 float velocity = getVelocity(mVelocityTracker); 790 float translation = getTranslation(mTouchedView); 791 boolean ret = (Math.abs(velocity) > getEscapeVelocity()) 792 && (velocity > 0) == (translation > 0); 793 return ret; 794 } 795 handleUpEvent(MotionEvent ev, View animView, float velocity, float translation)796 protected boolean handleUpEvent(MotionEvent ev, View animView, float velocity, 797 float translation) { 798 return false; 799 } 800 isSwiping()801 public boolean isSwiping() { 802 return mIsSwiping; 803 } 804 805 @Nullable getSwipedView()806 public View getSwipedView() { 807 return mIsSwiping ? mTouchedView : null; 808 } 809 resetViewIfSwiping(View view)810 protected void resetViewIfSwiping(View view) { 811 if (getSwipedView() == view) { 812 resetSwipeState(); 813 } 814 } 815 resetSwipeState()816 private void resetSwipeState() { 817 resetSwipeStates(/* resetAll= */ false); 818 } 819 resetTouchState()820 public void resetTouchState() { 821 resetSwipeStates(/* resetAll= */ true); 822 } 823 forceResetSwipeState(@onNull View view)824 public void forceResetSwipeState(@NonNull View view) { 825 if (view.getTranslationX() == 0) return; 826 setTranslation(view, 0); 827 updateSwipeProgressFromOffset(view, /* dismissable= */ true, 0); 828 } 829 830 /** This method resets the swipe state, and if `resetAll` is true, also resets the snap state */ resetSwipeStates(boolean resetAll)831 private void resetSwipeStates(boolean resetAll) { 832 final View touchedView = mTouchedView; 833 final boolean wasSnapping = mSnappingChild; 834 final boolean wasSwiping = mIsSwiping; 835 mTouchedView = null; 836 mIsSwiping = false; 837 // If we were swiping, then we resetting swipe requires resetting everything. 838 resetAll |= wasSwiping; 839 if (resetAll) { 840 mSnappingChild = false; 841 } 842 if (touchedView == null) return; // No view to reset visually 843 // When snap needs to be reset, first thing is to cancel any translation animation 844 final boolean snapNeedsReset = resetAll && wasSnapping; 845 if (snapNeedsReset) { 846 cancelTranslateAnimation(touchedView); 847 } 848 // actually reset the view to default state 849 if (resetAll) { 850 snapChildIfNeeded(touchedView, false, 0); 851 } 852 // report if a swipe or snap was reset. 853 if (wasSwiping || snapNeedsReset) { 854 onChildSnappedBack(touchedView, 0); 855 } 856 } 857 getTouchSlop(MotionEvent event)858 private float getTouchSlop(MotionEvent event) { 859 // Adjust the touch slop if another gesture may be being performed. 860 return event.getClassification() == MotionEvent.CLASSIFICATION_AMBIGUOUS_GESTURE 861 ? mTouchSlop * mTouchSlopMultiplier 862 : mTouchSlop; 863 } 864 isAvailableToDragAndDrop(View v)865 private boolean isAvailableToDragAndDrop(View v) { 866 if (mFeatureFlags.isEnabled(Flags.NOTIFICATION_DRAG_TO_CONTENTS)) { 867 if (v instanceof ExpandableNotificationRow) { 868 ExpandableNotificationRow enr = (ExpandableNotificationRow) v; 869 boolean canBubble = enr.getEntry().canBubble(); 870 Notification notif = enr.getEntry().getSbn().getNotification(); 871 PendingIntent dragIntent = notif.contentIntent != null ? notif.contentIntent 872 : notif.fullScreenIntent; 873 if (dragIntent != null && dragIntent.isActivity() && !canBubble) { 874 return true; 875 } 876 } 877 } 878 return false; 879 } 880 881 @Override dump(@onNull PrintWriter pw, @NonNull String[] args)882 public void dump(@NonNull PrintWriter pw, @NonNull String[] args) { 883 pw.append("mTouchedView=").print(mTouchedView); 884 if (mTouchedView instanceof ExpandableNotificationRow) { 885 pw.append(" key=").println(logKey((ExpandableNotificationRow) mTouchedView)); 886 } else { 887 pw.println(); 888 } 889 pw.append("mIsSwiping=").println(mIsSwiping); 890 pw.append("mSnappingChild=").println(mSnappingChild); 891 pw.append("mLongPressSent=").println(mLongPressSent); 892 pw.append("mInitialTouchPos=").println(mInitialTouchPos); 893 pw.append("mTranslation=").println(mTranslation); 894 pw.append("mCanCurrViewBeDimissed=").println(mCanCurrViewBeDimissed); 895 pw.append("mMenuRowIntercepting=").println(mMenuRowIntercepting); 896 pw.append("mDisableHwLayers=").println(mDisableHwLayers); 897 pw.append("mDismissPendingMap: ").println(mDismissPendingMap.size()); 898 if (!mDismissPendingMap.isEmpty()) { 899 mDismissPendingMap.forEach((view, animator) -> { 900 pw.append(" ").print(view); 901 pw.append(": ").println(animator); 902 }); 903 } 904 } 905 906 public interface Callback { getChildAtPosition(MotionEvent ev)907 View getChildAtPosition(MotionEvent ev); 908 canChildBeDismissed(View v)909 boolean canChildBeDismissed(View v); 910 911 /** 912 * Returns true if the provided child can be dismissed by a swipe in the given direction. 913 * 914 * @param isRightOrDown {@code true} if the swipe direction is right or down, 915 * {@code false} if it is left or up. 916 */ canChildBeDismissedInDirection(View v, boolean isRightOrDown)917 default boolean canChildBeDismissedInDirection(View v, boolean isRightOrDown) { 918 return canChildBeDismissed(v); 919 } 920 isAntiFalsingNeeded()921 boolean isAntiFalsingNeeded(); 922 onBeginDrag(View v)923 void onBeginDrag(View v); 924 onChildDismissed(View v)925 void onChildDismissed(View v); 926 onDragCancelled(View v)927 void onDragCancelled(View v); 928 929 /** 930 * Called when the child is long pressed and available to start drag and drop. 931 * 932 * @param v the view that was long pressed. 933 */ onLongPressSent(View v)934 void onLongPressSent(View v); 935 936 /** 937 * Called when the child is snapped to a position. 938 * 939 * @param animView the view that was snapped. 940 * @param targetLeft the left position the view was snapped to. 941 */ onChildSnappedBack(View animView, float targetLeft)942 void onChildSnappedBack(View animView, float targetLeft); 943 944 /** 945 * Updates the swipe progress on a child. 946 * 947 * @return if true, prevents the default alpha fading. 948 */ updateSwipeProgress(View animView, boolean dismissable, float swipeProgress)949 boolean updateSwipeProgress(View animView, boolean dismissable, float swipeProgress); 950 951 /** 952 * @return The factor the falsing threshold should be multiplied with 953 */ getFalsingThresholdFactor()954 float getFalsingThresholdFactor(); 955 956 /** 957 * @return The position, in pixels, at which a constrained swipe should start being 958 * constrained. 959 */ getConstrainSwipeStartPosition()960 default int getConstrainSwipeStartPosition() { 961 return 0; 962 } 963 964 /** 965 * @return If true, the given view is draggable. 966 */ canChildBeDragged(@onNull View animView)967 default boolean canChildBeDragged(@NonNull View animView) { return true; } 968 } 969 } 970