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