Home | History | Annotate | Download | only in systemui
      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 
     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