1/*
2 * Copyright (C) 2018 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 */
16package com.android.systemui.qs.touch;
17
18import static android.view.MotionEvent.INVALID_POINTER_ID;
19
20import android.content.Context;
21import android.graphics.PointF;
22import android.support.annotation.NonNull;
23import android.support.annotation.VisibleForTesting;
24import android.util.Log;
25import android.view.MotionEvent;
26import android.view.ViewConfiguration;
27
28/**
29 * One dimensional 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 * Copied from packages/apps/Launcher3/src/com/android/launcher3/touch/SwipeDetector.java
36 */
37public class SwipeDetector {
38
39    private static final boolean DBG = false;
40    private static final String TAG = "SwipeDetector";
41
42    private int mScrollConditions;
43    public static final int DIRECTION_POSITIVE = 1 << 0;
44    public static final int DIRECTION_NEGATIVE = 1 << 1;
45    public static final int DIRECTION_BOTH = DIRECTION_NEGATIVE | DIRECTION_POSITIVE;
46
47    private static final float ANIMATION_DURATION = 1200;
48
49    protected int mActivePointerId = INVALID_POINTER_ID;
50
51    /**
52     * The minimum release velocity in pixels per millisecond that triggers fling..
53     */
54    public static final float RELEASE_VELOCITY_PX_MS = 1.0f;
55
56    /**
57     * The time constant used to calculate dampening in the low-pass filter of scroll velocity.
58     * Cutoff frequency is set at 10 Hz.
59     */
60    public static final float SCROLL_VELOCITY_DAMPENING_RC = 1000f / (2f * (float) Math.PI * 10);
61
62    /* Scroll state, this is set to true during dragging and animation. */
63    private ScrollState mState = ScrollState.IDLE;
64
65    enum ScrollState {
66        IDLE,
67        DRAGGING,      // onDragStart, onDrag
68        SETTLING       // onDragEnd
69    }
70
71    public static abstract class Direction {
72
73        abstract float getDisplacement(MotionEvent ev, int pointerIndex, PointF refPoint);
74
75        /**
76         * Distance in pixels a touch can wander before we think the user is scrolling.
77         */
78        abstract float getActiveTouchSlop(MotionEvent ev, int pointerIndex, PointF downPos);
79    }
80
81    public static final Direction VERTICAL = new Direction() {
82
83        @Override
84        float getDisplacement(MotionEvent ev, int pointerIndex, PointF refPoint) {
85            return ev.getY(pointerIndex) - refPoint.y;
86        }
87
88        @Override
89        float getActiveTouchSlop(MotionEvent ev, int pointerIndex, PointF downPos) {
90            return Math.abs(ev.getX(pointerIndex) - downPos.x);
91        }
92    };
93
94    public static final Direction HORIZONTAL = new Direction() {
95
96        @Override
97        float getDisplacement(MotionEvent ev, int pointerIndex, PointF refPoint) {
98            return ev.getX(pointerIndex) - refPoint.x;
99        }
100
101        @Override
102        float getActiveTouchSlop(MotionEvent ev, int pointerIndex, PointF downPos) {
103            return Math.abs(ev.getY(pointerIndex) - downPos.y);
104        }
105    };
106
107    //------------------- ScrollState transition diagram -----------------------------------
108    //
109    // IDLE ->      (mDisplacement > mTouchSlop) -> DRAGGING
110    // DRAGGING -> (MotionEvent#ACTION_UP, MotionEvent#ACTION_CANCEL) -> SETTLING
111    // SETTLING -> (MotionEvent#ACTION_DOWN) -> DRAGGING
112    // SETTLING -> (View settled) -> IDLE
113
114    private void setState(ScrollState newState) {
115        if (DBG) {
116            Log.d(TAG, "setState:" + mState + "->" + newState);
117        }
118        // onDragStart and onDragEnd is reported ONLY on state transition
119        if (newState == ScrollState.DRAGGING) {
120            initializeDragging();
121            if (mState == ScrollState.IDLE) {
122                reportDragStart(false /* recatch */);
123            } else if (mState == ScrollState.SETTLING) {
124                reportDragStart(true /* recatch */);
125            }
126        }
127        if (newState == ScrollState.SETTLING) {
128            reportDragEnd();
129        }
130
131        mState = newState;
132    }
133
134    public boolean isDraggingOrSettling() {
135        return mState == ScrollState.DRAGGING || mState == ScrollState.SETTLING;
136    }
137
138    /**
139     * There's no touch and there's no animation.
140     */
141    public boolean isIdleState() {
142        return mState == ScrollState.IDLE;
143    }
144
145    public boolean isSettlingState() {
146        return mState == ScrollState.SETTLING;
147    }
148
149    public boolean isDraggingState() {
150        return mState == ScrollState.DRAGGING;
151    }
152
153    private final PointF mDownPos = new PointF();
154    private final PointF mLastPos = new PointF();
155    private final Direction mDir;
156
157    private final float mTouchSlop;
158
159    /* Client of this gesture detector can register a callback. */
160    private final Listener mListener;
161
162    private long mCurrentMillis;
163
164    private float mVelocity;
165    private float mLastDisplacement;
166    private float mDisplacement;
167
168    private float mSubtractDisplacement;
169    private boolean mIgnoreSlopWhenSettling;
170
171    public interface Listener {
172        void onDragStart(boolean start);
173
174        boolean onDrag(float displacement, float velocity);
175
176        void onDragEnd(float velocity, boolean fling);
177    }
178
179    public SwipeDetector(@NonNull Context context, @NonNull Listener l, @NonNull Direction dir) {
180        this(ViewConfiguration.get(context).getScaledTouchSlop(), l, dir);
181    }
182
183    @VisibleForTesting
184    protected SwipeDetector(float touchSlope, @NonNull Listener l, @NonNull Direction dir) {
185        mTouchSlop = touchSlope;
186        mListener = l;
187        mDir = dir;
188    }
189
190    public void setDetectableScrollConditions(int scrollDirectionFlags, boolean ignoreSlop) {
191        mScrollConditions = scrollDirectionFlags;
192        mIgnoreSlopWhenSettling = ignoreSlop;
193    }
194
195    private boolean shouldScrollStart(MotionEvent ev, int pointerIndex) {
196        // reject cases where the angle or slop condition is not met.
197        if (Math.max(mDir.getActiveTouchSlop(ev, pointerIndex, mDownPos), mTouchSlop)
198                > Math.abs(mDisplacement)) {
199            return false;
200        }
201
202        // Check if the client is interested in scroll in current direction.
203        if (((mScrollConditions & DIRECTION_NEGATIVE) > 0 && mDisplacement > 0) ||
204                ((mScrollConditions & DIRECTION_POSITIVE) > 0 && mDisplacement < 0)) {
205            return true;
206        }
207        return false;
208    }
209
210    public boolean onTouchEvent(MotionEvent ev) {
211        switch (ev.getActionMasked()) {
212            case MotionEvent.ACTION_DOWN:
213                mActivePointerId = ev.getPointerId(0);
214                mDownPos.set(ev.getX(), ev.getY());
215                mLastPos.set(mDownPos);
216                mLastDisplacement = 0;
217                mDisplacement = 0;
218                mVelocity = 0;
219
220                if (mState == ScrollState.SETTLING && mIgnoreSlopWhenSettling) {
221                    setState(ScrollState.DRAGGING);
222                }
223                break;
224            //case MotionEvent.ACTION_POINTER_DOWN:
225            case MotionEvent.ACTION_POINTER_UP:
226                int ptrIdx = ev.getActionIndex();
227                int ptrId = ev.getPointerId(ptrIdx);
228                if (ptrId == mActivePointerId) {
229                    final int newPointerIdx = ptrIdx == 0 ? 1 : 0;
230                    mDownPos.set(
231                            ev.getX(newPointerIdx) - (mLastPos.x - mDownPos.x),
232                            ev.getY(newPointerIdx) - (mLastPos.y - mDownPos.y));
233                    mLastPos.set(ev.getX(newPointerIdx), ev.getY(newPointerIdx));
234                    mActivePointerId = ev.getPointerId(newPointerIdx);
235                }
236                break;
237            case MotionEvent.ACTION_MOVE:
238                int pointerIndex = ev.findPointerIndex(mActivePointerId);
239                if (pointerIndex == INVALID_POINTER_ID) {
240                    break;
241                }
242                mDisplacement = mDir.getDisplacement(ev, pointerIndex, mDownPos);
243                computeVelocity(mDir.getDisplacement(ev, pointerIndex, mLastPos),
244                        ev.getEventTime());
245
246                // handle state and listener calls.
247                if (mState != ScrollState.DRAGGING && shouldScrollStart(ev, pointerIndex)) {
248                    setState(ScrollState.DRAGGING);
249                }
250                if (mState == ScrollState.DRAGGING) {
251                    reportDragging();
252                }
253                mLastPos.set(ev.getX(pointerIndex), ev.getY(pointerIndex));
254                break;
255            case MotionEvent.ACTION_CANCEL:
256            case MotionEvent.ACTION_UP:
257                // These are synthetic events and there is no need to update internal values.
258                if (mState == ScrollState.DRAGGING) {
259                    setState(ScrollState.SETTLING);
260                }
261                break;
262            default:
263                break;
264        }
265        return true;
266    }
267
268    public void finishedScrolling() {
269        setState(ScrollState.IDLE);
270    }
271
272    private boolean reportDragStart(boolean recatch) {
273        mListener.onDragStart(!recatch);
274        if (DBG) {
275            Log.d(TAG, "onDragStart recatch:" + recatch);
276        }
277        return true;
278    }
279
280    private void initializeDragging() {
281        if (mState == ScrollState.SETTLING && mIgnoreSlopWhenSettling) {
282            mSubtractDisplacement = 0;
283        }
284        if (mDisplacement > 0) {
285            mSubtractDisplacement = mTouchSlop;
286        } else {
287            mSubtractDisplacement = -mTouchSlop;
288        }
289    }
290
291    private boolean reportDragging() {
292        if (mDisplacement != mLastDisplacement) {
293            if (DBG) {
294                Log.d(TAG, String.format("onDrag disp=%.1f, velocity=%.1f",
295                        mDisplacement, mVelocity));
296            }
297
298            mLastDisplacement = mDisplacement;
299            return mListener.onDrag(mDisplacement - mSubtractDisplacement, mVelocity);
300        }
301        return true;
302    }
303
304    private void reportDragEnd() {
305        if (DBG) {
306            Log.d(TAG, String.format("onScrollEnd disp=%.1f, velocity=%.1f",
307                    mDisplacement, mVelocity));
308        }
309        mListener.onDragEnd(mVelocity, Math.abs(mVelocity) > RELEASE_VELOCITY_PX_MS);
310
311    }
312
313    /**
314     * Computes the damped velocity.
315     */
316    public float computeVelocity(float delta, long currentMillis) {
317        long previousMillis = mCurrentMillis;
318        mCurrentMillis = currentMillis;
319
320        float deltaTimeMillis = mCurrentMillis - previousMillis;
321        float velocity = (deltaTimeMillis > 0) ? (delta / deltaTimeMillis) : 0;
322        if (Math.abs(mVelocity) < 0.001f) {
323            mVelocity = velocity;
324        } else {
325            float alpha = computeDampeningFactor(deltaTimeMillis);
326            mVelocity = interpolate(mVelocity, velocity, alpha);
327        }
328        return mVelocity;
329    }
330
331    /**
332     * Returns a time-dependent dampening factor using delta time.
333     */
334    private static float computeDampeningFactor(float deltaTime) {
335        return deltaTime / (SCROLL_VELOCITY_DAMPENING_RC + deltaTime);
336    }
337
338    /**
339     * Returns the linear interpolation between two values
340     */
341    private static float interpolate(float from, float to, float alpha) {
342        return (1.0f - alpha) * from + alpha * to;
343    }
344
345    public static long calculateDuration(float velocity, float progressNeeded) {
346        // TODO: make these values constants after tuning.
347        float velocityDivisor = Math.max(2f, Math.abs(0.5f * velocity));
348        float travelDistance = Math.max(0.2f, progressNeeded);
349        long duration = (long) Math.max(100, ANIMATION_DURATION / velocityDivisor * travelDistance);
350        if (DBG) {
351            Log.d(TAG, String.format("calculateDuration=%d, v=%f, d=%f", duration, velocity, progressNeeded));
352        }
353        return duration;
354    }
355}
356
357