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