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