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