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