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