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