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