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