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