TaskStackViewTouchHandler.java revision 85cfec811e35025dbde54f4dc09fe0e1337c36b8
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                    // Enable HW layers
152                    mSv.addHwLayersRefCount("stackScroll");
153                }
154
155                mLastMotionX = x;
156                mLastMotionY = y;
157                break;
158            }
159            case MotionEvent.ACTION_CANCEL:
160            case MotionEvent.ACTION_UP: {
161                // Animate the scroll back if we've cancelled
162                mSv.animateBoundScroll();
163                // Disable HW layers
164                if (mIsScrolling) {
165                    mSv.decHwLayersRefCount("stackScroll");
166                }
167                // Reset the drag state and the velocity tracker
168                mIsScrolling = false;
169                mActivePointerId = INACTIVE_POINTER_ID;
170                mActiveTaskView = null;
171                mTotalScrollMotion = 0;
172                recycleVelocityTracker();
173                break;
174            }
175        }
176
177        return wasScrolling || mIsScrolling;
178    }
179
180    /** Handles touch events once we have intercepted them */
181    public boolean onTouchEvent(MotionEvent ev) {
182        // Short circuit if we have no children
183        boolean hasChildren = (mSv.getChildCount() > 0);
184        if (!hasChildren) {
185            return false;
186        }
187
188        // Pass through to swipe helper if we are swiping
189        if (mInterceptedBySwipeHelper && mSwipeHelper.onTouchEvent(ev)) {
190            return true;
191        }
192
193        // Update the velocity tracker
194        initVelocityTrackerIfNotExists();
195        mVelocityTracker.addMovement(ev);
196
197        int action = ev.getAction();
198        switch (action & MotionEvent.ACTION_MASK) {
199            case MotionEvent.ACTION_DOWN: {
200                // Save the touch down info
201                mInitialMotionX = mLastMotionX = (int) ev.getX();
202                mInitialMotionY = mLastMotionY = (int) ev.getY();
203                mActivePointerId = ev.getPointerId(0);
204                mActiveTaskView = findViewAtPoint(mLastMotionX, mLastMotionY);
205                // Stop the current scroll if it is still flinging
206                mSv.abortScroller();
207                mSv.abortBoundScrollAnimation();
208                // Initialize the velocity tracker
209                initOrResetVelocityTracker();
210                mVelocityTracker.addMovement(ev);
211                // Disallow parents from intercepting touch events
212                final ViewParent parent = mSv.getParent();
213                if (parent != null) {
214                    parent.requestDisallowInterceptTouchEvent(true);
215                }
216                break;
217            }
218            case MotionEvent.ACTION_POINTER_DOWN: {
219                final int index = ev.getActionIndex();
220                mActivePointerId = ev.getPointerId(index);
221                mLastMotionX = (int) ev.getX(index);
222                mLastMotionY = (int) ev.getY(index);
223                break;
224            }
225            case MotionEvent.ACTION_MOVE: {
226                if (mActivePointerId == INACTIVE_POINTER_ID) break;
227
228                int activePointerIndex = ev.findPointerIndex(mActivePointerId);
229                int x = (int) ev.getX(activePointerIndex);
230                int y = (int) ev.getY(activePointerIndex);
231                int yTotal = Math.abs(y - mInitialMotionY);
232                int deltaY = mLastMotionY - y;
233                if (!mIsScrolling) {
234                    if (yTotal > mScrollTouchSlop) {
235                        mIsScrolling = true;
236                        // Initialize the velocity tracker
237                        initOrResetVelocityTracker();
238                        mVelocityTracker.addMovement(ev);
239                        // Disallow parents from intercepting touch events
240                        final ViewParent parent = mSv.getParent();
241                        if (parent != null) {
242                            parent.requestDisallowInterceptTouchEvent(true);
243                        }
244                        // Enable HW layers
245                        mSv.addHwLayersRefCount("stackScroll");
246                    }
247                }
248                if (mIsScrolling) {
249                    int curStackScroll = mSv.getStackScroll();
250                    int overScrollAmount = mSv.getScrollAmountOutOfBounds(curStackScroll + deltaY);
251                    if (overScrollAmount != 0) {
252                        // Bound the overscroll to a fixed amount, and inversely scale the y-movement
253                        // relative to how close we are to the max overscroll
254                        float maxOverScroll = mSv.mStackAlgorithm.mTaskRect.height() / 3f;
255                        deltaY = Math.round(deltaY * (1f - (Math.min(maxOverScroll, overScrollAmount)
256                                / maxOverScroll)));
257                    }
258                    mSv.setStackScroll(curStackScroll + deltaY);
259                    if (mSv.isScrollOutOfBounds()) {
260                        mVelocityTracker.clear();
261                    }
262                }
263                mLastMotionX = x;
264                mLastMotionY = y;
265                mTotalScrollMotion += Math.abs(deltaY);
266                break;
267            }
268            case MotionEvent.ACTION_UP: {
269                final VelocityTracker velocityTracker = mVelocityTracker;
270                velocityTracker.computeCurrentVelocity(1000, mMaximumVelocity);
271                int velocity = (int) velocityTracker.getYVelocity(mActivePointerId);
272
273                if (mIsScrolling && (Math.abs(velocity) > mMinimumVelocity)) {
274                    // Enable HW layers on the stack
275                    mSv.addHwLayersRefCount("flingScroll");
276                    // XXX: Make this animation a function of the velocity AND distance
277                    int overscrollRange = (int) (Math.min(1f,
278                            Math.abs((float) velocity / mMaximumVelocity)) *
279                            Constants.Values.TaskStackView.TaskStackOverscrollRange);
280                    // Fling scroll
281                    mSv.mScroller.fling(0, mSv.getStackScroll(),
282                            0, -velocity,
283                            0, 0,
284                            mSv.mMinScroll, mSv.mMaxScroll,
285                            0, overscrollRange);
286                    // Invalidate to kick off computeScroll
287                    mSv.invalidate(mSv.mStackAlgorithm.mStackRect);
288                } else if (mSv.isScrollOutOfBounds()) {
289                    // Animate the scroll back into bounds
290                    // XXX: Make this animation a function of the velocity OR distance
291                    mSv.animateBoundScroll();
292                }
293
294                if (mIsScrolling) {
295                    // Disable HW layers
296                    mSv.decHwLayersRefCount("stackScroll");
297                }
298                mActivePointerId = INACTIVE_POINTER_ID;
299                mIsScrolling = false;
300                mTotalScrollMotion = 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                    mVelocityTracker.clear();
314                }
315                break;
316            }
317            case MotionEvent.ACTION_CANCEL: {
318                if (mIsScrolling) {
319                    // Disable HW layers
320                    mSv.decHwLayersRefCount("stackScroll");
321                }
322                if (mSv.isScrollOutOfBounds()) {
323                    // Animate the scroll back into bounds
324                    // XXX: Make this animation a function of the velocity OR distance
325                    mSv.animateBoundScroll();
326                }
327                mActivePointerId = INACTIVE_POINTER_ID;
328                mIsScrolling = false;
329                mTotalScrollMotion = 0;
330                recycleVelocityTracker();
331                break;
332            }
333        }
334        return true;
335    }
336
337    /**** SwipeHelper Implementation ****/
338
339    @Override
340    public View getChildAtPosition(MotionEvent ev) {
341        return findViewAtPoint((int) ev.getX(), (int) ev.getY());
342    }
343
344    @Override
345    public boolean canChildBeDismissed(View v) {
346        return true;
347    }
348
349    @Override
350    public void onBeginDrag(View v) {
351        TaskView tv = (TaskView) v;
352        // Disable clipping with the stack while we are swiping
353        tv.setClipViewInStack(false);
354        // Enable HW layers on that task
355        tv.enableHwLayers();
356        // Disallow touch events from this task view
357        tv.setTouchEnabled(false);
358        // Hide the footer
359        tv.animateFooterVisibility(false, mSv.mConfig.taskViewLockToAppShortAnimDuration, 0);
360        // Disallow parents from intercepting touch events
361        final ViewParent parent = mSv.getParent();
362        if (parent != null) {
363            parent.requestDisallowInterceptTouchEvent(true);
364        }
365    }
366
367    @Override
368    public void onSwipeChanged(View v, float delta) {
369        // Do nothing
370    }
371
372    @Override
373    public void onChildDismissed(View v) {
374        TaskView tv = (TaskView) v;
375        // Disable HW layers on that task
376        if (mSv.mHwLayersTrigger.getCount() == 0) {
377            tv.disableHwLayers();
378        }
379        // Re-enable clipping with the stack (we will reuse this view)
380        tv.setClipViewInStack(true);
381        // Remove the task view from the stack
382        mSv.onTaskViewDismissed(tv);
383    }
384
385    @Override
386    public void onSnapBackCompleted(View v) {
387        TaskView tv = (TaskView) v;
388        // Disable HW layers on that task
389        if (mSv.mHwLayersTrigger.getCount() == 0) {
390            tv.disableHwLayers();
391        }
392        // Re-enable clipping with the stack
393        tv.setClipViewInStack(true);
394        // Re-enable touch events from this task view
395        tv.setTouchEnabled(true);
396        // Restore the footer
397        tv.animateFooterVisibility(true, mSv.mConfig.taskViewLockToAppShortAnimDuration, 0);
398    }
399
400    @Override
401    public void onDragCancelled(View v) {
402        // Do nothing
403    }
404}
405