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