1package com.android.launcher3.allapps;
2
3import android.content.Context;
4import android.util.Log;
5import android.view.MotionEvent;
6import android.view.ViewConfiguration;
7import 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 */
15public 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
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
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     */
86    public boolean isIdleState() {
87        return mState == ScrollState.IDLE;
88    }
89
90    public boolean isSettlingState() {
91        return mState == ScrollState.SETTLING;
92    }
93
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
115    public void setListener(Listener l) {
116        mListener = l;
117    }
118
119    public interface Listener {
120        void onDragStart(boolean start);
121
122        boolean onDrag(float displacement, float velocity);
123
124        void onDragEnd(float velocity, boolean fling);
125    }
126
127    public VerticalPullDetector(Context context) {
128        mTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop();
129    }
130
131    public void setDetectableScrollConditions(int scrollDirectionFlags, boolean ignoreSlop) {
132        mScrollConditions = scrollDirectionFlags;
133        mIgnoreSlopWhenSettling = ignoreSlop;
134    }
135
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
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
199    public void finishedScrolling() {
200        setState(ScrollState.IDLE);
201    }
202
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
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
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
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     */
247    private float computeVelocity(MotionEvent to) {
248        return computeVelocity(to.getY() - mLastY, to.getEventTime());
249    }
250
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     */
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     */
276    private static float interpolate(float from, float to, float alpha) {
277        return (1.0f - alpha) * from + alpha * to;
278    }
279
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
295        public void setVelocityAtZero(float velocity) {
296            mSteeper = velocity > FAST_FLING_PX_MS;
297        }
298
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