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