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