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