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.view.MotionEvent;
22 import android.view.VelocityTracker;
23 import android.view.View;
24 
25 /**
26  * OnTouchListener that enables swipe-to-dismiss gesture on heads-up notifications.
27  */
28 class HeadsUpNotificationOnTouchListener implements View.OnTouchListener {
29     /**
30      * Minimum velocity to initiate a fling, as measured in pixels per second.
31      */
32     private static final int MINIMUM_FLING_VELOCITY = 2000;
33 
34     /**
35      * Distance a touch can wander before we think the user is scrolling in pixels.
36      */
37     private static final int TOUCH_SLOP = 20;
38 
39     /**
40      * The proportion which view has to be swiped before it dismisses.
41      */
42     private static final float THRESHOLD = 0.3f;
43 
44     /**
45      * The unit of velocity in milliseconds. A value of 1 means "pixels per millisecond",
46      * 1000 means "pixels per 1000 milliseconds (1 second)".
47      */
48     private static final int VELOCITY_UNITS = 1000;
49 
50     private final View mView;
51     private final DismissCallbacks mCallbacks;
52 
53     private VelocityTracker mVelocityTracker;
54     private float mDownX;
55     private boolean mSwiping;
56     private int mSwipingSlop;
57     private float mTranslationX;
58     private boolean mDismissOnSwipe = true;
59 
60     /**
61      * The callback indicating the supplied view has been dismissed.
62      */
63     interface DismissCallbacks {
onDismiss()64         void onDismiss();
65     }
66 
HeadsUpNotificationOnTouchListener(View view, boolean dismissOnSwipe, DismissCallbacks callbacks)67     HeadsUpNotificationOnTouchListener(View view, boolean dismissOnSwipe,
68             DismissCallbacks callbacks) {
69         mView = view;
70         mCallbacks = callbacks;
71         mDismissOnSwipe = dismissOnSwipe;
72     }
73 
74     @Override
onTouch(View view, MotionEvent motionEvent)75     public boolean onTouch(View view, MotionEvent motionEvent) {
76         motionEvent.offsetLocation(mTranslationX, /* deltaY= */ 0);
77         int viewWidth = mView.getWidth();
78 
79         switch (motionEvent.getActionMasked()) {
80             case MotionEvent.ACTION_DOWN: {
81                 mDownX = motionEvent.getRawX();
82                 mVelocityTracker = VelocityTracker.obtain();
83                 mVelocityTracker.addMovement(motionEvent);
84                 return false;
85             }
86 
87             case MotionEvent.ACTION_UP: {
88                 if (mVelocityTracker == null) {
89                     return false;
90                 }
91 
92                 float deltaX = motionEvent.getRawX() - mDownX;
93                 mVelocityTracker.addMovement(motionEvent);
94                 mVelocityTracker.computeCurrentVelocity(VELOCITY_UNITS);
95                 float velocityX = mVelocityTracker.getXVelocity();
96                 float absVelocityX = Math.abs(velocityX);
97                 float absVelocityY = Math.abs(mVelocityTracker.getYVelocity());
98                 boolean dismiss = false;
99                 boolean dismissRight = false;
100                 if (Math.abs(deltaX) > viewWidth * THRESHOLD) {
101                     // dismiss when the movement is more than the defined threshold.
102                     dismiss = true;
103                     dismissRight = deltaX > 0;
104                 } else if (MINIMUM_FLING_VELOCITY <= absVelocityX
105                         && absVelocityY < absVelocityX
106                         && mSwiping) {
107                     // dismiss when the velocity is more than the defined threshold.
108                     // dismiss only if flinging in the same direction as dragging.
109                     dismiss = (velocityX < 0) == (deltaX < 0);
110                     dismissRight = mVelocityTracker.getXVelocity() > 0;
111                 }
112                 if (dismiss && mDismissOnSwipe) {
113                     mCallbacks.onDismiss();
114                     mView.animate()
115                             .translationX(dismissRight ? viewWidth : -viewWidth)
116                             .alpha(0)
117                             .setListener(new AnimatorListenerAdapter() {
118                                 @Override
119                                 public void onAnimationEnd(Animator animation) {
120                                     mView.setAlpha(1f);
121                                     mView.setTranslationX(0);
122                                 }
123                             });
124                 } else if (mSwiping) {
125                     animateToCenter();
126                 }
127                 reset();
128                 break;
129             }
130 
131             case MotionEvent.ACTION_CANCEL: {
132                 if (mVelocityTracker == null) {
133                     return false;
134                 }
135                 animateToCenter();
136                 reset();
137                 return false;
138             }
139 
140             case MotionEvent.ACTION_MOVE: {
141                 if (mVelocityTracker == null) {
142                     return false;
143                 }
144 
145                 mVelocityTracker.addMovement(motionEvent);
146                 float deltaX = motionEvent.getRawX() - mDownX;
147                 if (Math.abs(deltaX) > TOUCH_SLOP) {
148                     mSwiping = true;
149                     mSwipingSlop = (deltaX > 0 ? TOUCH_SLOP : -TOUCH_SLOP);
150                     mView.getParent().requestDisallowInterceptTouchEvent(true);
151 
152                     // prevent onClickListener being triggered when moving.
153                     MotionEvent cancelEvent = MotionEvent.obtain(motionEvent);
154                     cancelEvent.setAction(MotionEvent.ACTION_CANCEL |
155                             (motionEvent.getActionIndex() <<
156                                     MotionEvent.ACTION_POINTER_INDEX_SHIFT));
157                     mView.onTouchEvent(cancelEvent);
158                     cancelEvent.recycle();
159                 }
160 
161                 if (mSwiping) {
162                     mTranslationX = deltaX;
163                     mView.setTranslationX(deltaX - mSwipingSlop);
164                     if (!mDismissOnSwipe) {
165                         return true;
166                     }
167                     mView.setAlpha(Math.max(0f, Math.min(1f,
168                             1f - 2f * Math.abs(deltaX) / viewWidth)));
169                     return true;
170                 }
171             }
172 
173             default: {
174                 return false;
175             }
176         }
177         return false;
178     }
179 
animateToCenter()180     private void animateToCenter() {
181         mView.animate()
182                 .translationX(0)
183                 .alpha(1)
184                 .setListener(null);
185     }
186 
reset()187     private void reset() {
188         if (mVelocityTracker != null) {
189             mVelocityTracker.recycle();
190         }
191         mVelocityTracker = null;
192         mTranslationX = 0;
193         mDownX = 0;
194         mSwiping = false;
195     }
196 }
197