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