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