• 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.util.Log;
23 import android.view.MotionEvent;
24 import android.view.VelocityTracker;
25 import android.view.ViewConfiguration;
26 
27 import com.android.launcher3.Utilities;
28 import com.android.launcher3.testing.TestProtocol;
29 
30 import androidx.annotation.NonNull;
31 import androidx.annotation.VisibleForTesting;
32 
33 /**
34  * One dimensional scroll/drag/swipe gesture detector.
35  *
36  * Definition of swipe is different from android system in that this detector handles
37  * 'swipe to dismiss', 'swiping up/down a container' but also keeps scrolling state before
38  * swipe action happens
39  */
40 public class SwipeDetector {
41 
42     private static final boolean DBG = false;
43     private static final String TAG = "SwipeDetector";
44 
45     private int mScrollConditions;
46     public static final int DIRECTION_POSITIVE = 1 << 0;
47     public static final int DIRECTION_NEGATIVE = 1 << 1;
48     public static final int DIRECTION_BOTH = DIRECTION_NEGATIVE | DIRECTION_POSITIVE;
49 
50     private static final float ANIMATION_DURATION = 1200;
51 
52     protected int mActivePointerId = INVALID_POINTER_ID;
53 
54     /**
55      * The minimum release velocity in pixels per millisecond that triggers fling..
56      */
57     public static final float RELEASE_VELOCITY_PX_MS = 1.0f;
58 
59     /* Scroll state, this is set to true during dragging and animation. */
60     private ScrollState mState = ScrollState.IDLE;
61 
62     enum ScrollState {
63         IDLE,
64         DRAGGING,      // onDragStart, onDrag
65         SETTLING       // onDragEnd
66     }
67 
68     public static abstract class Direction {
69 
getDisplacement(MotionEvent ev, int pointerIndex, PointF refPoint, boolean isRtl)70         abstract float getDisplacement(MotionEvent ev, int pointerIndex, PointF refPoint,
71                 boolean isRtl);
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 
getVelocity(VelocityTracker tracker, boolean isRtl)78         abstract float getVelocity(VelocityTracker tracker, boolean isRtl);
79 
isPositive(float displacement)80         abstract boolean isPositive(float displacement);
81 
isNegative(float displacement)82         abstract boolean isNegative(float displacement);
83     }
84 
85     public static final Direction VERTICAL = new Direction() {
86 
87         @Override
88         float getDisplacement(MotionEvent ev, int pointerIndex, PointF refPoint, boolean isRtl) {
89             return ev.getY(pointerIndex) - refPoint.y;
90         }
91 
92         @Override
93         float getActiveTouchSlop(MotionEvent ev, int pointerIndex, PointF downPos) {
94             return Math.abs(ev.getX(pointerIndex) - downPos.x);
95         }
96 
97         @Override
98         float getVelocity(VelocityTracker tracker, boolean isRtl) {
99             return tracker.getYVelocity();
100         }
101 
102         @Override
103         boolean isPositive(float displacement) {
104             // Up
105             return displacement < 0;
106         }
107 
108         @Override
109         boolean isNegative(float displacement) {
110             // Down
111             return displacement > 0;
112         }
113     };
114 
115     public static final Direction HORIZONTAL = new Direction() {
116 
117         @Override
118         float getDisplacement(MotionEvent ev, int pointerIndex, PointF refPoint, boolean isRtl) {
119             float displacement = ev.getX(pointerIndex) - refPoint.x;
120             if (isRtl) {
121                 displacement = -displacement;
122             }
123             return displacement;
124         }
125 
126         @Override
127         float getActiveTouchSlop(MotionEvent ev, int pointerIndex, PointF downPos) {
128             return Math.abs(ev.getY(pointerIndex) - downPos.y);
129         }
130 
131         @Override
132         float getVelocity(VelocityTracker tracker, boolean isRtl) {
133             float velocity = tracker.getXVelocity();
134             if (isRtl) {
135                 velocity = -velocity;
136             }
137             return velocity;
138         }
139 
140         @Override
141         boolean isPositive(float displacement) {
142             // Right
143             return displacement > 0;
144         }
145 
146         @Override
147         boolean isNegative(float displacement) {
148             // Left
149             return displacement < 0;
150         }
151     };
152 
153     //------------------- ScrollState transition diagram -----------------------------------
154     //
155     // IDLE ->      (mDisplacement > mTouchSlop) -> DRAGGING
156     // DRAGGING -> (MotionEvent#ACTION_UP, MotionEvent#ACTION_CANCEL) -> SETTLING
157     // SETTLING -> (MotionEvent#ACTION_DOWN) -> DRAGGING
158     // SETTLING -> (View settled) -> IDLE
159 
setState(ScrollState newState)160     private void setState(ScrollState newState) {
161         if (TestProtocol.sDebugTracing) {
162             Log.d(TestProtocol.NO_ALLAPPS_EVENT_TAG, "setState -- start: " + newState);
163         }
164         if (DBG) {
165             Log.d(TAG, "setState:" + mState + "->" + newState);
166         }
167         // onDragStart and onDragEnd is reported ONLY on state transition
168         if (newState == ScrollState.DRAGGING) {
169             initializeDragging();
170             if (mState == ScrollState.IDLE) {
171                 if (TestProtocol.sDebugTracing) {
172                     Log.d(TestProtocol.NO_ALLAPPS_EVENT_TAG, "setState -- 1: " + newState);
173                 }
174                 reportDragStart(false /* recatch */);
175             } else if (mState == ScrollState.SETTLING) {
176                 reportDragStart(true /* recatch */);
177             }
178         }
179         if (newState == ScrollState.SETTLING) {
180             reportDragEnd();
181         }
182 
183         mState = newState;
184         if (com.android.launcher3.testing.TestProtocol.sDebugTracing) {
185             android.util.Log.e(TestProtocol.NO_ALLAPPS_EVENT_TAG,
186                     "setState: " + newState + " @ " + android.util.Log.getStackTraceString(
187                             new Throwable()));
188         }
189     }
190 
isDraggingOrSettling()191     public boolean isDraggingOrSettling() {
192         return mState == ScrollState.DRAGGING || mState == ScrollState.SETTLING;
193     }
194 
getDownX()195     public int getDownX() {
196         return (int) mDownPos.x;
197     }
198 
getDownY()199     public int getDownY() {
200         return (int) mDownPos.y;
201     }
202     /**
203      * There's no touch and there's no animation.
204      */
isIdleState()205     public boolean isIdleState() {
206         return mState == ScrollState.IDLE;
207     }
208 
isSettlingState()209     public boolean isSettlingState() {
210         return mState == ScrollState.SETTLING;
211     }
212 
isDraggingState()213     public boolean isDraggingState() {
214         return mState == ScrollState.DRAGGING;
215     }
216 
217     private final PointF mDownPos = new PointF();
218     private final PointF mLastPos = new PointF();
219     private final Direction mDir;
220     private final boolean mIsRtl;
221 
222     private final float mTouchSlop;
223     private final float mMaxVelocity;
224 
225     /* Client of this gesture detector can register a callback. */
226     private final Listener mListener;
227 
228     private VelocityTracker mVelocityTracker;
229 
230     private float mLastDisplacement;
231     private float mDisplacement;
232 
233     private float mSubtractDisplacement;
234     private boolean mIgnoreSlopWhenSettling;
235 
236     public interface Listener {
onDragStart(boolean start)237         void onDragStart(boolean start);
238 
onDrag(float displacement)239         boolean onDrag(float displacement);
240 
onDrag(float displacement, MotionEvent event)241         default boolean onDrag(float displacement, MotionEvent event) {
242             return onDrag(displacement);
243         }
244 
onDragEnd(float velocity, boolean fling)245         void onDragEnd(float velocity, boolean fling);
246     }
247 
SwipeDetector(@onNull Context context, @NonNull Listener l, @NonNull Direction dir)248     public SwipeDetector(@NonNull Context context, @NonNull Listener l, @NonNull Direction dir) {
249         this(ViewConfiguration.get(context), l, dir, Utilities.isRtl(context.getResources()));
250     }
251 
252     @VisibleForTesting
SwipeDetector(@onNull ViewConfiguration config, @NonNull Listener l, @NonNull Direction dir, boolean isRtl)253     protected SwipeDetector(@NonNull ViewConfiguration config, @NonNull Listener l,
254             @NonNull Direction dir, boolean isRtl) {
255         mListener = l;
256         mDir = dir;
257         mIsRtl = isRtl;
258         mTouchSlop = config.getScaledTouchSlop();
259         mMaxVelocity = config.getScaledMaximumFlingVelocity();
260     }
261 
setDetectableScrollConditions(int scrollDirectionFlags, boolean ignoreSlop)262     public void setDetectableScrollConditions(int scrollDirectionFlags, boolean ignoreSlop) {
263         mScrollConditions = scrollDirectionFlags;
264         mIgnoreSlopWhenSettling = ignoreSlop;
265     }
266 
getScrollDirections()267     public int getScrollDirections() {
268         return mScrollConditions;
269     }
270 
shouldScrollStart(MotionEvent ev, int pointerIndex)271     private boolean shouldScrollStart(MotionEvent ev, int pointerIndex) {
272         // reject cases where the angle or slop condition is not met.
273         if (Math.max(mDir.getActiveTouchSlop(ev, pointerIndex, mDownPos), mTouchSlop)
274                 > Math.abs(mDisplacement)) {
275             return false;
276         }
277 
278         // Check if the client is interested in scroll in current direction.
279         if (((mScrollConditions & DIRECTION_NEGATIVE) > 0 && mDir.isNegative(mDisplacement)) ||
280                 ((mScrollConditions & DIRECTION_POSITIVE) > 0 && mDir.isPositive(mDisplacement))) {
281             return true;
282         }
283         return false;
284     }
285 
onTouchEvent(MotionEvent ev)286     public boolean onTouchEvent(MotionEvent ev) {
287         int actionMasked = ev.getActionMasked();
288         if (actionMasked == MotionEvent.ACTION_DOWN && mVelocityTracker != null) {
289             mVelocityTracker.clear();
290         }
291         if (mVelocityTracker == null) {
292             mVelocityTracker = VelocityTracker.obtain();
293         }
294         mVelocityTracker.addMovement(ev);
295 
296         switch (actionMasked) {
297             case MotionEvent.ACTION_DOWN:
298                 mActivePointerId = ev.getPointerId(0);
299                 mDownPos.set(ev.getX(), ev.getY());
300                 mLastPos.set(mDownPos);
301                 mLastDisplacement = 0;
302                 mDisplacement = 0;
303 
304                 if (mState == ScrollState.SETTLING && mIgnoreSlopWhenSettling) {
305                     setState(ScrollState.DRAGGING);
306                 }
307                 break;
308             //case MotionEvent.ACTION_POINTER_DOWN:
309             case MotionEvent.ACTION_POINTER_UP:
310                 int ptrIdx = ev.getActionIndex();
311                 int ptrId = ev.getPointerId(ptrIdx);
312                 if (ptrId == mActivePointerId) {
313                     final int newPointerIdx = ptrIdx == 0 ? 1 : 0;
314                     mDownPos.set(
315                             ev.getX(newPointerIdx) - (mLastPos.x - mDownPos.x),
316                             ev.getY(newPointerIdx) - (mLastPos.y - mDownPos.y));
317                     mLastPos.set(ev.getX(newPointerIdx), ev.getY(newPointerIdx));
318                     mActivePointerId = ev.getPointerId(newPointerIdx);
319                 }
320                 break;
321             case MotionEvent.ACTION_MOVE:
322                 int pointerIndex = ev.findPointerIndex(mActivePointerId);
323                 if (pointerIndex == INVALID_POINTER_ID) {
324                     break;
325                 }
326                 mDisplacement = mDir.getDisplacement(ev, pointerIndex, mDownPos, mIsRtl);
327                 if (TestProtocol.sDebugTracing) {
328                     Log.d(TestProtocol.NO_ALLAPPS_EVENT_TAG, "onTouchEvent 1");
329                 }
330 
331                 // handle state and listener calls.
332                 if (mState != ScrollState.DRAGGING && shouldScrollStart(ev, pointerIndex)) {
333                     if (TestProtocol.sDebugTracing) {
334                         Log.d(TestProtocol.NO_ALLAPPS_EVENT_TAG, "onTouchEvent 2");
335                     }
336                     setState(ScrollState.DRAGGING);
337                 }
338                 if (mState == ScrollState.DRAGGING) {
339                     reportDragging(ev);
340                 }
341                 mLastPos.set(ev.getX(pointerIndex), ev.getY(pointerIndex));
342                 break;
343             case MotionEvent.ACTION_CANCEL:
344             case MotionEvent.ACTION_UP:
345                 // These are synthetic events and there is no need to update internal values.
346                 if (mState == ScrollState.DRAGGING) {
347                     setState(ScrollState.SETTLING);
348                 }
349                 mVelocityTracker.recycle();
350                 mVelocityTracker = null;
351                 break;
352             default:
353                 break;
354         }
355         return true;
356     }
357 
finishedScrolling()358     public void finishedScrolling() {
359         setState(ScrollState.IDLE);
360     }
361 
reportDragStart(boolean recatch)362     private boolean reportDragStart(boolean recatch) {
363         mListener.onDragStart(!recatch);
364         if (DBG) {
365             Log.d(TAG, "onDragStart recatch:" + recatch);
366         }
367         return true;
368     }
369 
initializeDragging()370     private void initializeDragging() {
371         if (mState == ScrollState.SETTLING && mIgnoreSlopWhenSettling) {
372             mSubtractDisplacement = 0;
373         }
374         if (mDisplacement > 0) {
375             mSubtractDisplacement = mTouchSlop;
376         } else {
377             mSubtractDisplacement = -mTouchSlop;
378         }
379     }
380 
381     /**
382      * Returns if the start drag was towards the positive direction or negative.
383      *
384      * @see #setDetectableScrollConditions(int, boolean)
385      * @see #DIRECTION_BOTH
386      */
wasInitialTouchPositive()387     public boolean wasInitialTouchPositive() {
388         return mDir.isPositive(mSubtractDisplacement);
389     }
390 
reportDragging(MotionEvent event)391     private boolean reportDragging(MotionEvent event) {
392         if (mDisplacement != mLastDisplacement) {
393             if (DBG) {
394                 Log.d(TAG, String.format("onDrag disp=%.1f", mDisplacement));
395             }
396 
397             mLastDisplacement = mDisplacement;
398             return mListener.onDrag(mDisplacement - mSubtractDisplacement, event);
399         }
400         return true;
401     }
402 
reportDragEnd()403     private void reportDragEnd() {
404         mVelocityTracker.computeCurrentVelocity(1000, mMaxVelocity);
405         float velocity = mDir.getVelocity(mVelocityTracker, mIsRtl) / 1000;
406         if (DBG) {
407             Log.d(TAG, String.format("onScrollEnd disp=%.1f, velocity=%.1f",
408                     mDisplacement, velocity));
409         }
410 
411         mListener.onDragEnd(velocity, Math.abs(velocity) > RELEASE_VELOCITY_PX_MS);
412     }
413 
calculateDuration(float velocity, float progressNeeded)414     public static long calculateDuration(float velocity, float progressNeeded) {
415         // TODO: make these values constants after tuning.
416         float velocityDivisor = Math.max(2f, Math.abs(0.5f * velocity));
417         float travelDistance = Math.max(0.2f, progressNeeded);
418         long duration = (long) Math.max(100, ANIMATION_DURATION / velocityDivisor * travelDistance);
419         if (DBG) {
420             Log.d(TAG, String.format("calculateDuration=%d, v=%f, d=%f", duration, velocity, progressNeeded));
421         }
422         return duration;
423     }
424 }
425