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