TaskStackViewTouchHandler.java revision ebfc6981828b0699eef85c58b23a61f2cac41af3
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;
26import com.android.systemui.recents.RecentsConfiguration;
27
28/* Handles touch events for a TaskStackView. */
29class TaskStackViewTouchHandler implements SwipeHelper.Callback {
30    static int INACTIVE_POINTER_ID = -1;
31
32    RecentsConfiguration mConfig;
33    TaskStackView mSv;
34    TaskStackViewScroller mScroller;
35    VelocityTracker mVelocityTracker;
36
37    boolean mIsScrolling;
38
39    float mInitialP;
40    float mLastP;
41    float mTotalPMotion;
42    int mInitialMotionX, mInitialMotionY;
43    int mLastMotionX, mLastMotionY;
44    int mActivePointerId = INACTIVE_POINTER_ID;
45    TaskView mActiveTaskView = null;
46
47    int mMinimumVelocity;
48    int mMaximumVelocity;
49    // The scroll touch slop is used to calculate when we start scrolling
50    int mScrollTouchSlop;
51    // The page touch slop is used to calculate when we start swiping
52    float mPagingTouchSlop;
53
54    SwipeHelper mSwipeHelper;
55    boolean mInterceptedBySwipeHelper;
56
57    public TaskStackViewTouchHandler(Context context, TaskStackView sv,
58            RecentsConfiguration config, TaskStackViewScroller scroller) {
59        ViewConfiguration configuration = ViewConfiguration.get(context);
60        mMinimumVelocity = configuration.getScaledMinimumFlingVelocity();
61        mMaximumVelocity = configuration.getScaledMaximumFlingVelocity();
62        mScrollTouchSlop = configuration.getScaledTouchSlop();
63        mPagingTouchSlop = configuration.getScaledPagingTouchSlop();
64        mSv = sv;
65        mScroller = scroller;
66        mConfig = config;
67
68        float densityScale = context.getResources().getDisplayMetrics().density;
69        mSwipeHelper = new SwipeHelper(SwipeHelper.X, this, densityScale, mPagingTouchSlop);
70        mSwipeHelper.setMinAlpha(1f);
71    }
72
73    /** Velocity tracker helpers */
74    void initOrResetVelocityTracker() {
75        if (mVelocityTracker == null) {
76            mVelocityTracker = VelocityTracker.obtain();
77        } else {
78            mVelocityTracker.clear();
79        }
80    }
81    void initVelocityTrackerIfNotExists() {
82        if (mVelocityTracker == null) {
83            mVelocityTracker = VelocityTracker.obtain();
84        }
85    }
86    void recycleVelocityTracker() {
87        if (mVelocityTracker != null) {
88            mVelocityTracker.recycle();
89            mVelocityTracker = null;
90        }
91    }
92
93    /** Returns the view at the specified coordinates */
94    TaskView findViewAtPoint(int x, int y) {
95        int childCount = mSv.getChildCount();
96        for (int i = childCount - 1; i >= 0; i--) {
97            TaskView tv = (TaskView) mSv.getChildAt(i);
98            if (tv.getVisibility() == View.VISIBLE) {
99                if (mSv.isTransformedTouchPointInView(x, y, tv)) {
100                    return tv;
101                }
102            }
103        }
104        return null;
105    }
106
107    /** Constructs a simulated motion event for the current stack scroll. */
108    MotionEvent createMotionEventForStackScroll(MotionEvent ev) {
109        MotionEvent pev = MotionEvent.obtainNoHistory(ev);
110        pev.setLocation(0, mScroller.progressToScrollRange(mScroller.getStackScroll()));
111        return pev;
112    }
113
114    /** Touch preprocessing for handling below */
115    public boolean onInterceptTouchEvent(MotionEvent ev) {
116        // Return early if we have no children
117        boolean hasChildren = (mSv.getChildCount() > 0);
118        if (!hasChildren) {
119            return false;
120        }
121
122        // Pass through to swipe helper if we are swiping
123        mInterceptedBySwipeHelper = mSwipeHelper.onInterceptTouchEvent(ev);
124        if (mInterceptedBySwipeHelper) {
125            return true;
126        }
127
128        boolean wasScrolling = mScroller.isScrolling() ||
129                (mScroller.mScrollAnimator != null && mScroller.mScrollAnimator.isRunning());
130        int action = ev.getAction();
131        switch (action & MotionEvent.ACTION_MASK) {
132            case MotionEvent.ACTION_DOWN: {
133                // Save the touch down info
134                mInitialMotionX = mLastMotionX = (int) ev.getX();
135                mInitialMotionY = mLastMotionY = (int) ev.getY();
136                mInitialP = mLastP = mSv.mLayoutAlgorithm.screenYToCurveProgress(mLastMotionY);
137                mActivePointerId = ev.getPointerId(0);
138                mActiveTaskView = findViewAtPoint(mLastMotionX, mLastMotionY);
139                // Stop the current scroll if it is still flinging
140                mScroller.stopScroller();
141                mScroller.stopBoundScrollAnimation();
142                // Initialize the velocity tracker
143                initOrResetVelocityTracker();
144                mVelocityTracker.addMovement(createMotionEventForStackScroll(ev));
145                // Check if the scroller is finished yet
146                mIsScrolling = mScroller.isScrolling();
147                break;
148            }
149            case MotionEvent.ACTION_MOVE: {
150                if (mActivePointerId == INACTIVE_POINTER_ID) break;
151
152                int activePointerIndex = ev.findPointerIndex(mActivePointerId);
153                int y = (int) ev.getY(activePointerIndex);
154                int x = (int) ev.getX(activePointerIndex);
155                if (Math.abs(y - mInitialMotionY) > mScrollTouchSlop) {
156                    // Save the touch move info
157                    mIsScrolling = true;
158                    // Initialize the velocity tracker if necessary
159                    initVelocityTrackerIfNotExists();
160                    mVelocityTracker.addMovement(createMotionEventForStackScroll(ev));
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
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    /**** SwipeHelper Implementation ****/
340
341    @Override
342    public View getChildAtPosition(MotionEvent ev) {
343        return findViewAtPoint((int) ev.getX(), (int) ev.getY());
344    }
345
346    @Override
347    public boolean canChildBeDismissed(View v) {
348        return true;
349    }
350
351    @Override
352    public void onBeginDrag(View v) {
353        TaskView tv = (TaskView) v;
354        // Disable clipping with the stack while we are swiping
355        tv.setClipViewInStack(false);
356        // Disallow touch events from this task view
357        tv.setTouchEnabled(false);
358        // Hide the footer
359        tv.animateFooterVisibility(false, mSv.mConfig.taskViewLockToAppShortAnimDuration);
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        // Re-enable clipping with the stack (we will reuse this view)
376        tv.setClipViewInStack(true);
377        // Re-enable touch events from this task view
378        tv.setTouchEnabled(true);
379        // Remove the task view from the stack
380        mSv.onTaskViewDismissed(tv);
381    }
382
383    @Override
384    public void onSnapBackCompleted(View v) {
385        TaskView tv = (TaskView) v;
386        // Re-enable clipping with the stack
387        tv.setClipViewInStack(true);
388        // Re-enable touch events from this task view
389        tv.setTouchEnabled(true);
390        // Restore the footer
391        tv.animateFooterVisibility(true, mSv.mConfig.taskViewLockToAppShortAnimDuration);
392    }
393
394    @Override
395    public void onDragCancelled(View v) {
396        // Do nothing
397    }
398}
399