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