1/*
2 * Copyright (C) 2014 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
17package com.android.systemui.recents.views;
18
19import android.content.Context;
20import android.view.InputDevice;
21import android.view.MotionEvent;
22import android.view.VelocityTracker;
23import android.view.View;
24import android.view.ViewConfiguration;
25import android.view.ViewParent;
26import com.android.systemui.recents.Constants;
27import com.android.systemui.recents.RecentsConfiguration;
28
29/* Handles touch events for a TaskStackView. */
30class TaskStackViewTouchHandler implements SwipeHelper.Callback {
31    static int INACTIVE_POINTER_ID = -1;
32
33    RecentsConfiguration mConfig;
34    TaskStackView mSv;
35    TaskStackViewScroller mScroller;
36    VelocityTracker mVelocityTracker;
37
38    boolean mIsScrolling;
39
40    float mInitialP;
41    float mLastP;
42    float mTotalPMotion;
43    int mInitialMotionX, mInitialMotionY;
44    int mLastMotionX, mLastMotionY;
45    int mActivePointerId = INACTIVE_POINTER_ID;
46    TaskView mActiveTaskView = null;
47
48    int mMinimumVelocity;
49    int mMaximumVelocity;
50    // The scroll touch slop is used to calculate when we start scrolling
51    int mScrollTouchSlop;
52    // The page touch slop is used to calculate when we start swiping
53    float mPagingTouchSlop;
54
55    SwipeHelper mSwipeHelper;
56    boolean mInterceptedBySwipeHelper;
57
58    public TaskStackViewTouchHandler(Context context, TaskStackView sv,
59            RecentsConfiguration config, TaskStackViewScroller scroller) {
60        ViewConfiguration configuration = ViewConfiguration.get(context);
61        mMinimumVelocity = configuration.getScaledMinimumFlingVelocity();
62        mMaximumVelocity = configuration.getScaledMaximumFlingVelocity();
63        mScrollTouchSlop = configuration.getScaledTouchSlop();
64        mPagingTouchSlop = configuration.getScaledPagingTouchSlop();
65        mSv = sv;
66        mScroller = scroller;
67        mConfig = config;
68
69        float densityScale = context.getResources().getDisplayMetrics().density;
70        mSwipeHelper = new SwipeHelper(SwipeHelper.X, this, densityScale, mPagingTouchSlop);
71        mSwipeHelper.setMinAlpha(1f);
72    }
73
74    /** Velocity tracker helpers */
75    void initOrResetVelocityTracker() {
76        if (mVelocityTracker == null) {
77            mVelocityTracker = VelocityTracker.obtain();
78        } else {
79            mVelocityTracker.clear();
80        }
81    }
82    void initVelocityTrackerIfNotExists() {
83        if (mVelocityTracker == null) {
84            mVelocityTracker = VelocityTracker.obtain();
85        }
86    }
87    void recycleVelocityTracker() {
88        if (mVelocityTracker != null) {
89            mVelocityTracker.recycle();
90            mVelocityTracker = null;
91        }
92    }
93
94    /** Returns the view at the specified coordinates */
95    TaskView findViewAtPoint(int x, int y) {
96        int childCount = mSv.getChildCount();
97        for (int i = childCount - 1; i >= 0; i--) {
98            TaskView tv = (TaskView) mSv.getChildAt(i);
99            if (tv.getVisibility() == View.VISIBLE) {
100                if (mSv.isTransformedTouchPointInView(x, y, tv)) {
101                    return tv;
102                }
103            }
104        }
105        return null;
106    }
107
108    /** Constructs a simulated motion event for the current stack scroll. */
109    MotionEvent createMotionEventForStackScroll(MotionEvent ev) {
110        MotionEvent pev = MotionEvent.obtainNoHistory(ev);
111        pev.setLocation(0, mScroller.progressToScrollRange(mScroller.getStackScroll()));
112        return pev;
113    }
114
115    /** Touch preprocessing for handling below */
116    public boolean onInterceptTouchEvent(MotionEvent ev) {
117        // Return early if we have no children
118        boolean hasChildren = (mSv.getChildCount() > 0);
119        if (!hasChildren) {
120            return false;
121        }
122
123        // Pass through to swipe helper if we are swiping
124        mInterceptedBySwipeHelper = mSwipeHelper.onInterceptTouchEvent(ev);
125        if (mInterceptedBySwipeHelper) {
126            return true;
127        }
128
129        boolean wasScrolling = mScroller.isScrolling() ||
130                (mScroller.mScrollAnimator != null && mScroller.mScrollAnimator.isRunning());
131        int action = ev.getAction();
132        switch (action & MotionEvent.ACTION_MASK) {
133            case MotionEvent.ACTION_DOWN: {
134                // Save the touch down info
135                mInitialMotionX = mLastMotionX = (int) ev.getX();
136                mInitialMotionY = mLastMotionY = (int) ev.getY();
137                mInitialP = mLastP = mSv.mLayoutAlgorithm.screenYToCurveProgress(mLastMotionY);
138                mActivePointerId = ev.getPointerId(0);
139                mActiveTaskView = findViewAtPoint(mLastMotionX, mLastMotionY);
140                // Stop the current scroll if it is still flinging
141                mScroller.stopScroller();
142                mScroller.stopBoundScrollAnimation();
143                // Initialize the velocity tracker
144                initOrResetVelocityTracker();
145                mVelocityTracker.addMovement(createMotionEventForStackScroll(ev));
146                break;
147            }
148            case MotionEvent.ACTION_MOVE: {
149                if (mActivePointerId == INACTIVE_POINTER_ID) break;
150
151                // Initialize the velocity tracker if necessary
152                initVelocityTrackerIfNotExists();
153                mVelocityTracker.addMovement(createMotionEventForStackScroll(ev));
154
155                int activePointerIndex = ev.findPointerIndex(mActivePointerId);
156                int y = (int) ev.getY(activePointerIndex);
157                int x = (int) ev.getX(activePointerIndex);
158                if (Math.abs(y - mInitialMotionY) > mScrollTouchSlop) {
159                    // Save the touch move info
160                    mIsScrolling = true;
161                    // Disallow parents from intercepting touch events
162                    final ViewParent parent = mSv.getParent();
163                    if (parent != null) {
164                        parent.requestDisallowInterceptTouchEvent(true);
165                    }
166                }
167
168                mLastMotionX = x;
169                mLastMotionY = y;
170                mLastP = mSv.mLayoutAlgorithm.screenYToCurveProgress(mLastMotionY);
171                break;
172            }
173            case MotionEvent.ACTION_CANCEL:
174            case MotionEvent.ACTION_UP: {
175                // Animate the scroll back if we've cancelled
176                mScroller.animateBoundScroll();
177                // Reset the drag state and the velocity tracker
178                mIsScrolling = false;
179                mActivePointerId = INACTIVE_POINTER_ID;
180                mActiveTaskView = null;
181                mTotalPMotion = 0;
182                recycleVelocityTracker();
183                break;
184            }
185        }
186
187        return wasScrolling || mIsScrolling;
188    }
189
190    /** Handles touch events once we have intercepted them */
191    public boolean onTouchEvent(MotionEvent ev) {
192        // Short circuit if we have no children
193        boolean hasChildren = (mSv.getChildCount() > 0);
194        if (!hasChildren) {
195            return false;
196        }
197
198        // Pass through to swipe helper if we are swiping
199        if (mInterceptedBySwipeHelper && mSwipeHelper.onTouchEvent(ev)) {
200            return true;
201        }
202
203        // Update the velocity tracker
204        initVelocityTrackerIfNotExists();
205
206        int action = ev.getAction();
207        switch (action & MotionEvent.ACTION_MASK) {
208            case MotionEvent.ACTION_DOWN: {
209                // Save the touch down info
210                mInitialMotionX = mLastMotionX = (int) ev.getX();
211                mInitialMotionY = mLastMotionY = (int) ev.getY();
212                mInitialP = mLastP = mSv.mLayoutAlgorithm.screenYToCurveProgress(mLastMotionY);
213                mActivePointerId = ev.getPointerId(0);
214                mActiveTaskView = findViewAtPoint(mLastMotionX, mLastMotionY);
215                // Stop the current scroll if it is still flinging
216                mScroller.stopScroller();
217                mScroller.stopBoundScrollAnimation();
218                // Initialize the velocity tracker
219                initOrResetVelocityTracker();
220                mVelocityTracker.addMovement(createMotionEventForStackScroll(ev));
221                // Disallow parents from intercepting touch events
222                final ViewParent parent = mSv.getParent();
223                if (parent != null) {
224                    parent.requestDisallowInterceptTouchEvent(true);
225                }
226                break;
227            }
228            case MotionEvent.ACTION_POINTER_DOWN: {
229                final int index = ev.getActionIndex();
230                mActivePointerId = ev.getPointerId(index);
231                mLastMotionX = (int) ev.getX(index);
232                mLastMotionY = (int) ev.getY(index);
233                mLastP = mSv.mLayoutAlgorithm.screenYToCurveProgress(mLastMotionY);
234                break;
235            }
236            case MotionEvent.ACTION_MOVE: {
237                if (mActivePointerId == INACTIVE_POINTER_ID) break;
238
239                mVelocityTracker.addMovement(createMotionEventForStackScroll(ev));
240
241                int activePointerIndex = ev.findPointerIndex(mActivePointerId);
242                int x = (int) ev.getX(activePointerIndex);
243                int y = (int) ev.getY(activePointerIndex);
244                int yTotal = Math.abs(y - mInitialMotionY);
245                float curP = mSv.mLayoutAlgorithm.screenYToCurveProgress(y);
246                float deltaP = mLastP - curP;
247                if (!mIsScrolling) {
248                    if (yTotal > mScrollTouchSlop) {
249                        mIsScrolling = true;
250                        // Disallow parents from intercepting touch events
251                        final ViewParent parent = mSv.getParent();
252                        if (parent != null) {
253                            parent.requestDisallowInterceptTouchEvent(true);
254                        }
255                    }
256                }
257                if (mIsScrolling) {
258                    float curStackScroll = mScroller.getStackScroll();
259                    float overScrollAmount = mScroller.getScrollAmountOutOfBounds(curStackScroll + deltaP);
260                    if (Float.compare(overScrollAmount, 0f) != 0) {
261                        // Bound the overscroll to a fixed amount, and inversely scale the y-movement
262                        // relative to how close we are to the max overscroll
263                        float maxOverScroll = mConfig.taskStackOverscrollPct;
264                        deltaP *= (1f - (Math.min(maxOverScroll, overScrollAmount)
265                                / maxOverScroll));
266                    }
267                    mScroller.setStackScroll(curStackScroll + deltaP);
268                }
269                mLastMotionX = x;
270                mLastMotionY = y;
271                mLastP = mSv.mLayoutAlgorithm.screenYToCurveProgress(mLastMotionY);
272                mTotalPMotion += Math.abs(deltaP);
273                break;
274            }
275            case MotionEvent.ACTION_UP: {
276                mVelocityTracker.computeCurrentVelocity(1000, mMaximumVelocity);
277                int velocity = (int) mVelocityTracker.getYVelocity(mActivePointerId);
278                if (mIsScrolling && (Math.abs(velocity) > mMinimumVelocity)) {
279                    float overscrollRangePct = Math.abs((float) velocity / mMaximumVelocity);
280                    int overscrollRange = (int) (Math.min(1f, overscrollRangePct) *
281                            (Constants.Values.TaskStackView.TaskStackMaxOverscrollRange -
282                            Constants.Values.TaskStackView.TaskStackMinOverscrollRange));
283                    mScroller.mScroller.fling(0,
284                            mScroller.progressToScrollRange(mScroller.getStackScroll()),
285                            0, velocity,
286                            0, 0,
287                            mScroller.progressToScrollRange(mSv.mLayoutAlgorithm.mMinScrollP),
288                            mScroller.progressToScrollRange(mSv.mLayoutAlgorithm.mMaxScrollP),
289                            0, Constants.Values.TaskStackView.TaskStackMinOverscrollRange +
290                                    overscrollRange);
291                    // Invalidate to kick off computeScroll
292                    mSv.invalidate();
293                } else if (mScroller.isScrollOutOfBounds()) {
294                    // Animate the scroll back into bounds
295                    mScroller.animateBoundScroll();
296                }
297
298                mActivePointerId = INACTIVE_POINTER_ID;
299                mIsScrolling = false;
300                mTotalPMotion = 0;
301                recycleVelocityTracker();
302                break;
303            }
304            case MotionEvent.ACTION_POINTER_UP: {
305                int pointerIndex = ev.getActionIndex();
306                int pointerId = ev.getPointerId(pointerIndex);
307                if (pointerId == mActivePointerId) {
308                    // Select a new active pointer id and reset the motion state
309                    final int newPointerIndex = (pointerIndex == 0) ? 1 : 0;
310                    mActivePointerId = ev.getPointerId(newPointerIndex);
311                    mLastMotionX = (int) ev.getX(newPointerIndex);
312                    mLastMotionY = (int) ev.getY(newPointerIndex);
313                    mLastP = mSv.mLayoutAlgorithm.screenYToCurveProgress(mLastMotionY);
314                    mVelocityTracker.clear();
315                }
316                break;
317            }
318            case MotionEvent.ACTION_CANCEL: {
319                if (mScroller.isScrollOutOfBounds()) {
320                    // Animate the scroll back into bounds
321                    mScroller.animateBoundScroll();
322                }
323                mActivePointerId = INACTIVE_POINTER_ID;
324                mIsScrolling = false;
325                mTotalPMotion = 0;
326                recycleVelocityTracker();
327                break;
328            }
329        }
330        return true;
331    }
332
333    /** Handles generic motion events */
334    public boolean onGenericMotionEvent(MotionEvent ev) {
335        if ((ev.getSource() & InputDevice.SOURCE_CLASS_POINTER) ==
336                InputDevice.SOURCE_CLASS_POINTER) {
337            int action = ev.getAction();
338            switch (action & MotionEvent.ACTION_MASK) {
339                case MotionEvent.ACTION_SCROLL:
340                    // Find the front most task and scroll the next task to the front
341                    float vScroll = ev.getAxisValue(MotionEvent.AXIS_VSCROLL);
342                    if (vScroll > 0) {
343                        if (mSv.ensureFocusedTask()) {
344                            mSv.focusNextTask(true, false);
345                        }
346                    } else {
347                        if (mSv.ensureFocusedTask()) {
348                            mSv.focusNextTask(false, false);
349                        }
350                    }
351                    return true;
352            }
353        }
354        return false;
355    }
356
357    /**** SwipeHelper Implementation ****/
358
359    @Override
360    public View getChildAtPosition(MotionEvent ev) {
361        return findViewAtPoint((int) ev.getX(), (int) ev.getY());
362    }
363
364    @Override
365    public boolean canChildBeDismissed(View v) {
366        return true;
367    }
368
369    @Override
370    public void onBeginDrag(View v) {
371        TaskView tv = (TaskView) v;
372        // Disable clipping with the stack while we are swiping
373        tv.setClipViewInStack(false);
374        // Disallow touch events from this task view
375        tv.setTouchEnabled(false);
376        // Disallow parents from intercepting touch events
377        final ViewParent parent = mSv.getParent();
378        if (parent != null) {
379            parent.requestDisallowInterceptTouchEvent(true);
380        }
381    }
382
383    @Override
384    public void onSwipeChanged(View v, float delta) {
385        // Do nothing
386    }
387
388    @Override
389    public void onChildDismissed(View v) {
390        TaskView tv = (TaskView) v;
391        // Re-enable clipping with the stack (we will reuse this view)
392        tv.setClipViewInStack(true);
393        // Re-enable touch events from this task view
394        tv.setTouchEnabled(true);
395        // Remove the task view from the stack
396        mSv.onTaskViewDismissed(tv);
397    }
398
399    @Override
400    public void onSnapBackCompleted(View v) {
401        TaskView tv = (TaskView) v;
402        // Re-enable clipping with the stack
403        tv.setClipViewInStack(true);
404        // Re-enable touch events from this task view
405        tv.setTouchEnabled(true);
406    }
407
408    @Override
409    public void onDragCancelled(View v) {
410        // Do nothing
411    }
412}
413