TaskStackViewTouchHandler.java revision 606b3da71ab64e489e8f65e0b1092138dbfaf7b7
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                // Check if the scroller is finished yet
147                mIsScrolling = mScroller.isScrolling();
148                break;
149            }
150            case MotionEvent.ACTION_MOVE: {
151                if (mActivePointerId == INACTIVE_POINTER_ID) break;
152
153                int activePointerIndex = ev.findPointerIndex(mActivePointerId);
154                int y = (int) ev.getY(activePointerIndex);
155                int x = (int) ev.getX(activePointerIndex);
156                if (Math.abs(y - mInitialMotionY) > mScrollTouchSlop) {
157                    // Save the touch move info
158                    mIsScrolling = true;
159                    // Initialize the velocity tracker if necessary
160                    initVelocityTrackerIfNotExists();
161                    mVelocityTracker.addMovement(createMotionEventForStackScroll(ev));
162                    // Disallow parents from intercepting touch events
163                    final ViewParent parent = mSv.getParent();
164                    if (parent != null) {
165                        parent.requestDisallowInterceptTouchEvent(true);
166                    }
167                }
168
169                mLastMotionX = x;
170                mLastMotionY = y;
171                mLastP = mSv.mLayoutAlgorithm.screenYToCurveProgress(mLastMotionY);
172                break;
173            }
174            case MotionEvent.ACTION_CANCEL:
175            case MotionEvent.ACTION_UP: {
176                // Animate the scroll back if we've cancelled
177                mScroller.animateBoundScroll();
178                // Reset the drag state and the velocity tracker
179                mIsScrolling = false;
180                mActivePointerId = INACTIVE_POINTER_ID;
181                mActiveTaskView = null;
182                mTotalPMotion = 0;
183                recycleVelocityTracker();
184                break;
185            }
186        }
187
188        return wasScrolling || mIsScrolling;
189    }
190
191    /** Handles touch events once we have intercepted them */
192    public boolean onTouchEvent(MotionEvent ev) {
193        // Short circuit if we have no children
194        boolean hasChildren = (mSv.getChildCount() > 0);
195        if (!hasChildren) {
196            return false;
197        }
198
199        // Pass through to swipe helper if we are swiping
200        if (mInterceptedBySwipeHelper && mSwipeHelper.onTouchEvent(ev)) {
201            return true;
202        }
203
204        // Update the velocity tracker
205        initVelocityTrackerIfNotExists();
206
207        int action = ev.getAction();
208        switch (action & MotionEvent.ACTION_MASK) {
209            case MotionEvent.ACTION_DOWN: {
210                // Save the touch down info
211                mInitialMotionX = mLastMotionX = (int) ev.getX();
212                mInitialMotionY = mLastMotionY = (int) ev.getY();
213                mInitialP = mLastP = mSv.mLayoutAlgorithm.screenYToCurveProgress(mLastMotionY);
214                mActivePointerId = ev.getPointerId(0);
215                mActiveTaskView = findViewAtPoint(mLastMotionX, mLastMotionY);
216                // Stop the current scroll if it is still flinging
217                mScroller.stopScroller();
218                mScroller.stopBoundScrollAnimation();
219                // Initialize the velocity tracker
220                initOrResetVelocityTracker();
221                mVelocityTracker.addMovement(createMotionEventForStackScroll(ev));
222                // Disallow parents from intercepting touch events
223                final ViewParent parent = mSv.getParent();
224                if (parent != null) {
225                    parent.requestDisallowInterceptTouchEvent(true);
226                }
227                break;
228            }
229            case MotionEvent.ACTION_POINTER_DOWN: {
230                final int index = ev.getActionIndex();
231                mActivePointerId = ev.getPointerId(index);
232                mLastMotionX = (int) ev.getX(index);
233                mLastMotionY = (int) ev.getY(index);
234                mLastP = mSv.mLayoutAlgorithm.screenYToCurveProgress(mLastMotionY);
235                break;
236            }
237            case MotionEvent.ACTION_MOVE: {
238                if (mActivePointerId == INACTIVE_POINTER_ID) break;
239
240                int activePointerIndex = ev.findPointerIndex(mActivePointerId);
241                int x = (int) ev.getX(activePointerIndex);
242                int y = (int) ev.getY(activePointerIndex);
243                int yTotal = Math.abs(y - mInitialMotionY);
244                float curP = mSv.mLayoutAlgorithm.screenYToCurveProgress(y);
245                float deltaP = mLastP - curP;
246                if (!mIsScrolling) {
247                    if (yTotal > mScrollTouchSlop) {
248                        mIsScrolling = true;
249                        // Initialize the velocity tracker
250                        initOrResetVelocityTracker();
251                        mVelocityTracker.addMovement(createMotionEventForStackScroll(ev));
252                        // Disallow parents from intercepting touch events
253                        final ViewParent parent = mSv.getParent();
254                        if (parent != null) {
255                            parent.requestDisallowInterceptTouchEvent(true);
256                        }
257                    }
258                }
259                if (mIsScrolling) {
260                    float curStackScroll = mScroller.getStackScroll();
261                    float overScrollAmount = mScroller.getScrollAmountOutOfBounds(curStackScroll + deltaP);
262                    if (Float.compare(overScrollAmount, 0f) != 0) {
263                        // Bound the overscroll to a fixed amount, and inversely scale the y-movement
264                        // relative to how close we are to the max overscroll
265                        float maxOverScroll = mConfig.taskStackOverscrollPct;
266                        deltaP *= (1f - (Math.min(maxOverScroll, overScrollAmount)
267                                / maxOverScroll));
268                    }
269                    mScroller.setStackScroll(curStackScroll + deltaP);
270                    if (mScroller.isScrollOutOfBounds()) {
271                        mVelocityTracker.clear();
272                    } else {
273                        mVelocityTracker.addMovement(createMotionEventForStackScroll(ev));
274                    }
275                }
276                mLastMotionX = x;
277                mLastMotionY = y;
278                mLastP = mSv.mLayoutAlgorithm.screenYToCurveProgress(mLastMotionY);
279                mTotalPMotion += Math.abs(deltaP);
280                break;
281            }
282            case MotionEvent.ACTION_UP: {
283                final VelocityTracker velocityTracker = mVelocityTracker;
284                velocityTracker.computeCurrentVelocity(1000, mMaximumVelocity);
285                int velocity = (int) velocityTracker.getYVelocity(mActivePointerId);
286                if (mIsScrolling && (Math.abs(velocity) > mMinimumVelocity)) {
287                    int overscrollRange = (int) (Math.min(1f,
288                            Math.abs((float) velocity / mMaximumVelocity)) *
289                            Constants.Values.TaskStackView.TaskStackOverscrollRange);
290                    // Fling scroll
291                    mScroller.mScroller.fling(0, mScroller.progressToScrollRange(mScroller.getStackScroll()),
292                            0, velocity,
293                            0, 0,
294                            mScroller.progressToScrollRange(mSv.mLayoutAlgorithm.mMinScrollP),
295                            mScroller.progressToScrollRange(mSv.mLayoutAlgorithm.mMaxScrollP),
296                            0, overscrollRange);
297                    // Invalidate to kick off computeScroll
298                    mSv.invalidate();
299                } else if (mScroller.isScrollOutOfBounds()) {
300                    // Animate the scroll back into bounds
301                    mScroller.animateBoundScroll();
302                }
303
304                mActivePointerId = INACTIVE_POINTER_ID;
305                mIsScrolling = false;
306                mTotalPMotion = 0;
307                recycleVelocityTracker();
308                break;
309            }
310            case MotionEvent.ACTION_POINTER_UP: {
311                int pointerIndex = ev.getActionIndex();
312                int pointerId = ev.getPointerId(pointerIndex);
313                if (pointerId == mActivePointerId) {
314                    // Select a new active pointer id and reset the motion state
315                    final int newPointerIndex = (pointerIndex == 0) ? 1 : 0;
316                    mActivePointerId = ev.getPointerId(newPointerIndex);
317                    mLastMotionX = (int) ev.getX(newPointerIndex);
318                    mLastMotionY = (int) ev.getY(newPointerIndex);
319                    mLastP = mSv.mLayoutAlgorithm.screenYToCurveProgress(mLastMotionY);
320                    mVelocityTracker.clear();
321                }
322                break;
323            }
324            case MotionEvent.ACTION_CANCEL: {
325                if (mScroller.isScrollOutOfBounds()) {
326                    // Animate the scroll back into bounds
327                    mScroller.animateBoundScroll();
328                }
329                mActivePointerId = INACTIVE_POINTER_ID;
330                mIsScrolling = false;
331                mTotalPMotion = 0;
332                recycleVelocityTracker();
333                break;
334            }
335        }
336        return true;
337    }
338
339    /** Handles generic motion events */
340    public boolean onGenericMotionEvent(MotionEvent ev) {
341        if ((ev.getSource() & InputDevice.SOURCE_CLASS_POINTER) ==
342                InputDevice.SOURCE_CLASS_POINTER) {
343            int action = ev.getAction();
344            switch (action & MotionEvent.ACTION_MASK) {
345                case MotionEvent.ACTION_SCROLL:
346                    // Find the front most task and scroll the next task to the front
347                    float vScroll = ev.getAxisValue(MotionEvent.AXIS_VSCROLL);
348                    if (vScroll > 0) {
349                        if (mSv.ensureFocusedTask()) {
350                            mSv.focusNextTask(true, false);
351                        }
352                    } else {
353                        if (mSv.ensureFocusedTask()) {
354                            mSv.focusNextTask(false, false);
355                        }
356                    }
357                    return true;
358            }
359        }
360        return false;
361    }
362
363    /**** SwipeHelper Implementation ****/
364
365    @Override
366    public View getChildAtPosition(MotionEvent ev) {
367        return findViewAtPoint((int) ev.getX(), (int) ev.getY());
368    }
369
370    @Override
371    public boolean canChildBeDismissed(View v) {
372        return true;
373    }
374
375    @Override
376    public void onBeginDrag(View v) {
377        TaskView tv = (TaskView) v;
378        // Disable clipping with the stack while we are swiping
379        tv.setClipViewInStack(false);
380        // Disallow touch events from this task view
381        tv.setTouchEnabled(false);
382        // Disallow parents from intercepting touch events
383        final ViewParent parent = mSv.getParent();
384        if (parent != null) {
385            parent.requestDisallowInterceptTouchEvent(true);
386        }
387    }
388
389    @Override
390    public void onSwipeChanged(View v, float delta) {
391        // Do nothing
392    }
393
394    @Override
395    public void onChildDismissed(View v) {
396        TaskView tv = (TaskView) v;
397        // Re-enable clipping with the stack (we will reuse this view)
398        tv.setClipViewInStack(true);
399        // Re-enable touch events from this task view
400        tv.setTouchEnabled(true);
401        // Remove the task view from the stack
402        mSv.onTaskViewDismissed(tv);
403    }
404
405    @Override
406    public void onSnapBackCompleted(View v) {
407        TaskView tv = (TaskView) v;
408        // Re-enable clipping with the stack
409        tv.setClipViewInStack(true);
410        // Re-enable touch events from this task view
411        tv.setTouchEnabled(true);
412    }
413
414    @Override
415    public void onDragCancelled(View v) {
416        // Do nothing
417    }
418}
419