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