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