TaskStackViewTouchHandler.java revision 00d947bda0b9a2ea133dfd7b0e445245a0e2c23c
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                final VelocityTracker velocityTracker = mVelocityTracker;
277                velocityTracker.computeCurrentVelocity(1000, mMaximumVelocity);
278                int velocity = (int) velocityTracker.getYVelocity(mActivePointerId);
279                if (mIsScrolling && (Math.abs(velocity) > mMinimumVelocity)) {
280                    int overscrollRange = (int) (Math.min(1f,
281                            Math.abs((float) velocity / mMaximumVelocity)) *
282                            Constants.Values.TaskStackView.TaskStackOverscrollRange);
283                    // Fling scroll
284                    mScroller.mScroller.fling(0, mScroller.progressToScrollRange(mScroller.getStackScroll()),
285                            0, velocity,
286                            0, 0,
287                            mScroller.progressToScrollRange(mSv.mLayoutAlgorithm.mMinScrollP),
288                            mScroller.progressToScrollRange(mSv.mLayoutAlgorithm.mMaxScrollP),
289                            0, overscrollRange);
290                    // Invalidate to kick off computeScroll
291                    mSv.invalidate();
292                } else if (mScroller.isScrollOutOfBounds()) {
293                    // Animate the scroll back into bounds
294                    mScroller.animateBoundScroll();
295                }
296
297                mActivePointerId = INACTIVE_POINTER_ID;
298                mIsScrolling = false;
299                mTotalPMotion = 0;
300                recycleVelocityTracker();
301                break;
302            }
303            case MotionEvent.ACTION_POINTER_UP: {
304                int pointerIndex = ev.getActionIndex();
305                int pointerId = ev.getPointerId(pointerIndex);
306                if (pointerId == mActivePointerId) {
307                    // Select a new active pointer id and reset the motion state
308                    final int newPointerIndex = (pointerIndex == 0) ? 1 : 0;
309                    mActivePointerId = ev.getPointerId(newPointerIndex);
310                    mLastMotionX = (int) ev.getX(newPointerIndex);
311                    mLastMotionY = (int) ev.getY(newPointerIndex);
312                    mLastP = mSv.mLayoutAlgorithm.screenYToCurveProgress(mLastMotionY);
313                    mVelocityTracker.clear();
314                }
315                break;
316            }
317            case MotionEvent.ACTION_CANCEL: {
318                if (mScroller.isScrollOutOfBounds()) {
319                    // Animate the scroll back into bounds
320                    mScroller.animateBoundScroll();
321                }
322                mActivePointerId = INACTIVE_POINTER_ID;
323                mIsScrolling = false;
324                mTotalPMotion = 0;
325                recycleVelocityTracker();
326                break;
327            }
328        }
329        return true;
330    }
331
332    /** Handles generic motion events */
333    public boolean onGenericMotionEvent(MotionEvent ev) {
334        if ((ev.getSource() & InputDevice.SOURCE_CLASS_POINTER) ==
335                InputDevice.SOURCE_CLASS_POINTER) {
336            int action = ev.getAction();
337            switch (action & MotionEvent.ACTION_MASK) {
338                case MotionEvent.ACTION_SCROLL:
339                    // Find the front most task and scroll the next task to the front
340                    float vScroll = ev.getAxisValue(MotionEvent.AXIS_VSCROLL);
341                    if (vScroll > 0) {
342                        if (mSv.ensureFocusedTask()) {
343                            mSv.focusNextTask(true, false);
344                        }
345                    } else {
346                        if (mSv.ensureFocusedTask()) {
347                            mSv.focusNextTask(false, false);
348                        }
349                    }
350                    return true;
351            }
352        }
353        return false;
354    }
355
356    /**** SwipeHelper Implementation ****/
357
358    @Override
359    public View getChildAtPosition(MotionEvent ev) {
360        return findViewAtPoint((int) ev.getX(), (int) ev.getY());
361    }
362
363    @Override
364    public boolean canChildBeDismissed(View v) {
365        return true;
366    }
367
368    @Override
369    public void onBeginDrag(View v) {
370        TaskView tv = (TaskView) v;
371        // Disable clipping with the stack while we are swiping
372        tv.setClipViewInStack(false);
373        // Disallow touch events from this task view
374        tv.setTouchEnabled(false);
375        // Disallow parents from intercepting touch events
376        final ViewParent parent = mSv.getParent();
377        if (parent != null) {
378            parent.requestDisallowInterceptTouchEvent(true);
379        }
380    }
381
382    @Override
383    public void onSwipeChanged(View v, float delta) {
384        // Do nothing
385    }
386
387    @Override
388    public void onChildDismissed(View v) {
389        TaskView tv = (TaskView) v;
390        // Re-enable clipping with the stack (we will reuse this view)
391        tv.setClipViewInStack(true);
392        // Re-enable touch events from this task view
393        tv.setTouchEnabled(true);
394        // Remove the task view from the stack
395        mSv.onTaskViewDismissed(tv);
396    }
397
398    @Override
399    public void onSnapBackCompleted(View v) {
400        TaskView tv = (TaskView) v;
401        // Re-enable clipping with the stack
402        tv.setClipViewInStack(true);
403        // Re-enable touch events from this task view
404        tv.setTouchEnabled(true);
405    }
406
407    @Override
408    public void onDragCancelled(View v) {
409        // Do nothing
410    }
411}
412