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