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