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