1 package com.android.launcher3.allapps;
2 
3 import android.content.Context;
4 import android.util.Log;
5 import android.view.MotionEvent;
6 import android.view.ViewConfiguration;
7 import android.view.animation.Interpolator;
8 
9 /**
10  * One dimensional scroll gesture detector for all apps container pull up interaction.
11  * Client (e.g., AllAppsTransitionController) of this class can register a listener.
12  * <p/>
13  * Features that this gesture detector can support.
14  */
15 public class VerticalPullDetector {
16 
17     private static final boolean DBG = false;
18     private static final String TAG = "VerticalPullDetector";
19 
20     private float mTouchSlop;
21 
22     private int mScrollConditions;
23     public static final int DIRECTION_UP = 1 << 0;
24     public static final int DIRECTION_DOWN = 1 << 1;
25     public static final int DIRECTION_BOTH = DIRECTION_DOWN | DIRECTION_UP;
26 
27     private static final float ANIMATION_DURATION = 1200;
28     private static final float FAST_FLING_PX_MS = 10;
29 
30     /**
31      * The minimum release velocity in pixels per millisecond that triggers fling..
32      */
33     public static final float RELEASE_VELOCITY_PX_MS = 1.0f;
34 
35     /**
36      * The time constant used to calculate dampening in the low-pass filter of scroll velocity.
37      * Cutoff frequency is set at 10 Hz.
38      */
39     public static final float SCROLL_VELOCITY_DAMPENING_RC = 1000f / (2f * (float) Math.PI * 10);
40 
41     /* Scroll state, this is set to true during dragging and animation. */
42     private ScrollState mState = ScrollState.IDLE;
43 
44     enum ScrollState {
45         IDLE,
46         DRAGGING,      // onDragStart, onDrag
47         SETTLING       // onDragEnd
48     }
49 
50     ;
51 
52     //------------------- ScrollState transition diagram -----------------------------------
53     //
54     // IDLE ->      (mDisplacement > mTouchSlop) -> DRAGGING
55     // DRAGGING -> (MotionEvent#ACTION_UP, MotionEvent#ACTION_CANCEL) -> SETTLING
56     // SETTLING -> (MotionEvent#ACTION_DOWN) -> DRAGGING
57     // SETTLING -> (View settled) -> IDLE
58 
setState(ScrollState newState)59     private void setState(ScrollState newState) {
60         if (DBG) {
61             Log.d(TAG, "setState:" + mState + "->" + newState);
62         }
63         // onDragStart and onDragEnd is reported ONLY on state transition
64         if (newState == ScrollState.DRAGGING) {
65             initializeDragging();
66             if (mState == ScrollState.IDLE) {
67                 reportDragStart(false /* recatch */);
68             } else if (mState == ScrollState.SETTLING) {
69                 reportDragStart(true /* recatch */);
70             }
71         }
72         if (newState == ScrollState.SETTLING) {
73             reportDragEnd();
74         }
75 
76         mState = newState;
77     }
78 
isDraggingOrSettling()79     public boolean isDraggingOrSettling() {
80         return mState == ScrollState.DRAGGING || mState == ScrollState.SETTLING;
81     }
82 
83     /**
84      * There's no touch and there's no animation.
85      */
isIdleState()86     public boolean isIdleState() {
87         return mState == ScrollState.IDLE;
88     }
89 
isSettlingState()90     public boolean isSettlingState() {
91         return mState == ScrollState.SETTLING;
92     }
93 
isDraggingState()94     public boolean isDraggingState() {
95         return mState == ScrollState.DRAGGING;
96     }
97 
98     private float mDownX;
99     private float mDownY;
100 
101     private float mLastY;
102     private long mCurrentMillis;
103 
104     private float mVelocity;
105     private float mLastDisplacement;
106     private float mDisplacementY;
107     private float mDisplacementX;
108 
109     private float mSubtractDisplacement;
110     private boolean mIgnoreSlopWhenSettling;
111 
112     /* Client of this gesture detector can register a callback. */
113     Listener mListener;
114 
setListener(Listener l)115     public void setListener(Listener l) {
116         mListener = l;
117     }
118 
119     public interface Listener {
onDragStart(boolean start)120         void onDragStart(boolean start);
121 
onDrag(float displacement, float velocity)122         boolean onDrag(float displacement, float velocity);
123 
onDragEnd(float velocity, boolean fling)124         void onDragEnd(float velocity, boolean fling);
125     }
126 
VerticalPullDetector(Context context)127     public VerticalPullDetector(Context context) {
128         mTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop();
129     }
130 
setDetectableScrollConditions(int scrollDirectionFlags, boolean ignoreSlop)131     public void setDetectableScrollConditions(int scrollDirectionFlags, boolean ignoreSlop) {
132         mScrollConditions = scrollDirectionFlags;
133         mIgnoreSlopWhenSettling = ignoreSlop;
134     }
135 
shouldScrollStart()136     private boolean shouldScrollStart() {
137         // reject cases where the slop condition is not met.
138         if (Math.abs(mDisplacementY) < mTouchSlop) {
139             return false;
140         }
141 
142         // reject cases where the angle condition is not met.
143         float deltaY = Math.abs(mDisplacementY);
144         float deltaX = Math.max(Math.abs(mDisplacementX), 1);
145         if (deltaX > deltaY) {
146             return false;
147         }
148         // Check if the client is interested in scroll in current direction.
149         if (((mScrollConditions & DIRECTION_DOWN) > 0 && mDisplacementY > 0) ||
150                 ((mScrollConditions & DIRECTION_UP) > 0 && mDisplacementY < 0)) {
151             return true;
152         }
153         return false;
154     }
155 
onTouchEvent(MotionEvent ev)156     public boolean onTouchEvent(MotionEvent ev) {
157         switch (ev.getAction()) {
158             case MotionEvent.ACTION_DOWN:
159                 mDownX = ev.getX();
160                 mDownY = ev.getY();
161                 mLastDisplacement = 0;
162                 mDisplacementY = 0;
163                 mVelocity = 0;
164 
165                 if (mState == ScrollState.SETTLING && mIgnoreSlopWhenSettling) {
166                     setState(ScrollState.DRAGGING);
167                 }
168                 break;
169             case MotionEvent.ACTION_MOVE:
170                 mDisplacementX = ev.getX() - mDownX;
171                 mDisplacementY = ev.getY() - mDownY;
172                 computeVelocity(ev);
173 
174                 // handle state and listener calls.
175                 if (mState != ScrollState.DRAGGING && shouldScrollStart()) {
176                     setState(ScrollState.DRAGGING);
177                 }
178                 if (mState == ScrollState.DRAGGING) {
179                     reportDragging();
180                 }
181                 break;
182             case MotionEvent.ACTION_CANCEL:
183             case MotionEvent.ACTION_UP:
184                 // These are synthetic events and there is no need to update internal values.
185                 if (mState == ScrollState.DRAGGING) {
186                     setState(ScrollState.SETTLING);
187                 }
188                 break;
189             default:
190                 //TODO: add multi finger tracking by tracking active pointer.
191                 break;
192         }
193         // Do house keeping.
194         mLastDisplacement = mDisplacementY;
195         mLastY = ev.getY();
196         return true;
197     }
198 
finishedScrolling()199     public void finishedScrolling() {
200         setState(ScrollState.IDLE);
201     }
202 
reportDragStart(boolean recatch)203     private boolean reportDragStart(boolean recatch) {
204         mListener.onDragStart(!recatch);
205         if (DBG) {
206             Log.d(TAG, "onDragStart recatch:" + recatch);
207         }
208         return true;
209     }
210 
initializeDragging()211     private void initializeDragging() {
212         if (mState == ScrollState.SETTLING && mIgnoreSlopWhenSettling) {
213             mSubtractDisplacement = 0;
214         }
215         if (mDisplacementY > 0) {
216             mSubtractDisplacement = mTouchSlop;
217         } else {
218             mSubtractDisplacement = -mTouchSlop;
219         }
220     }
221 
reportDragging()222     private boolean reportDragging() {
223         float delta = mDisplacementY - mLastDisplacement;
224         if (delta != 0) {
225             if (DBG) {
226                 Log.d(TAG, String.format("onDrag disp=%.1f, velocity=%.1f",
227                         mDisplacementY, mVelocity));
228             }
229 
230             return mListener.onDrag(mDisplacementY - mSubtractDisplacement, mVelocity);
231         }
232         return true;
233     }
234 
reportDragEnd()235     private void reportDragEnd() {
236         if (DBG) {
237             Log.d(TAG, String.format("onScrollEnd disp=%.1f, velocity=%.1f",
238                     mDisplacementY, mVelocity));
239         }
240         mListener.onDragEnd(mVelocity, Math.abs(mVelocity) > RELEASE_VELOCITY_PX_MS);
241 
242     }
243 
244     /**
245      * Computes the damped velocity using the two motion events and the previous velocity.
246      */
computeVelocity(MotionEvent to)247     private float computeVelocity(MotionEvent to) {
248         return computeVelocity(to.getY() - mLastY, to.getEventTime());
249     }
250 
computeVelocity(float delta, long currentMillis)251     public float computeVelocity(float delta, long currentMillis) {
252         long previousMillis = mCurrentMillis;
253         mCurrentMillis = currentMillis;
254 
255         float deltaTimeMillis = mCurrentMillis - previousMillis;
256         float velocity = (deltaTimeMillis > 0) ? (delta / deltaTimeMillis) : 0;
257         if (Math.abs(mVelocity) < 0.001f) {
258             mVelocity = velocity;
259         } else {
260             float alpha = computeDampeningFactor(deltaTimeMillis);
261             mVelocity = interpolate(mVelocity, velocity, alpha);
262         }
263         return mVelocity;
264     }
265 
266     /**
267      * Returns a time-dependent dampening factor using delta time.
268      */
computeDampeningFactor(float deltaTime)269     private static float computeDampeningFactor(float deltaTime) {
270         return deltaTime / (SCROLL_VELOCITY_DAMPENING_RC + deltaTime);
271     }
272 
273     /**
274      * Returns the linear interpolation between two values
275      */
interpolate(float from, float to, float alpha)276     private static float interpolate(float from, float to, float alpha) {
277         return (1.0f - alpha) * from + alpha * to;
278     }
279 
calculateDuration(float velocity, float progressNeeded)280     public long calculateDuration(float velocity, float progressNeeded) {
281         // TODO: make these values constants after tuning.
282         float velocityDivisor = Math.max(2f, Math.abs(0.5f * velocity));
283         float travelDistance = Math.max(0.2f, progressNeeded);
284         long duration = (long) Math.max(100, ANIMATION_DURATION / velocityDivisor * travelDistance);
285         if (DBG) {
286             Log.d(TAG, String.format("calculateDuration=%d, v=%f, d=%f", duration, velocity, progressNeeded));
287         }
288         return duration;
289     }
290 
291     public static class ScrollInterpolator implements Interpolator {
292 
293         boolean mSteeper;
294 
setVelocityAtZero(float velocity)295         public void setVelocityAtZero(float velocity) {
296             mSteeper = velocity > FAST_FLING_PX_MS;
297         }
298 
getInterpolation(float t)299         public float getInterpolation(float t) {
300             t -= 1.0f;
301             float output = t * t * t;
302             if (mSteeper) {
303                 output *= t * t; // Make interpolation initial slope steeper
304             }
305             return output + 1;
306         }
307     }
308 }
309