TaskStackViewTouchHandler.java revision b99b18e78e8aa9a27596c13ea05a188ab43c7e12
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
189        // Short circuit if we have no children
190        boolean hasChildren = (mSv.getChildCount() > 0);
191        if (!hasChildren) {
192            return false;
193        }
194
195        // Pass through to swipe helper if we are swiping
196        if (mInterceptedBySwipeHelper && mSwipeHelper.onTouchEvent(ev)) {
197            return true;
198        }
199
200        // Update the velocity tracker
201        initVelocityTrackerIfNotExists();
202
203        int action = ev.getAction();
204        switch (action & MotionEvent.ACTION_MASK) {
205            case MotionEvent.ACTION_DOWN: {
206                // Save the touch down info
207                mInitialMotionX = mLastMotionX = (int) ev.getX();
208                mInitialMotionY = mLastMotionY = (int) ev.getY();
209                mInitialP = mLastP = mSv.mLayoutAlgorithm.screenYToCurveProgress(mLastMotionY);
210                mActivePointerId = ev.getPointerId(0);
211                mActiveTaskView = findViewAtPoint(mLastMotionX, mLastMotionY);
212                // Stop the current scroll if it is still flinging
213                mScroller.stopScroller();
214                mScroller.stopBoundScrollAnimation();
215                // Initialize the velocity tracker
216                initOrResetVelocityTracker();
217                mVelocityTracker.addMovement(createMotionEventForStackScroll(ev));
218                // Disallow parents from intercepting touch events
219                final ViewParent parent = mSv.getParent();
220                if (parent != null) {
221                    parent.requestDisallowInterceptTouchEvent(true);
222                }
223                break;
224            }
225            case MotionEvent.ACTION_POINTER_DOWN: {
226                final int index = ev.getActionIndex();
227                mActivePointerId = ev.getPointerId(index);
228                mLastMotionX = (int) ev.getX(index);
229                mLastMotionY = (int) ev.getY(index);
230                mLastP = mSv.mLayoutAlgorithm.screenYToCurveProgress(mLastMotionY);
231                break;
232            }
233            case MotionEvent.ACTION_MOVE: {
234                if (mActivePointerId == INACTIVE_POINTER_ID) break;
235
236                int activePointerIndex = ev.findPointerIndex(mActivePointerId);
237                int x = (int) ev.getX(activePointerIndex);
238                int y = (int) ev.getY(activePointerIndex);
239                int yTotal = Math.abs(y - mInitialMotionY);
240                float curP = mSv.mLayoutAlgorithm.screenYToCurveProgress(y);
241                float deltaP = mLastP - curP;
242                if (!mIsScrolling) {
243                    if (yTotal > mScrollTouchSlop) {
244                        mIsScrolling = true;
245                        // Initialize the velocity tracker
246                        initOrResetVelocityTracker();
247                        mVelocityTracker.addMovement(createMotionEventForStackScroll(ev));
248                        // Disallow parents from intercepting touch events
249                        final ViewParent parent = mSv.getParent();
250                        if (parent != null) {
251                            parent.requestDisallowInterceptTouchEvent(true);
252                        }
253                    }
254                }
255                if (mIsScrolling) {
256                    float curStackScroll = mScroller.getStackScroll();
257                    float overScrollAmount = mScroller.getScrollAmountOutOfBounds(curStackScroll + deltaP);
258                    if (Float.compare(overScrollAmount, 0f) != 0) {
259                        // Bound the overscroll to a fixed amount, and inversely scale the y-movement
260                        // relative to how close we are to the max overscroll
261                        float maxOverScroll = 0.25f;
262                        deltaP *= (1f - (Math.min(maxOverScroll, overScrollAmount)
263                                / maxOverScroll));
264                    }
265                    mScroller.setStackScroll(curStackScroll + deltaP);
266                    if (mScroller.isScrollOutOfBounds()) {
267                        mVelocityTracker.clear();
268                    } else {
269                        mVelocityTracker.addMovement(createMotionEventForStackScroll(ev));
270                    }
271                }
272                mLastMotionX = x;
273                mLastMotionY = y;
274                mLastP = mSv.mLayoutAlgorithm.screenYToCurveProgress(mLastMotionY);
275                mTotalPMotion += Math.abs(deltaP);
276                break;
277            }
278            case MotionEvent.ACTION_UP: {
279                final VelocityTracker velocityTracker = mVelocityTracker;
280                velocityTracker.computeCurrentVelocity(1000, mMaximumVelocity);
281                int velocity = (int) velocityTracker.getYVelocity(mActivePointerId);
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