• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright 2018 Google Inc.
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.car.notification;
18 
19 import android.animation.Animator;
20 import android.animation.AnimatorListenerAdapter;
21 import android.content.res.Resources;
22 import android.view.MotionEvent;
23 import android.view.VelocityTracker;
24 import android.view.View;
25 import android.view.ViewConfiguration;
26 import android.view.ViewPropertyAnimator;
27 import android.view.ViewTreeObserver;
28 
29 import com.android.internal.annotations.VisibleForTesting;
30 
31 import java.util.concurrent.TimeUnit;
32 
33 /**
34  * OnTouchListener that enables swipe-to-dismiss gesture on heads-up notifications.
35  */
36 class HeadsUpNotificationOnTouchListener implements View.OnTouchListener {
37     // todo(b/301474982): converge common logic in this and CarNotificationItemTouchListener class.
38     private static final int INITIAL_TRANSLATION_X = 0;
39     private static final int INITIAL_TRANSLATION_Y = 0;
40     private static final float MAXIMUM_ALPHA = 1f;
41     private static final float MINIMUM_ALPHA = 0f;
42     /**
43      * Factor by which view's alpha decreases based on the translation in the direction of dismiss.
44      * Example: If set to 1f, the view will be invisible when it has translated the maximum possible
45      * translation, similarly for 2f, view will be invisible halfway.
46      */
47     private static final float ALPHA_FADE_FACTOR_MULTIPLIER = 2f;
48 
49     /**
50      * The unit of velocity in milliseconds. A value of 1 means "pixels per millisecond",
51      * 1000 means "pixels per 1000 milliseconds (1 second)".
52      */
53     private static final int PIXELS_PER_SECOND = (int) TimeUnit.SECONDS.toMillis(1);
54     private final View mView;
55     private final DismissCallbacks mCallbacks;
56     private final Axis mDismissAxis;
57     /**
58      * Distance a touch can wander before we think the user is scrolling in pixels.
59      */
60     private final int mTouchSlop;
61     private final boolean mDismissOnSwipe;
62     /**
63      * The proportion which view has to be swiped before it dismisses.
64      */
65     private final float mPercentageOfMaxTransaltionToDismiss;
66     /**
67      * The minimum velocity in pixel per second the swipe gesture to initiate a dismiss action.
68      */
69     private final int mMinimumFlingVelocity;
70     /**
71      * The cap on velocity in pixel per second a swipe gesture is calculated to have.
72      */
73     private final int mMaximumFlingVelocity;
74     /**
75      * The transaltion that a view can have. To set change value of
76      * {@code R.dimen.max_translation_headsup} to a non zero value. If set to zero, the view's
77      * dimensions(height/width) will be used instead.
78      */
79     private float mMaxTranslation;
80     /**
81      * Distance by which a view should be translated by to be considered dismissed. Can be
82      * configured by setting {@code R.dimen.percentage_of_max_translation_to_dismiss}
83      */
84     private float mDismissDelta;
85     private VelocityTracker mVelocityTracker;
86     private float mDownX;
87     private float mDownY;
88     private boolean mSwiping;
89     private int mSwipingSlop;
90     private float mTranslation;
91 
92     /**
93      * The callback indicating the supplied view has been dismissed.
94      */
95     interface DismissCallbacks {
onDismiss()96         void onDismiss();
97     }
98 
99     private enum Axis {
100         HORIZONTAL, VERTICAL;
101 
getOppositeAxis()102         public Axis getOppositeAxis() {
103             switch (this) {
104                 case VERTICAL:
105                     return HORIZONTAL;
106                 default:
107                     return VERTICAL;
108             }
109         }
110     }
111 
HeadsUpNotificationOnTouchListener(View view, boolean dismissOnSwipe, DismissCallbacks callbacks)112     HeadsUpNotificationOnTouchListener(View view, boolean dismissOnSwipe,
113             DismissCallbacks callbacks) {
114         mView = view;
115         mCallbacks = callbacks;
116         mDismissOnSwipe = dismissOnSwipe;
117         Resources res = view.getContext().getResources();
118         mDismissAxis = res.getBoolean(R.bool.config_isHeadsUpNotificationDismissibleVertically)
119                 ? Axis.VERTICAL : Axis.HORIZONTAL;
120         mTouchSlop = res.getDimensionPixelSize(R.dimen.touch_slop);
121         mPercentageOfMaxTransaltionToDismiss =
122                 res.getFloat(R.dimen.percentage_of_max_translation_to_dismiss);
123         mMaxTranslation = res.getDimension(R.dimen.max_translation_headsup);
124         if (mMaxTranslation != 0) {
125             mDismissDelta = mMaxTranslation * mPercentageOfMaxTransaltionToDismiss;
126         } else {
127             mView.getViewTreeObserver().addOnGlobalLayoutListener(
128                     new ViewTreeObserver.OnGlobalLayoutListener() {
129                         @Override
130                         public void onGlobalLayout() {
131                             mView.getViewTreeObserver().removeOnGlobalLayoutListener(this);
132                             if (mDismissAxis == Axis.VERTICAL) {
133                                 mMaxTranslation = view.getHeight();
134                             } else {
135                                 mMaxTranslation = view.getWidth();
136                             }
137                             mDismissDelta = mMaxTranslation * mPercentageOfMaxTransaltionToDismiss;
138                         }
139                     });
140         }
141         ViewConfiguration viewConfiguration = ViewConfiguration.get(view.getContext());
142         mMaximumFlingVelocity = viewConfiguration.getScaledMaximumFlingVelocity();
143         mMinimumFlingVelocity = viewConfiguration.getScaledMinimumFlingVelocity();
144     }
145 
146     @Override
onTouch(View view, MotionEvent motionEvent)147     public boolean onTouch(View view, MotionEvent motionEvent) {
148         if (mDismissAxis == Axis.VERTICAL) {
149             motionEvent.offsetLocation(INITIAL_TRANSLATION_X, /* deltaY= */ mTranslation);
150         } else {
151             motionEvent.offsetLocation(/* deltaX= */ mTranslation, INITIAL_TRANSLATION_Y);
152         }
153 
154         switch (motionEvent.getActionMasked()) {
155             case MotionEvent.ACTION_DOWN: {
156                 mDownX = motionEvent.getRawX();
157                 mDownY = motionEvent.getRawY();
158                 mVelocityTracker = obtainVelocityTracker();
159                 mVelocityTracker.addMovement(motionEvent);
160                 break;
161             }
162 
163             case MotionEvent.ACTION_UP: {
164                 if (mVelocityTracker == null) {
165                     return false;
166                 }
167 
168                 mVelocityTracker.addMovement(motionEvent);
169                 mVelocityTracker.computeCurrentVelocity(PIXELS_PER_SECOND, mMaximumFlingVelocity);
170                 float deltaInDismissAxis =
171                         getDeltaInAxis(mDownX, mDownY, motionEvent, mDismissAxis);
172                 boolean shouldBeDismissed = false;
173                 boolean dismissInPositiveDirection = false;
174                 if (Math.abs(deltaInDismissAxis) > mDismissDelta) {
175                     // dismiss when the movement is more than the defined threshold.
176                     shouldBeDismissed = true;
177                     dismissInPositiveDirection = deltaInDismissAxis > 0;
178                 } else if (mSwiping && isFlingEnoughForDismiss(mVelocityTracker, mDismissAxis)
179                         && isFlingInSameDirectionAsDelta(
180                                 deltaInDismissAxis, mVelocityTracker, mDismissAxis)) {
181                     // dismiss when the velocity is more than the defined threshold.
182                     // dismiss only if flinging in the same direction as dragging.
183                     shouldBeDismissed = true;
184                     dismissInPositiveDirection =
185                             getVelocityInAxis(mVelocityTracker, mDismissAxis) > 0;
186                 }
187 
188                 if (shouldBeDismissed && mDismissOnSwipe) {
189                     mCallbacks.onDismiss();
190                     animateDismissInAxis(mView, mDismissAxis, dismissInPositiveDirection);
191                 } else if (mSwiping) {
192                     animateToCenter();
193                 }
194                 reset();
195                 break;
196             }
197 
198             case MotionEvent.ACTION_CANCEL: {
199                 if (mVelocityTracker == null) {
200                     return false;
201                 }
202                 animateToCenter();
203                 reset();
204                 return false;
205             }
206 
207             case MotionEvent.ACTION_MOVE: {
208                 if (mVelocityTracker == null) {
209                     return false;
210                 }
211 
212                 mVelocityTracker.addMovement(motionEvent);
213                 float deltaInDismissAxis =
214                         getDeltaInAxis(mDownX, mDownY, motionEvent, mDismissAxis);
215                 if (Math.abs(deltaInDismissAxis) > mTouchSlop) {
216                     mSwiping = true;
217                     mSwipingSlop = (deltaInDismissAxis > 0 ? mTouchSlop : -mTouchSlop);
218                     disallowAndCancelTouchEvents(mView, motionEvent);
219                 }
220 
221                 if (mSwiping) {
222                     mTranslation = deltaInDismissAxis;
223                     moveView(mView,
224                             /* translation= */ deltaInDismissAxis - mSwipingSlop, mDismissAxis);
225                     if (mDismissOnSwipe) {
226                         mView.setAlpha(getAlphaForDismissingView(mTranslation, mMaxTranslation));
227                     }
228                     return true;
229                 }
230             }
231         }
232         return false;
233     }
234 
animateToCenter()235     private void animateToCenter() {
236         mView.animate()
237                 .translationX(INITIAL_TRANSLATION_X)
238                 .translationY(INITIAL_TRANSLATION_Y)
239                 .alpha(MAXIMUM_ALPHA)
240                 .setListener(null);
241     }
242 
reset()243     private void reset() {
244         if (mVelocityTracker != null) {
245             mVelocityTracker.recycle();
246         }
247         mVelocityTracker = null;
248         mTranslation = 0;
249         mDownX = 0;
250         mDownY = 0;
251         mSwiping = false;
252     }
253 
resetView(View view)254     private void resetView(View view) {
255         view.setTranslationX(INITIAL_TRANSLATION_X);
256         view.setTranslationY(INITIAL_TRANSLATION_Y);
257         view.setAlpha(MAXIMUM_ALPHA);
258     }
259 
getDeltaInAxis( float downX, float downY, MotionEvent motionEvent, Axis dismissAxis)260     private float getDeltaInAxis(
261             float downX, float downY, MotionEvent motionEvent, Axis dismissAxis) {
262         switch (dismissAxis) {
263             case VERTICAL:
264                 return motionEvent.getRawY() - downY;
265             default:
266                 return motionEvent.getRawX() - downX;
267         }
268     }
269 
disallowAndCancelTouchEvents(View view, MotionEvent motionEvent)270     private void disallowAndCancelTouchEvents(View view, MotionEvent motionEvent) {
271         view.getParent().requestDisallowInterceptTouchEvent(true);
272 
273         // prevent onClickListener being triggered when moving.
274         MotionEvent cancelEvent = obtainMotionEvent(motionEvent);
275         cancelEvent.setAction(MotionEvent.ACTION_CANCEL
276                 | (motionEvent.getActionIndex()
277                 << MotionEvent.ACTION_POINTER_INDEX_SHIFT));
278         view.onTouchEvent(cancelEvent);
279         cancelEvent.recycle();
280     }
281 
moveView(View view, float translation, Axis dismissAxis)282     private void moveView(View view, float translation, Axis dismissAxis) {
283         if (dismissAxis == Axis.VERTICAL) {
284             view.setTranslationY(translation);
285         } else {
286             view.setTranslationX(translation);
287         }
288     }
289 
getAlphaForDismissingView(float translation, float maxTranslation)290     private float getAlphaForDismissingView(float translation, float maxTranslation) {
291         float fractionMoved = Math.abs(translation) / Math.abs(maxTranslation);
292         // min is required to avoid value greater than MAXIMUM_ALPHA
293         float alphaBasedOnTranslation = Math.min(MAXIMUM_ALPHA,
294                 MAXIMUM_ALPHA - (ALPHA_FADE_FACTOR_MULTIPLIER * fractionMoved));
295         // max is required to avoid alpha values less than min
296         return Math.max(MINIMUM_ALPHA, alphaBasedOnTranslation);
297     }
298 
isFlingEnoughForDismiss(VelocityTracker velocityTracker, Axis axis)299     private boolean isFlingEnoughForDismiss(VelocityTracker velocityTracker, Axis axis) {
300         float velocityInDismissingDirection = getVelocityInAxis(velocityTracker, axis);
301         float velocityInOppositeDirection =
302                 getVelocityInAxis(velocityTracker, axis.getOppositeAxis());
303         boolean isMoreFlingInDismissAxis =
304                 Math.abs(velocityInDismissingDirection) > Math.abs(velocityInOppositeDirection);
305         return mMinimumFlingVelocity <= Math.abs(velocityInDismissingDirection)
306                 && isMoreFlingInDismissAxis;
307     }
308 
getVelocityInAxis(VelocityTracker velocityTracker, Axis axis)309     private float getVelocityInAxis(VelocityTracker velocityTracker, Axis axis) {
310         switch (axis) {
311             case VERTICAL:
312                 return velocityTracker.getYVelocity();
313             default:
314                 return velocityTracker.getXVelocity();
315         }
316     }
317 
isFlingInSameDirectionAsDelta(float delta, VelocityTracker velocityTracker, Axis axis)318     private boolean isFlingInSameDirectionAsDelta(float delta, VelocityTracker velocityTracker,
319             Axis axis) {
320         float velocityInDismissingDirection = getVelocityInAxis(velocityTracker, axis);
321         boolean isVelocityInPositiveDirection = velocityInDismissingDirection > 0;
322         boolean isDeltaInPositiveDirection = delta > 0;
323         return isVelocityInPositiveDirection == isDeltaInPositiveDirection;
324     }
325 
animateDismissInAxis(View view, Axis axis, boolean dismissInPositiveDirection)326     private void animateDismissInAxis(View view, Axis axis, boolean dismissInPositiveDirection) {
327         float dismissTranslation = dismissInPositiveDirection ? mMaxTranslation : -mMaxTranslation;
328         ViewPropertyAnimator animator = view.animate();
329         if (axis == Axis.VERTICAL) {
330             animator.translationY(dismissTranslation);
331         } else {
332             animator.translationX(dismissTranslation);
333         }
334         animator.alpha(MINIMUM_ALPHA).setListener(new AnimatorListenerAdapter() {
335             @Override
336             public void onAnimationEnd(Animator animation) {
337                 resetView(mView);
338             }
339         }).start();
340     }
341 
342     /**
343      * Should be overridden in test to not access static obtain method.
344      */
345     @VisibleForTesting
obtainMotionEvent(MotionEvent motionEvent)346     MotionEvent obtainMotionEvent(MotionEvent motionEvent) {
347         return MotionEvent.obtain(motionEvent);
348     }
349 
350     /**
351      * Should be overridden in test to not access static obtain method.
352      */
353     @VisibleForTesting
obtainVelocityTracker()354     VelocityTracker obtainVelocityTracker() {
355         return VelocityTracker.obtain();
356     }
357 
358     @VisibleForTesting
getMinimumFlingVelocity()359     int getMinimumFlingVelocity() {
360         return mMinimumFlingVelocity;
361     }
362 
363     @VisibleForTesting
getTouchSlop()364     int getTouchSlop() {
365         return mTouchSlop;
366     }
367 
368     @VisibleForTesting
getPercentageOfMaxTransaltionToDismiss()369     float getPercentageOfMaxTransaltionToDismiss() {
370         return mPercentageOfMaxTransaltionToDismiss;
371     }
372 }
373