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