• 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.graphics.PointF;
21 import android.util.Log;
22 import android.view.MotionEvent;
23 import android.view.VelocityTracker;
24 import android.view.ViewConfiguration;
25 
26 import androidx.annotation.NonNull;
27 import androidx.annotation.VisibleForTesting;
28 
29 import java.util.LinkedList;
30 import java.util.Queue;
31 
32 /**
33  * Scroll/drag/swipe gesture detector.
34  *
35  * Definition of swipe is different from android system in that this detector handles
36  * 'swipe to dismiss', 'swiping up/down a container' but also keeps scrolling state before
37  * swipe action happens.
38  *
39  * @see SingleAxisSwipeDetector
40  * @see BothAxesSwipeDetector
41  */
42 public abstract class BaseSwipeDetector {
43 
44     private static final boolean DBG = false;
45     private static final String TAG = "BaseSwipeDetector";
46     private static final float ANIMATION_DURATION = 1200;
47     /** The minimum release velocity in pixels per millisecond that triggers fling.*/
48     private static final float RELEASE_VELOCITY_PX_MS = 1.0f;
49     private static final PointF sTempPoint = new PointF();
50 
51     private final PointF mDownPos = new PointF();
52     private final PointF mLastPos = new PointF();
53     protected final boolean mIsRtl;
54     protected final float mTouchSlop;
55     protected final float mMaxVelocity;
56     private final Queue<Runnable> mSetStateQueue = new LinkedList<>();
57 
58     private int mActivePointerId = INVALID_POINTER_ID;
59     private VelocityTracker mVelocityTracker;
60     private PointF mLastDisplacement = new PointF();
61     private PointF mDisplacement = new PointF();
62     protected PointF mSubtractDisplacement = new PointF();
63     @VisibleForTesting ScrollState mState = ScrollState.IDLE;
64     private boolean mIsSettingState;
65 
66     protected boolean mIgnoreSlopWhenSettling;
67 
68     private enum ScrollState {
69         IDLE,
70         DRAGGING,      // onDragStart, onDrag
71         SETTLING       // onDragEnd
72     }
73 
BaseSwipeDetector(@onNull ViewConfiguration config, boolean isRtl)74     protected BaseSwipeDetector(@NonNull ViewConfiguration config, boolean isRtl) {
75         mTouchSlop = config.getScaledTouchSlop();
76         mMaxVelocity = config.getScaledMaximumFlingVelocity();
77         mIsRtl = isRtl;
78     }
79 
calculateDuration(float velocity, float progressNeeded)80     public static long calculateDuration(float velocity, float progressNeeded) {
81         // TODO: make these values constants after tuning.
82         float velocityDivisor = Math.max(2f, Math.abs(0.5f * velocity));
83         float travelDistance = Math.max(0.2f, progressNeeded);
84         long duration = (long) Math.max(100, ANIMATION_DURATION / velocityDivisor * travelDistance);
85         if (DBG) {
86             Log.d(TAG, String.format(
87                     "calculateDuration=%d, v=%f, d=%f", duration, velocity, progressNeeded));
88         }
89         return duration;
90     }
91 
getDownX()92     public int getDownX() {
93         return (int) mDownPos.x;
94     }
95 
getDownY()96     public int getDownY() {
97         return (int) mDownPos.y;
98     }
99     /**
100      * There's no touch and there's no animation.
101      */
isIdleState()102     public boolean isIdleState() {
103         return mState == ScrollState.IDLE;
104     }
105 
isSettlingState()106     public boolean isSettlingState() {
107         return mState == ScrollState.SETTLING;
108     }
109 
isDraggingState()110     public boolean isDraggingState() {
111         return mState == ScrollState.DRAGGING;
112     }
113 
isDraggingOrSettling()114     public boolean isDraggingOrSettling() {
115         return mState == ScrollState.DRAGGING || mState == ScrollState.SETTLING;
116     }
117 
finishedScrolling()118     public void finishedScrolling() {
119         setState(ScrollState.IDLE);
120     }
121 
isFling(float velocity)122     public boolean isFling(float velocity) {
123         return Math.abs(velocity) > RELEASE_VELOCITY_PX_MS;
124     }
125 
onTouchEvent(MotionEvent ev)126     public boolean onTouchEvent(MotionEvent ev) {
127         int actionMasked = ev.getActionMasked();
128         if (actionMasked == MotionEvent.ACTION_DOWN && mVelocityTracker != null) {
129             mVelocityTracker.clear();
130         }
131         if (mVelocityTracker == null) {
132             mVelocityTracker = VelocityTracker.obtain();
133         }
134         mVelocityTracker.addMovement(ev);
135 
136         switch (actionMasked) {
137             case MotionEvent.ACTION_DOWN:
138                 mActivePointerId = ev.getPointerId(0);
139                 mDownPos.set(ev.getX(), ev.getY());
140                 mLastPos.set(mDownPos);
141                 mLastDisplacement.set(0, 0);
142                 mDisplacement.set(0, 0);
143 
144                 if (mState == ScrollState.SETTLING && mIgnoreSlopWhenSettling) {
145                     setState(ScrollState.DRAGGING);
146                 }
147                 break;
148             //case MotionEvent.ACTION_POINTER_DOWN:
149             case MotionEvent.ACTION_POINTER_UP:
150                 int ptrIdx = ev.getActionIndex();
151                 int ptrId = ev.getPointerId(ptrIdx);
152                 if (ptrId == mActivePointerId) {
153                     final int newPointerIdx = ptrIdx == 0 ? 1 : 0;
154                     mDownPos.set(
155                             ev.getX(newPointerIdx) - (mLastPos.x - mDownPos.x),
156                             ev.getY(newPointerIdx) - (mLastPos.y - mDownPos.y));
157                     mLastPos.set(ev.getX(newPointerIdx), ev.getY(newPointerIdx));
158                     mActivePointerId = ev.getPointerId(newPointerIdx);
159                 }
160                 break;
161             case MotionEvent.ACTION_MOVE:
162                 int pointerIndex = ev.findPointerIndex(mActivePointerId);
163                 if (pointerIndex == INVALID_POINTER_ID) {
164                     break;
165                 }
166                 mDisplacement.set(ev.getX(pointerIndex) - mDownPos.x,
167                         ev.getY(pointerIndex) - mDownPos.y);
168                 if (mIsRtl) {
169                     mDisplacement.x = -mDisplacement.x;
170                 }
171 
172                 // handle state and listener calls.
173                 if (mState != ScrollState.DRAGGING && shouldScrollStart(mDisplacement)) {
174                     setState(ScrollState.DRAGGING);
175                 }
176                 if (mState == ScrollState.DRAGGING) {
177                     reportDragging(ev);
178                 }
179                 mLastPos.set(ev.getX(pointerIndex), ev.getY(pointerIndex));
180                 break;
181             case MotionEvent.ACTION_CANCEL:
182             case MotionEvent.ACTION_UP:
183                 // These are synthetic events and there is no need to update internal values.
184                 if (mState == ScrollState.DRAGGING) {
185                     setState(ScrollState.SETTLING);
186                 }
187                 mVelocityTracker.recycle();
188                 mVelocityTracker = null;
189                 break;
190             default:
191                 break;
192         }
193         return true;
194     }
195 
196     //------------------- ScrollState transition diagram -----------------------------------
197     //
198     // IDLE -> (mDisplacement > mTouchSlop) -> DRAGGING
199     // DRAGGING -> (MotionEvent#ACTION_UP, MotionEvent#ACTION_CANCEL) -> SETTLING
200     // SETTLING -> (MotionEvent#ACTION_DOWN) -> DRAGGING
201     // SETTLING -> (View settled) -> IDLE
202 
setState(ScrollState newState)203     private void setState(ScrollState newState) {
204         if (mIsSettingState) {
205             mSetStateQueue.add(() -> setState(newState));
206             return;
207         }
208         mIsSettingState = true;
209 
210         if (DBG) {
211             Log.d(TAG, "setState:" + mState + "->" + newState);
212         }
213         // onDragStart and onDragEnd is reported ONLY on state transition
214         if (newState == ScrollState.DRAGGING) {
215             initializeDragging();
216             if (mState == ScrollState.IDLE) {
217                 reportDragStart(false /* recatch */);
218             } else if (mState == ScrollState.SETTLING) {
219                 reportDragStart(true /* recatch */);
220             }
221         }
222         if (newState == ScrollState.SETTLING) {
223             reportDragEnd();
224         }
225 
226         mState = newState;
227         mIsSettingState = false;
228         if (!mSetStateQueue.isEmpty()) {
229             mSetStateQueue.remove().run();
230         }
231     }
232 
initializeDragging()233     private void initializeDragging() {
234         if (mState == ScrollState.SETTLING && mIgnoreSlopWhenSettling) {
235             mSubtractDisplacement.set(0, 0);
236         } else {
237             mSubtractDisplacement.x = mDisplacement.x > 0 ? mTouchSlop : -mTouchSlop;
238             mSubtractDisplacement.y = mDisplacement.y > 0 ? mTouchSlop : -mTouchSlop;
239         }
240     }
241 
shouldScrollStart(PointF displacement)242     protected abstract boolean shouldScrollStart(PointF displacement);
243 
reportDragStart(boolean recatch)244     private void reportDragStart(boolean recatch) {
245         reportDragStartInternal(recatch);
246         if (DBG) {
247             Log.d(TAG, "onDragStart recatch:" + recatch);
248         }
249     }
250 
reportDragStartInternal(boolean recatch)251     protected abstract void reportDragStartInternal(boolean recatch);
252 
reportDragging(MotionEvent event)253     private void reportDragging(MotionEvent event) {
254         if (mDisplacement != mLastDisplacement) {
255             if (DBG) {
256                 Log.d(TAG, String.format("onDrag disp=%s", mDisplacement));
257             }
258 
259             mLastDisplacement.set(mDisplacement);
260             sTempPoint.set(mDisplacement.x - mSubtractDisplacement.x,
261                     mDisplacement.y - mSubtractDisplacement.y);
262             reportDraggingInternal(sTempPoint, event);
263         }
264     }
265 
reportDraggingInternal(PointF displacement, MotionEvent event)266     protected abstract void reportDraggingInternal(PointF displacement, MotionEvent event);
267 
reportDragEnd()268     private void reportDragEnd() {
269         mVelocityTracker.computeCurrentVelocity(1000, mMaxVelocity);
270         PointF velocity = new PointF(mVelocityTracker.getXVelocity() / 1000,
271                 mVelocityTracker.getYVelocity() / 1000);
272         if (mIsRtl) {
273             velocity.x = -velocity.x;
274         }
275         if (DBG) {
276             Log.d(TAG, String.format("onScrollEnd disp=%.1s, velocity=%.1s",
277                     mDisplacement, velocity));
278         }
279 
280         reportDragEndInternal(velocity);
281     }
282 
reportDragEndInternal(PointF velocity)283     protected abstract void reportDragEndInternal(PointF velocity);
284 }
285