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
17package com.android.systemui;
18
19import android.animation.Animator;
20import android.animation.AnimatorListenerAdapter;
21import android.animation.ObjectAnimator;
22import android.animation.ValueAnimator;
23import android.animation.ValueAnimator.AnimatorUpdateListener;
24import android.content.Context;
25import android.graphics.RectF;
26import android.os.Handler;
27import android.util.Log;
28import android.view.MotionEvent;
29import android.view.VelocityTracker;
30import android.view.View;
31import android.view.ViewConfiguration;
32import android.view.accessibility.AccessibilityEvent;
33
34import com.android.systemui.classifier.FalsingManager;
35import com.android.systemui.plugins.statusbar.NotificationMenuRowPlugin;
36import com.android.systemui.plugins.statusbar.NotificationMenuRowPlugin.MenuItem;
37import com.android.systemui.statusbar.ExpandableNotificationRow;
38import com.android.systemui.statusbar.FlingAnimationUtils;
39
40import java.util.HashMap;
41
42public 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
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
114    public void setLongPressListener(LongPressListener listener) {
115        mLongPressListener = listener;
116    }
117
118    public void setDensityScale(float densityScale) {
119        mDensityScale = densityScale;
120    }
121
122    public void setPagingTouchSlop(float pagingTouchSlop) {
123        mPagingTouchSlop = pagingTouchSlop;
124    }
125
126    public void setDisableHardwareLayers(boolean disableHwLayers) {
127        mDisableHwLayers = disableHwLayers;
128    }
129
130    private float getPos(MotionEvent ev) {
131        return mSwipeDirection == X ? ev.getX() : ev.getY();
132    }
133
134    private float getPerpendicularPos(MotionEvent ev) {
135        return mSwipeDirection == X ? ev.getY() : ev.getX();
136    }
137
138    protected float getTranslation(View v) {
139        return mSwipeDirection == X ? v.getTranslationX() : v.getTranslationY();
140    }
141
142    private float getVelocity(VelocityTracker vt) {
143        return mSwipeDirection == X ? vt.getXVelocity() :
144                vt.getYVelocity();
145    }
146
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
153    private float getPerpendicularVelocity(VelocityTracker vt) {
154        return mSwipeDirection == X ? vt.getYVelocity() :
155                vt.getXVelocity();
156    }
157
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
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
178    protected float getSize(View v) {
179        return mSwipeDirection == X ? v.getMeasuredWidth() :
180                v.getMeasuredHeight();
181    }
182
183    public void setMinSwipeProgress(float minSwipeProgress) {
184        mMinSwipeProgress = minSwipeProgress;
185    }
186
187    public void setMaxSwipeProgress(float maxSwipeProgress) {
188        mMaxSwipeProgress = maxSwipeProgress;
189    }
190
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
197    private float getSwipeAlpha(float progress) {
198        return Math.min(0, Math.max(1, progress / SWIPE_PROGRESS_FADE_END));
199    }
200
201    private void updateSwipeProgressFromOffset(View animView, boolean dismissable) {
202        updateSwipeProgressFromOffset(animView, dismissable, getTranslation(animView));
203    }
204
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
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
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
253    public void removeLongPressCallback() {
254        if (mWatchLongPress != null) {
255            mHandler.removeCallbacks(mWatchLongPress);
256            mWatchLongPress = null;
257        }
258    }
259
260    @Override
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     */
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     */
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     */
447    protected void prepareDismissAnimation(View view, Animator anim) {
448        // Do nothing
449    }
450
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     */
491    protected void prepareSnapBackAnimation(View view, Animator anim) {
492        // Do nothing
493    }
494
495    /**
496     * Called when there's a down event.
497     */
498    public void onDownUpdate(View currView, MotionEvent ev) {
499        // Do nothing
500    }
501
502    /**
503     * Called on a move event.
504     */
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     */
513    public void onTranslationUpdate(View animView, float value, boolean canBeDismissed) {
514        updateSwipeProgressFromOffset(animView, canBeDismissed, value);
515    }
516
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     */
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
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
628    private int getFalsingThreshold() {
629        float factor = mCallback.getFalsingThresholdFactor();
630        return (int) (mFalsingThreshold * factor);
631    }
632
633    private float getMaxVelocity() {
634        return MAX_DISMISS_VELOCITY * mDensityScale;
635    }
636
637    protected float getEscapeVelocity() {
638        return getUnscaledEscapeVelocity() * mDensityScale;
639    }
640
641    protected float getUnscaledEscapeVelocity() {
642        return SWIPE_ESCAPE_VELOCITY;
643    }
644
645    protected long getMaxEscapeAnimDuration() {
646        return MAX_ESCAPE_ANIMATION_DURATION;
647    }
648
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
655    public boolean isDismissGesture(MotionEvent ev) {
656        return ev.getActionMasked() == MotionEvent.ACTION_UP
657                && !isFalseGesture(ev) && (swipedFastEnough() || swipedFarEnough())
658                && mCallback.canChildBeDismissed(mCurrView);
659    }
660
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
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
679    protected boolean handleUpEvent(MotionEvent ev, View animView, float velocity,
680            float translation) {
681        return false;
682    }
683
684    public interface Callback {
685        View getChildAtPosition(MotionEvent ev);
686
687        boolean canChildBeDismissed(View v);
688
689        boolean isAntiFalsingNeeded();
690
691        void onBeginDrag(View v);
692
693        void onChildDismissed(View v);
694
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         */
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         */
710        boolean updateSwipeProgress(View animView, boolean dismissable, float swipeProgress);
711
712        /**
713         * @return The factor the falsing threshold should be multiplied with
714         */
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         */
726        boolean onLongPress(View v, int x, int y, MenuItem item);
727    }
728}
729