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