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