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