TaskStackViewTouchHandler.java revision 5c9f4b90bf56b242467f0b5b4d2c7c5b71e6a777
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.InputDevice;
21import android.view.MotionEvent;
22import android.view.VelocityTracker;
23import android.view.View;
24import android.view.ViewConfiguration;
25import android.view.ViewParent;
26import com.android.internal.logging.MetricsLogger;
27import com.android.systemui.recents.Constants;
28import com.android.systemui.recents.Recents;
29import com.android.systemui.recents.RecentsConfiguration;
30
31import java.util.List;
32
33/* Handles touch events for a TaskStackView. */
34class TaskStackViewTouchHandler implements SwipeHelper.Callback {
35    static int INACTIVE_POINTER_ID = -1;
36
37    RecentsConfiguration mConfig;
38    TaskStackView mSv;
39    TaskStackViewScroller mScroller;
40    VelocityTracker mVelocityTracker;
41
42    boolean mIsScrolling;
43
44    float mInitialP;
45    float mLastP;
46    float mTotalPMotion;
47    int mInitialMotionX, mInitialMotionY;
48    int mLastMotionX, mLastMotionY;
49    int mActivePointerId = INACTIVE_POINTER_ID;
50    TaskView mActiveTaskView = null;
51
52    int mMinimumVelocity;
53    int mMaximumVelocity;
54    // The scroll touch slop is used to calculate when we start scrolling
55    int mScrollTouchSlop;
56    // The page touch slop is used to calculate when we start swiping
57    float mPagingTouchSlop;
58    // Used to calculate when a tap is outside a task view rectangle.
59    final int mWindowTouchSlop;
60
61    SwipeHelper mSwipeHelper;
62    boolean mInterceptedBySwipeHelper;
63
64    public TaskStackViewTouchHandler(Context context, TaskStackView sv,
65            RecentsConfiguration config, TaskStackViewScroller scroller) {
66        ViewConfiguration configuration = ViewConfiguration.get(context);
67        mMinimumVelocity = configuration.getScaledMinimumFlingVelocity();
68        mMaximumVelocity = configuration.getScaledMaximumFlingVelocity();
69        mScrollTouchSlop = configuration.getScaledTouchSlop();
70        mPagingTouchSlop = configuration.getScaledPagingTouchSlop();
71        mWindowTouchSlop = configuration.getScaledWindowTouchSlop();
72        mSv = sv;
73        mScroller = scroller;
74        mConfig = config;
75
76        float densityScale = context.getResources().getDisplayMetrics().density;
77        mSwipeHelper = new SwipeHelper(SwipeHelper.X, this, densityScale, mPagingTouchSlop);
78        mSwipeHelper.setMinAlpha(1f);
79    }
80
81    /** Velocity tracker helpers */
82    void initOrResetVelocityTracker() {
83        if (mVelocityTracker == null) {
84            mVelocityTracker = VelocityTracker.obtain();
85        } else {
86            mVelocityTracker.clear();
87        }
88    }
89    void initVelocityTrackerIfNotExists() {
90        if (mVelocityTracker == null) {
91            mVelocityTracker = VelocityTracker.obtain();
92        }
93    }
94    void recycleVelocityTracker() {
95        if (mVelocityTracker != null) {
96            mVelocityTracker.recycle();
97            mVelocityTracker = null;
98        }
99    }
100
101    /** Returns the view at the specified coordinates */
102    TaskView findViewAtPoint(int x, int y) {
103        List<TaskView> taskViews = mSv.getTaskViews();
104        int taskViewCount = taskViews.size();
105        for (int i = taskViewCount - 1; i >= 0; i--) {
106            TaskView tv = taskViews.get(i);
107            if (tv.getVisibility() == View.VISIBLE) {
108                if (mSv.isTransformedTouchPointInView(x, y, tv)) {
109                    return tv;
110                }
111            }
112        }
113        return null;
114    }
115
116    /** Constructs a simulated motion event for the current stack scroll. */
117    MotionEvent createMotionEventForStackScroll(MotionEvent ev) {
118        MotionEvent pev = MotionEvent.obtainNoHistory(ev);
119        pev.setLocation(0, mScroller.progressToScrollRange(mScroller.getStackScroll()));
120        return pev;
121    }
122
123    /** Touch preprocessing for handling below */
124    public boolean onInterceptTouchEvent(MotionEvent ev) {
125        // Return early if we have no children
126        boolean hasTaskViews = (mSv.getTaskViews().size() > 0);
127        if (!hasTaskViews) {
128            return false;
129        }
130
131        int action = ev.getAction();
132        if (mConfig.multiStackEnabled) {
133            // Check if we are within the bounds of the stack view contents
134            if ((action & MotionEvent.ACTION_MASK) == MotionEvent.ACTION_DOWN) {
135                if (!mSv.getTouchableRegion().contains((int) ev.getX(), (int) ev.getY())) {
136                    return false;
137                }
138            }
139        }
140
141        // Pass through to swipe helper if we are swiping
142        mInterceptedBySwipeHelper = mSwipeHelper.onInterceptTouchEvent(ev);
143        if (mInterceptedBySwipeHelper) {
144            return true;
145        }
146
147        boolean wasScrolling = mScroller.isScrolling() ||
148                (mScroller.mScrollAnimator != null && mScroller.mScrollAnimator.isRunning());
149        switch (action & MotionEvent.ACTION_MASK) {
150            case MotionEvent.ACTION_DOWN: {
151                // Save the touch down info
152                mInitialMotionX = mLastMotionX = (int) ev.getX();
153                mInitialMotionY = mLastMotionY = (int) ev.getY();
154                mInitialP = mLastP = mSv.mLayoutAlgorithm.screenYToCurveProgress(mLastMotionY);
155                mActivePointerId = ev.getPointerId(0);
156                mActiveTaskView = findViewAtPoint(mLastMotionX, mLastMotionY);
157                // Stop the current scroll if it is still flinging
158                mScroller.stopScroller();
159                mScroller.stopBoundScrollAnimation();
160                // Initialize the velocity tracker
161                initOrResetVelocityTracker();
162                mVelocityTracker.addMovement(createMotionEventForStackScroll(ev));
163                break;
164            }
165            case MotionEvent.ACTION_MOVE: {
166                if (mActivePointerId == INACTIVE_POINTER_ID) break;
167
168                // Initialize the velocity tracker if necessary
169                initVelocityTrackerIfNotExists();
170                mVelocityTracker.addMovement(createMotionEventForStackScroll(ev));
171
172                int activePointerIndex = ev.findPointerIndex(mActivePointerId);
173                int y = (int) ev.getY(activePointerIndex);
174                int x = (int) ev.getX(activePointerIndex);
175                if (Math.abs(y - mInitialMotionY) > mScrollTouchSlop) {
176                    // Save the touch move info
177                    mIsScrolling = true;
178                    // Disallow parents from intercepting touch events
179                    final ViewParent parent = mSv.getParent();
180                    if (parent != null) {
181                        parent.requestDisallowInterceptTouchEvent(true);
182                    }
183                }
184
185                mLastMotionX = x;
186                mLastMotionY = y;
187                mLastP = mSv.mLayoutAlgorithm.screenYToCurveProgress(mLastMotionY);
188                break;
189            }
190            case MotionEvent.ACTION_CANCEL:
191            case MotionEvent.ACTION_UP: {
192                // Animate the scroll back if we've cancelled
193                mScroller.animateBoundScroll();
194                // Reset the drag state and the velocity tracker
195                mIsScrolling = false;
196                mActivePointerId = INACTIVE_POINTER_ID;
197                mActiveTaskView = null;
198                mTotalPMotion = 0;
199                recycleVelocityTracker();
200                break;
201            }
202        }
203
204        return wasScrolling || mIsScrolling;
205    }
206
207    /** Handles touch events once we have intercepted them */
208    public boolean onTouchEvent(MotionEvent ev) {
209        // Short circuit if we have no children
210        boolean hasTaskViews = (mSv.getTaskViews().size() > 0);
211        if (!hasTaskViews) {
212            return false;
213        }
214
215        int action = ev.getAction();
216        if (mConfig.multiStackEnabled) {
217            // Check if we are within the bounds of the stack view contents
218            if ((action & MotionEvent.ACTION_MASK) == MotionEvent.ACTION_DOWN) {
219                if (!mSv.getTouchableRegion().contains((int) ev.getX(), (int) ev.getY())) {
220                    return false;
221                }
222            }
223        }
224
225        // Pass through to swipe helper if we are swiping
226        if (mInterceptedBySwipeHelper && mSwipeHelper.onTouchEvent(ev)) {
227            return true;
228        }
229
230        // Update the velocity tracker
231        initVelocityTrackerIfNotExists();
232
233        switch (action & MotionEvent.ACTION_MASK) {
234            case MotionEvent.ACTION_DOWN: {
235                // Save the touch down info
236                mInitialMotionX = mLastMotionX = (int) ev.getX();
237                mInitialMotionY = mLastMotionY = (int) ev.getY();
238                mInitialP = mLastP = mSv.mLayoutAlgorithm.screenYToCurveProgress(mLastMotionY);
239                mActivePointerId = ev.getPointerId(0);
240                mActiveTaskView = findViewAtPoint(mLastMotionX, mLastMotionY);
241                // Stop the current scroll if it is still flinging
242                mScroller.stopScroller();
243                mScroller.stopBoundScrollAnimation();
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                break;
253            }
254            case MotionEvent.ACTION_POINTER_DOWN: {
255                final int index = ev.getActionIndex();
256                mActivePointerId = ev.getPointerId(index);
257                mLastMotionX = (int) ev.getX(index);
258                mLastMotionY = (int) ev.getY(index);
259                mLastP = mSv.mLayoutAlgorithm.screenYToCurveProgress(mLastMotionY);
260                break;
261            }
262            case MotionEvent.ACTION_MOVE: {
263                if (mActivePointerId == INACTIVE_POINTER_ID) break;
264
265                mVelocityTracker.addMovement(createMotionEventForStackScroll(ev));
266
267                int activePointerIndex = ev.findPointerIndex(mActivePointerId);
268                int x = (int) ev.getX(activePointerIndex);
269                int y = (int) ev.getY(activePointerIndex);
270                int yTotal = Math.abs(y - mInitialMotionY);
271                float curP = mSv.mLayoutAlgorithm.screenYToCurveProgress(y);
272                float deltaP = mLastP - curP;
273                if (!mIsScrolling) {
274                    if (yTotal > mScrollTouchSlop) {
275                        mIsScrolling = true;
276                        // Disallow parents from intercepting touch events
277                        final ViewParent parent = mSv.getParent();
278                        if (parent != null) {
279                            parent.requestDisallowInterceptTouchEvent(true);
280                        }
281                    }
282                }
283                if (mIsScrolling) {
284                    float curStackScroll = mScroller.getStackScroll();
285                    float overScrollAmount = mScroller.getScrollAmountOutOfBounds(curStackScroll + deltaP);
286                    if (Float.compare(overScrollAmount, 0f) != 0) {
287                        // Bound the overscroll to a fixed amount, and inversely scale the y-movement
288                        // relative to how close we are to the max overscroll
289                        float maxOverScroll = mConfig.taskStackOverscrollPct;
290                        deltaP *= (1f - (Math.min(maxOverScroll, overScrollAmount)
291                                / maxOverScroll));
292                    }
293                    mScroller.setStackScroll(curStackScroll + deltaP);
294                }
295                mLastMotionX = x;
296                mLastMotionY = y;
297                mLastP = mSv.mLayoutAlgorithm.screenYToCurveProgress(mLastMotionY);
298                mTotalPMotion += Math.abs(deltaP);
299                break;
300            }
301            case MotionEvent.ACTION_UP: {
302                mVelocityTracker.computeCurrentVelocity(1000, mMaximumVelocity);
303                int velocity = (int) mVelocityTracker.getYVelocity(mActivePointerId);
304                if (mIsScrolling && (Math.abs(velocity) > mMinimumVelocity)) {
305                    float overscrollRangePct = Math.abs((float) velocity / mMaximumVelocity);
306                    int overscrollRange = (int) (Math.min(1f, overscrollRangePct) *
307                            (Constants.Values.TaskStackView.TaskStackMaxOverscrollRange -
308                                    Constants.Values.TaskStackView.TaskStackMinOverscrollRange));
309                    mScroller.mScroller.fling(0,
310                            mScroller.progressToScrollRange(mScroller.getStackScroll()),
311                            0, velocity,
312                            0, 0,
313                            mScroller.progressToScrollRange(mSv.mLayoutAlgorithm.mMinScrollP),
314                            mScroller.progressToScrollRange(mSv.mLayoutAlgorithm.mMaxScrollP),
315                            0, Constants.Values.TaskStackView.TaskStackMinOverscrollRange +
316                                    overscrollRange);
317                    // Invalidate to kick off computeScroll
318                    mSv.invalidate();
319                } else if (mScroller.isScrollOutOfBounds()) {
320                    // Animate the scroll back into bounds
321                    mScroller.animateBoundScroll();
322                } else if (mActiveTaskView == null) {
323                    // This tap didn't start on a task.
324                    maybeHideRecentsFromBackgroundTap((int) ev.getX(), (int) ev.getY());
325                }
326
327                mActivePointerId = INACTIVE_POINTER_ID;
328                mIsScrolling = false;
329                mTotalPMotion = 0;
330                recycleVelocityTracker();
331                break;
332            }
333            case MotionEvent.ACTION_POINTER_UP: {
334                int pointerIndex = ev.getActionIndex();
335                int pointerId = ev.getPointerId(pointerIndex);
336                if (pointerId == mActivePointerId) {
337                    // Select a new active pointer id and reset the motion state
338                    final int newPointerIndex = (pointerIndex == 0) ? 1 : 0;
339                    mActivePointerId = ev.getPointerId(newPointerIndex);
340                    mLastMotionX = (int) ev.getX(newPointerIndex);
341                    mLastMotionY = (int) ev.getY(newPointerIndex);
342                    mLastP = mSv.mLayoutAlgorithm.screenYToCurveProgress(mLastMotionY);
343                    mVelocityTracker.clear();
344                }
345                break;
346            }
347            case MotionEvent.ACTION_CANCEL: {
348                if (mScroller.isScrollOutOfBounds()) {
349                    // Animate the scroll back into bounds
350                    mScroller.animateBoundScroll();
351                }
352                mActivePointerId = INACTIVE_POINTER_ID;
353                mIsScrolling = false;
354                mTotalPMotion = 0;
355                recycleVelocityTracker();
356                break;
357            }
358        }
359        return true;
360    }
361
362    /** Hides recents if the up event at (x, y) is a tap on the background area. */
363    void maybeHideRecentsFromBackgroundTap(int x, int y) {
364        // Ignore the up event if it's too far from its start position. The user might have been
365        // trying to scroll or swipe.
366        int dx = Math.abs(mInitialMotionX - x);
367        int dy = Math.abs(mInitialMotionY - y);
368        if (dx > mScrollTouchSlop || dy > mScrollTouchSlop) {
369            return;
370        }
371
372        // Shift the tap position toward the center of the task stack and check to see if it would
373        // have hit a view. The user might have tried to tap on a task and missed slightly.
374        int shiftedX = x;
375        if (x > mSv.getTouchableRegion().centerX()) {
376            shiftedX -= mWindowTouchSlop;
377        } else {
378            shiftedX += mWindowTouchSlop;
379        }
380        if (findViewAtPoint(shiftedX, y) != null) {
381            return;
382        }
383
384        // The user intentionally tapped on the background, which is like a tap on the "desktop".
385        // Hide recents and transition to the launcher.
386        Recents recents = Recents.getInstanceAndStartIfNeeded(mSv.getContext());
387        recents.hideRecents(false /* altTab */, true /* homeKey */);
388    }
389
390    /** Handles generic motion events */
391    public boolean onGenericMotionEvent(MotionEvent ev) {
392        if ((ev.getSource() & InputDevice.SOURCE_CLASS_POINTER) ==
393                InputDevice.SOURCE_CLASS_POINTER) {
394            int action = ev.getAction();
395            switch (action & MotionEvent.ACTION_MASK) {
396                case MotionEvent.ACTION_SCROLL:
397                    // Find the front most task and scroll the next task to the front
398                    float vScroll = ev.getAxisValue(MotionEvent.AXIS_VSCROLL);
399                    if (vScroll > 0) {
400                        if (mSv.ensureFocusedTask(true)) {
401                            mSv.focusNextTask(true, false);
402                        }
403                    } else {
404                        if (mSv.ensureFocusedTask(true)) {
405                            mSv.focusNextTask(false, false);
406                        }
407                    }
408                    return true;
409            }
410        }
411        return false;
412    }
413
414    /**** SwipeHelper Implementation ****/
415
416    @Override
417    public View getChildAtPosition(MotionEvent ev) {
418        return findViewAtPoint((int) ev.getX(), (int) ev.getY());
419    }
420
421    @Override
422    public boolean canChildBeDismissed(View v) {
423        return true;
424    }
425
426    @Override
427    public void onBeginDrag(View v) {
428        TaskView tv = (TaskView) v;
429        // Disable clipping with the stack while we are swiping
430        tv.setClipViewInStack(false);
431        // Disallow touch events from this task view
432        tv.setTouchEnabled(false);
433        // Disallow parents from intercepting touch events
434        final ViewParent parent = mSv.getParent();
435        if (parent != null) {
436            parent.requestDisallowInterceptTouchEvent(true);
437        }
438        // Fade out the dismiss button
439        mSv.hideDismissAllButton(null);
440    }
441
442    @Override
443    public void onSwipeChanged(View v, float delta) {
444        // Do nothing
445    }
446
447    @Override
448    public void onChildDismissed(View v) {
449        TaskView tv = (TaskView) v;
450        // Re-enable clipping with the stack (we will reuse this view)
451        tv.setClipViewInStack(true);
452        // Re-enable touch events from this task view
453        tv.setTouchEnabled(true);
454        // Remove the task view from the stack
455        mSv.onTaskViewDismissed(tv);
456        // Keep track of deletions by keyboard
457        MetricsLogger.histogram(tv.getContext(), "overview_task_dismissed_source",
458                Constants.Metrics.DismissSourceSwipeGesture);
459    }
460
461    @Override
462    public void onSnapBackCompleted(View v) {
463        TaskView tv = (TaskView) v;
464        // Re-enable clipping with the stack
465        tv.setClipViewInStack(true);
466        // Re-enable touch events from this task view
467        tv.setTouchEnabled(true);
468        // Restore the dismiss button
469        mSv.showDismissAllButton();
470    }
471
472    @Override
473    public void onDragCancelled(View v) {
474        // Do nothing
475    }
476}
477