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