1 /*
2  * Copyright (C) 2018 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 package com.android.systemui.qs.touch;
17 
18 import static android.view.MotionEvent.INVALID_POINTER_ID;
19 
20 import android.content.Context;
21 import android.graphics.PointF;
22 import android.util.Log;
23 import android.view.MotionEvent;
24 import android.view.ViewConfiguration;
25 
26 import androidx.annotation.NonNull;
27 import androidx.annotation.VisibleForTesting;
28 
29 /**
30  * One dimensional scroll/drag/swipe gesture detector.
31  *
32  * Definition of swipe is different from android system in that this detector handles
33  * 'swipe to dismiss', 'swiping up/down a container' but also keeps scrolling state before
34  * swipe action happens
35  *
36  * Copied from packages/apps/Launcher3/src/com/android/launcher3/touch/SwipeDetector.java
37  */
38 public class SwipeDetector {
39 
40     private static final boolean DBG = false;
41     private static final String TAG = "SwipeDetector";
42 
43     private int mScrollConditions;
44     public static final int DIRECTION_POSITIVE = 1 << 0;
45     public static final int DIRECTION_NEGATIVE = 1 << 1;
46     public static final int DIRECTION_BOTH = DIRECTION_NEGATIVE | DIRECTION_POSITIVE;
47 
48     private static final float ANIMATION_DURATION = 1200;
49 
50     protected int mActivePointerId = INVALID_POINTER_ID;
51 
52     /**
53      * The minimum release velocity in pixels per millisecond that triggers fling..
54      */
55     public static final float RELEASE_VELOCITY_PX_MS = 1.0f;
56 
57     /**
58      * The time constant used to calculate dampening in the low-pass filter of scroll velocity.
59      * Cutoff frequency is set at 10 Hz.
60      */
61     public static final float SCROLL_VELOCITY_DAMPENING_RC = 1000f / (2f * (float) Math.PI * 10);
62 
63     /* Scroll state, this is set to true during dragging and animation. */
64     private ScrollState mState = ScrollState.IDLE;
65 
66     enum ScrollState {
67         IDLE,
68         DRAGGING,      // onDragStart, onDrag
69         SETTLING       // onDragEnd
70     }
71 
72     public static abstract class Direction {
73 
getDisplacement(MotionEvent ev, int pointerIndex, PointF refPoint)74         abstract float getDisplacement(MotionEvent ev, int pointerIndex, PointF refPoint);
75 
76         /**
77          * Distance in pixels a touch can wander before we think the user is scrolling.
78          */
getActiveTouchSlop(MotionEvent ev, int pointerIndex, PointF downPos)79         abstract float getActiveTouchSlop(MotionEvent ev, int pointerIndex, PointF downPos);
80     }
81 
82     public static final Direction VERTICAL = new Direction() {
83 
84         @Override
85         float getDisplacement(MotionEvent ev, int pointerIndex, PointF refPoint) {
86             return ev.getY(pointerIndex) - refPoint.y;
87         }
88 
89         @Override
90         float getActiveTouchSlop(MotionEvent ev, int pointerIndex, PointF downPos) {
91             return Math.abs(ev.getX(pointerIndex) - downPos.x);
92         }
93     };
94 
95     public static final Direction HORIZONTAL = new Direction() {
96 
97         @Override
98         float getDisplacement(MotionEvent ev, int pointerIndex, PointF refPoint) {
99             return ev.getX(pointerIndex) - refPoint.x;
100         }
101 
102         @Override
103         float getActiveTouchSlop(MotionEvent ev, int pointerIndex, PointF downPos) {
104             return Math.abs(ev.getY(pointerIndex) - downPos.y);
105         }
106     };
107 
108     //------------------- ScrollState transition diagram -----------------------------------
109     //
110     // IDLE ->      (mDisplacement > mTouchSlop) -> DRAGGING
111     // DRAGGING -> (MotionEvent#ACTION_UP, MotionEvent#ACTION_CANCEL) -> SETTLING
112     // SETTLING -> (MotionEvent#ACTION_DOWN) -> DRAGGING
113     // SETTLING -> (View settled) -> IDLE
114 
setState(ScrollState newState)115     private void setState(ScrollState newState) {
116         if (DBG) {
117             Log.d(TAG, "setState:" + mState + "->" + newState);
118         }
119         // onDragStart and onDragEnd is reported ONLY on state transition
120         if (newState == ScrollState.DRAGGING) {
121             initializeDragging();
122             if (mState == ScrollState.IDLE) {
123                 reportDragStart(false /* recatch */);
124             } else if (mState == ScrollState.SETTLING) {
125                 reportDragStart(true /* recatch */);
126             }
127         }
128         if (newState == ScrollState.SETTLING) {
129             reportDragEnd();
130         }
131 
132         mState = newState;
133     }
134 
isDraggingOrSettling()135     public boolean isDraggingOrSettling() {
136         return mState == ScrollState.DRAGGING || mState == ScrollState.SETTLING;
137     }
138 
139     /**
140      * There's no touch and there's no animation.
141      */
isIdleState()142     public boolean isIdleState() {
143         return mState == ScrollState.IDLE;
144     }
145 
isSettlingState()146     public boolean isSettlingState() {
147         return mState == ScrollState.SETTLING;
148     }
149 
isDraggingState()150     public boolean isDraggingState() {
151         return mState == ScrollState.DRAGGING;
152     }
153 
154     private final PointF mDownPos = new PointF();
155     private final PointF mLastPos = new PointF();
156     private final Direction mDir;
157 
158     private final float mTouchSlop;
159 
160     /* Client of this gesture detector can register a callback. */
161     private final Listener mListener;
162 
163     private long mCurrentMillis;
164 
165     private float mVelocity;
166     private float mLastDisplacement;
167     private float mDisplacement;
168 
169     private float mSubtractDisplacement;
170     private boolean mIgnoreSlopWhenSettling;
171 
172     public interface Listener {
onDragStart(boolean start)173         void onDragStart(boolean start);
174 
onDrag(float displacement, float velocity)175         boolean onDrag(float displacement, float velocity);
176 
onDragEnd(float velocity, boolean fling)177         void onDragEnd(float velocity, boolean fling);
178     }
179 
SwipeDetector(@onNull Context context, @NonNull Listener l, @NonNull Direction dir)180     public SwipeDetector(@NonNull Context context, @NonNull Listener l, @NonNull Direction dir) {
181         this(ViewConfiguration.get(context).getScaledTouchSlop(), l, dir);
182     }
183 
184     @VisibleForTesting
SwipeDetector(float touchSlope, @NonNull Listener l, @NonNull Direction dir)185     protected SwipeDetector(float touchSlope, @NonNull Listener l, @NonNull Direction dir) {
186         mTouchSlop = touchSlope;
187         mListener = l;
188         mDir = dir;
189     }
190 
setDetectableScrollConditions(int scrollDirectionFlags, boolean ignoreSlop)191     public void setDetectableScrollConditions(int scrollDirectionFlags, boolean ignoreSlop) {
192         mScrollConditions = scrollDirectionFlags;
193         mIgnoreSlopWhenSettling = ignoreSlop;
194     }
195 
shouldScrollStart(MotionEvent ev, int pointerIndex)196     private boolean shouldScrollStart(MotionEvent ev, int pointerIndex) {
197         // reject cases where the angle or slop condition is not met.
198         if (Math.max(mDir.getActiveTouchSlop(ev, pointerIndex, mDownPos), mTouchSlop)
199                 > Math.abs(mDisplacement)) {
200             return false;
201         }
202 
203         // Check if the client is interested in scroll in current direction.
204         if (((mScrollConditions & DIRECTION_NEGATIVE) > 0 && mDisplacement > 0) ||
205                 ((mScrollConditions & DIRECTION_POSITIVE) > 0 && mDisplacement < 0)) {
206             return true;
207         }
208         return false;
209     }
210 
onTouchEvent(MotionEvent ev)211     public boolean onTouchEvent(MotionEvent ev) {
212         switch (ev.getActionMasked()) {
213             case MotionEvent.ACTION_DOWN:
214                 mActivePointerId = ev.getPointerId(0);
215                 mDownPos.set(ev.getX(), ev.getY());
216                 mLastPos.set(mDownPos);
217                 mLastDisplacement = 0;
218                 mDisplacement = 0;
219                 mVelocity = 0;
220 
221                 if (mState == ScrollState.SETTLING && mIgnoreSlopWhenSettling) {
222                     setState(ScrollState.DRAGGING);
223                 }
224                 break;
225             //case MotionEvent.ACTION_POINTER_DOWN:
226             case MotionEvent.ACTION_POINTER_UP:
227                 int ptrIdx = ev.getActionIndex();
228                 int ptrId = ev.getPointerId(ptrIdx);
229                 if (ptrId == mActivePointerId) {
230                     final int newPointerIdx = ptrIdx == 0 ? 1 : 0;
231                     mDownPos.set(
232                             ev.getX(newPointerIdx) - (mLastPos.x - mDownPos.x),
233                             ev.getY(newPointerIdx) - (mLastPos.y - mDownPos.y));
234                     mLastPos.set(ev.getX(newPointerIdx), ev.getY(newPointerIdx));
235                     mActivePointerId = ev.getPointerId(newPointerIdx);
236                 }
237                 break;
238             case MotionEvent.ACTION_MOVE:
239                 int pointerIndex = ev.findPointerIndex(mActivePointerId);
240                 if (pointerIndex == INVALID_POINTER_ID) {
241                     break;
242                 }
243                 mDisplacement = mDir.getDisplacement(ev, pointerIndex, mDownPos);
244                 computeVelocity(mDir.getDisplacement(ev, pointerIndex, mLastPos),
245                         ev.getEventTime());
246 
247                 // handle state and listener calls.
248                 if (mState != ScrollState.DRAGGING && shouldScrollStart(ev, pointerIndex)) {
249                     setState(ScrollState.DRAGGING);
250                 }
251                 if (mState == ScrollState.DRAGGING) {
252                     reportDragging();
253                 }
254                 mLastPos.set(ev.getX(pointerIndex), ev.getY(pointerIndex));
255                 break;
256             case MotionEvent.ACTION_CANCEL:
257             case MotionEvent.ACTION_UP:
258                 // These are synthetic events and there is no need to update internal values.
259                 if (mState == ScrollState.DRAGGING) {
260                     setState(ScrollState.SETTLING);
261                 }
262                 break;
263             default:
264                 break;
265         }
266         return true;
267     }
268 
finishedScrolling()269     public void finishedScrolling() {
270         setState(ScrollState.IDLE);
271     }
272 
reportDragStart(boolean recatch)273     private boolean reportDragStart(boolean recatch) {
274         mListener.onDragStart(!recatch);
275         if (DBG) {
276             Log.d(TAG, "onDragStart recatch:" + recatch);
277         }
278         return true;
279     }
280 
initializeDragging()281     private void initializeDragging() {
282         if (mState == ScrollState.SETTLING && mIgnoreSlopWhenSettling) {
283             mSubtractDisplacement = 0;
284         }
285         if (mDisplacement > 0) {
286             mSubtractDisplacement = mTouchSlop;
287         } else {
288             mSubtractDisplacement = -mTouchSlop;
289         }
290     }
291 
reportDragging()292     private boolean reportDragging() {
293         if (mDisplacement != mLastDisplacement) {
294             if (DBG) {
295                 Log.d(TAG, String.format("onDrag disp=%.1f, velocity=%.1f",
296                         mDisplacement, mVelocity));
297             }
298 
299             mLastDisplacement = mDisplacement;
300             return mListener.onDrag(mDisplacement - mSubtractDisplacement, mVelocity);
301         }
302         return true;
303     }
304 
reportDragEnd()305     private void reportDragEnd() {
306         if (DBG) {
307             Log.d(TAG, String.format("onScrollEnd disp=%.1f, velocity=%.1f",
308                     mDisplacement, mVelocity));
309         }
310         mListener.onDragEnd(mVelocity, Math.abs(mVelocity) > RELEASE_VELOCITY_PX_MS);
311 
312     }
313 
314     /**
315      * Computes the damped velocity.
316      */
computeVelocity(float delta, long currentMillis)317     public float computeVelocity(float delta, long currentMillis) {
318         long previousMillis = mCurrentMillis;
319         mCurrentMillis = currentMillis;
320 
321         float deltaTimeMillis = mCurrentMillis - previousMillis;
322         float velocity = (deltaTimeMillis > 0) ? (delta / deltaTimeMillis) : 0;
323         if (Math.abs(mVelocity) < 0.001f) {
324             mVelocity = velocity;
325         } else {
326             float alpha = computeDampeningFactor(deltaTimeMillis);
327             mVelocity = interpolate(mVelocity, velocity, alpha);
328         }
329         return mVelocity;
330     }
331 
332     /**
333      * Returns a time-dependent dampening factor using delta time.
334      */
computeDampeningFactor(float deltaTime)335     private static float computeDampeningFactor(float deltaTime) {
336         return deltaTime / (SCROLL_VELOCITY_DAMPENING_RC + deltaTime);
337     }
338 
339     /**
340      * Returns the linear interpolation between two values
341      */
interpolate(float from, float to, float alpha)342     private static float interpolate(float from, float to, float alpha) {
343         return (1.0f - alpha) * from + alpha * to;
344     }
345 
calculateDuration(float velocity, float progressNeeded)346     public static long calculateDuration(float velocity, float progressNeeded) {
347         // TODO: make these values constants after tuning.
348         float velocityDivisor = Math.max(2f, Math.abs(0.5f * velocity));
349         float travelDistance = Math.max(0.2f, progressNeeded);
350         long duration = (long) Math.max(100, ANIMATION_DURATION / velocityDivisor * travelDistance);
351         if (DBG) {
352             Log.d(TAG, String.format("calculateDuration=%d, v=%f, d=%f", duration, velocity, progressNeeded));
353         }
354         return duration;
355     }
356 }
357 
358