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