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