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