TaskStackView.java revision 1e8d71b605b4872e93200706a80a88a3ff25498c
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.animation.Animator;
20import android.animation.AnimatorListenerAdapter;
21import android.animation.ObjectAnimator;
22import android.animation.ValueAnimator;
23import android.app.Activity;
24import android.content.ComponentName;
25import android.content.Context;
26import android.content.Intent;
27import android.graphics.Canvas;
28import android.graphics.Rect;
29import android.graphics.Region;
30import android.util.Pair;
31import android.view.LayoutInflater;
32import android.view.MotionEvent;
33import android.view.VelocityTracker;
34import android.view.View;
35import android.view.ViewConfiguration;
36import android.view.ViewParent;
37import android.widget.FrameLayout;
38import android.widget.OverScroller;
39import com.android.systemui.R;
40import com.android.systemui.recents.Console;
41import com.android.systemui.recents.Constants;
42import com.android.systemui.recents.RecentsConfiguration;
43import com.android.systemui.recents.RecentsPackageMonitor;
44import com.android.systemui.recents.RecentsTaskLoader;
45import com.android.systemui.recents.Utilities;
46import com.android.systemui.recents.model.Task;
47import com.android.systemui.recents.model.TaskStack;
48
49import java.util.ArrayList;
50import java.util.HashMap;
51import java.util.Set;
52
53
54/* The visual representation of a task stack view */
55public class TaskStackView extends FrameLayout implements TaskStack.TaskStackCallbacks,
56        TaskView.TaskViewCallbacks, ViewPool.ViewPoolConsumer<TaskView, Task>,
57        View.OnClickListener, View.OnLongClickListener, RecentsPackageMonitor.PackageCallbacks {
58
59    /** The TaskView callbacks */
60    interface TaskStackViewCallbacks {
61        public void onTaskLaunched(TaskStackView stackView, TaskView tv, TaskStack stack, Task t);
62        public void onTaskAppInfoLaunched(Task t);
63        public void onTaskRemoved(Task t);
64    }
65
66    TaskStack mStack;
67    TaskStackViewTouchHandler mTouchHandler;
68    TaskStackViewCallbacks mCb;
69    ViewPool<TaskView, Task> mViewPool;
70
71    // The various rects that define the stack view
72    Rect mRect = new Rect();
73    Rect mStackRect = new Rect();
74    Rect mStackRectSansPeek = new Rect();
75    Rect mTaskRect = new Rect();
76
77    // The virtual stack scroll that we use for the card layout
78    int mStackScroll;
79    int mMinScroll;
80    int mMaxScroll;
81    int mStashedScroll;
82    int mLastInfoPaneStackScroll;
83    int mFocusedTaskIndex = -1;
84    OverScroller mScroller;
85    ObjectAnimator mScrollAnimator;
86
87    // Optimizations
88    int mHwLayersRefCount;
89    int mStackViewsAnimationDuration;
90    boolean mStackViewsDirty = true;
91    boolean mAwaitingFirstLayout = true;
92    int[] mTmpVisibleRange = new int[2];
93    Rect mTmpRect = new Rect();
94    Rect mTmpRect2 = new Rect();
95    LayoutInflater mInflater;
96
97    public TaskStackView(Context context, TaskStack stack) {
98        super(context);
99        mStack = stack;
100        mStack.setCallbacks(this);
101        mScroller = new OverScroller(context);
102        mTouchHandler = new TaskStackViewTouchHandler(context, this);
103        mViewPool = new ViewPool<TaskView, Task>(context, this);
104        mInflater = LayoutInflater.from(context);
105    }
106
107    /** Sets the callbacks */
108    void setCallbacks(TaskStackViewCallbacks cb) {
109        mCb = cb;
110    }
111
112    /** Requests that the views be synchronized with the model */
113    void requestSynchronizeStackViewsWithModel() {
114        requestSynchronizeStackViewsWithModel(0);
115    }
116    void requestSynchronizeStackViewsWithModel(int duration) {
117        Console.log(Constants.Log.TaskStack.SynchronizeViewsWithModel,
118                "[TaskStackView|requestSynchronize]", "" + duration + "ms", Console.AnsiYellow);
119        if (!mStackViewsDirty) {
120            invalidate();
121        }
122        if (mAwaitingFirstLayout) {
123            // Skip the animation if we are awaiting first layout
124            mStackViewsAnimationDuration = 0;
125        } else {
126            mStackViewsAnimationDuration = Math.max(mStackViewsAnimationDuration, duration);
127        }
128        mStackViewsDirty = true;
129    }
130
131    // XXX: Optimization: Use a mapping of Task -> View
132    private TaskView getChildViewForTask(Task t) {
133        int childCount = getChildCount();
134        for (int i = 0; i < childCount; i++) {
135            TaskView tv = (TaskView) getChildAt(i);
136            if (tv.getTask() == t) {
137                return tv;
138            }
139        }
140        return null;
141    }
142
143    /** Update/get the transform */
144    public TaskViewTransform getStackTransform(int indexInStack, int stackScroll) {
145        TaskViewTransform transform = new TaskViewTransform();
146
147        // Return early if we have an invalid index
148        if (indexInStack < 0) return transform;
149
150        // Map the items to an continuous position relative to the specified scroll
151        int numPeekCards = Constants.Values.TaskStackView.StackPeekNumCards;
152        float overlapHeight = Constants.Values.TaskStackView.StackOverlapPct * mTaskRect.height();
153        float peekHeight = Constants.Values.TaskStackView.StackPeekHeightPct * mStackRect.height();
154        float t = ((indexInStack * overlapHeight) - stackScroll) / overlapHeight;
155        float boundedT = Math.max(t, -(numPeekCards + 1));
156
157        // Set the scale relative to its position
158        float minScale = Constants.Values.TaskStackView.StackPeekMinScale;
159        float scaleRange = 1f - minScale;
160        float scaleInc = scaleRange / numPeekCards;
161        float scale = Math.max(minScale, Math.min(1f, 1f + (boundedT * scaleInc)));
162        float scaleYOffset = ((1f - scale) * mTaskRect.height()) / 2;
163        transform.scale = scale;
164
165        // Set the translation
166        if (boundedT < 0f) {
167            transform.translationY = (int) ((Math.max(-numPeekCards, boundedT) /
168                    numPeekCards) * peekHeight - scaleYOffset);
169        } else {
170            transform.translationY = (int) (boundedT * overlapHeight - scaleYOffset);
171        }
172
173        // Set the alphas
174        transform.dismissAlpha = Math.max(-1f, Math.min(0f, t)) + 1f;
175
176        // Update the rect and visibility
177        transform.rect.set(mTaskRect);
178        if (t < -(numPeekCards + 1)) {
179            transform.visible = false;
180        } else {
181            transform.rect.offset(0, transform.translationY);
182            Utilities.scaleRectAboutCenter(transform.rect, transform.scale);
183            transform.visible = Rect.intersects(mRect, transform.rect);
184        }
185        transform.t = t;
186        return transform;
187    }
188
189    /**
190     * Gets the stack transforms of a list of tasks, and returns the visible range of tasks.
191     */
192    private ArrayList<TaskViewTransform> getStackTransforms(ArrayList<Task> tasks,
193                                                            int stackScroll,
194                                                            int[] visibleRangeOut,
195                                                            boolean boundTranslationsToRect) {
196        // XXX: Optimization: Use binary search to find the visible range
197
198        ArrayList<TaskViewTransform> taskTransforms = new ArrayList<TaskViewTransform>();
199        int taskCount = tasks.size();
200        int firstVisibleIndex = -1;
201        int lastVisibleIndex = -1;
202        for (int i = 0; i < taskCount; i++) {
203            TaskViewTransform transform = getStackTransform(i, stackScroll);
204            taskTransforms.add(transform);
205            if (transform.visible) {
206                if (firstVisibleIndex < 0) {
207                    firstVisibleIndex = i;
208                }
209                lastVisibleIndex = i;
210            }
211
212            if (boundTranslationsToRect) {
213                transform.translationY = Math.min(transform.translationY, mRect.bottom);
214            }
215        }
216        if (visibleRangeOut != null) {
217            visibleRangeOut[0] = firstVisibleIndex;
218            visibleRangeOut[1] = lastVisibleIndex;
219        }
220        return taskTransforms;
221    }
222
223    /** Synchronizes the views with the model */
224    void synchronizeStackViewsWithModel() {
225        Console.log(Constants.Log.TaskStack.SynchronizeViewsWithModel,
226                "[TaskStackView|synchronizeViewsWithModel]",
227                "mStackViewsDirty: " + mStackViewsDirty, Console.AnsiYellow);
228        if (mStackViewsDirty) {
229            // XXX: Consider using TaskViewTransform pool to prevent allocations
230            // XXX: Iterate children views, update transforms and remove all that are not visible
231            //      For all remaining tasks, update transforms and if visible add the view
232
233            // Get all the task transforms
234            int[] visibleRange = mTmpVisibleRange;
235            int stackScroll = getStackScroll();
236            ArrayList<Task> tasks = mStack.getTasks();
237            ArrayList<TaskViewTransform> taskTransforms = getStackTransforms(tasks, stackScroll,
238                    visibleRange, false);
239
240            // Update the visible state of all the tasks
241            int taskCount = tasks.size();
242            for (int i = 0; i < taskCount; i++) {
243                Task task = tasks.get(i);
244                TaskViewTransform transform = taskTransforms.get(i);
245                TaskView tv = getChildViewForTask(task);
246
247                if (transform.visible) {
248                    if (tv == null) {
249                        tv = mViewPool.pickUpViewFromPool(task, task);
250                        // When we are picking up a new view from the view pool, prepare it for any
251                        // following animation by putting it in a reasonable place
252                        if (mStackViewsAnimationDuration > 0 && i != 0) {
253                            int fromIndex = (transform.t < 0) ? (visibleRange[0] - 1) :
254                                    (visibleRange[1] + 1);
255                            tv.updateViewPropertiesToTaskTransform(null,
256                                    getStackTransform(fromIndex, stackScroll), 0);
257                        }
258                    }
259                } else {
260                    if (tv != null) {
261                        mViewPool.returnViewToPool(tv);
262                    }
263                }
264            }
265
266            // Update all the remaining view children
267            // NOTE: We have to iterate in reverse where because we are removing views directly
268            int childCount = getChildCount();
269            for (int i = childCount - 1; i >= 0; i--) {
270                TaskView tv = (TaskView) getChildAt(i);
271                Task task = tv.getTask();
272                int taskIndex = mStack.indexOfTask(task);
273                if (taskIndex < 0 || !taskTransforms.get(taskIndex).visible) {
274                    mViewPool.returnViewToPool(tv);
275                } else {
276                    tv.updateViewPropertiesToTaskTransform(null, taskTransforms.get(taskIndex),
277                            mStackViewsAnimationDuration);
278                }
279            }
280
281            Console.log(Constants.Log.TaskStack.SynchronizeViewsWithModel,
282                    "  [TaskStackView|viewChildren]", "" + getChildCount());
283
284            mStackViewsAnimationDuration = 0;
285            mStackViewsDirty = false;
286        }
287    }
288
289    /** Sets the current stack scroll */
290    public void setStackScroll(int value) {
291        mStackScroll = value;
292        requestSynchronizeStackViewsWithModel();
293
294        // Close any open info panes if the user has scrolled away from them
295        boolean isAnimatingScroll = (mScrollAnimator != null && mScrollAnimator.isRunning());
296        if (mLastInfoPaneStackScroll > -1 && !isAnimatingScroll) {
297            RecentsConfiguration config = RecentsConfiguration.getInstance();
298            if (Math.abs(mStackScroll - mLastInfoPaneStackScroll) >
299                    config.taskStackScrollDismissInfoPaneDistance) {
300                // Close any open info panes
301                closeOpenInfoPanes();
302            }
303        }
304    }
305    /** Sets the current stack scroll without synchronizing the stack view with the model */
306    public void setStackScrollRaw(int value) {
307        mStackScroll = value;
308    }
309
310    /**
311     * Returns the scroll to such that the task transform at that index will have t=0. (If the scroll
312     * is not bounded)
313     */
314    int getStackScrollForTaskIndex(int i) {
315        int taskHeight = mTaskRect.height();
316        return (int) (i * Constants.Values.TaskStackView.StackOverlapPct * taskHeight);
317    }
318
319    /** Gets the current stack scroll */
320    public int getStackScroll() {
321        return mStackScroll;
322    }
323
324    /** Animates the stack scroll into bounds */
325    ObjectAnimator animateBoundScroll() {
326        int curScroll = getStackScroll();
327        int newScroll = Math.max(mMinScroll, Math.min(mMaxScroll, curScroll));
328        if (newScroll != curScroll) {
329            // Enable hw layers on the stack
330            addHwLayersRefCount("animateBoundScroll");
331
332            // Start a new scroll animation
333            animateScroll(curScroll, newScroll, new Runnable() {
334                @Override
335                public void run() {
336                    // Disable hw layers on the stack
337                    decHwLayersRefCount("animateBoundScroll");
338                }
339            });
340        }
341        return mScrollAnimator;
342    }
343
344    /** Animates the stack scroll */
345    void animateScroll(int curScroll, int newScroll, final Runnable postRunnable) {
346        // Abort any current animations
347        abortScroller();
348        abortBoundScrollAnimation();
349
350        mScrollAnimator = ObjectAnimator.ofInt(this, "stackScroll", curScroll, newScroll);
351        mScrollAnimator.setDuration(Utilities.calculateTranslationAnimationDuration(newScroll -
352                curScroll, 250));
353        mScrollAnimator.setInterpolator(RecentsConfiguration.getInstance().defaultBezierInterpolator);
354        mScrollAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
355            @Override
356            public void onAnimationUpdate(ValueAnimator animation) {
357                setStackScroll((Integer) animation.getAnimatedValue());
358            }
359        });
360        mScrollAnimator.addListener(new AnimatorListenerAdapter() {
361            @Override
362            public void onAnimationEnd(Animator animation) {
363                if (postRunnable != null) {
364                    postRunnable.run();
365                }
366                mScrollAnimator.removeAllListeners();
367            }
368        });
369        mScrollAnimator.start();
370    }
371
372    /** Aborts any current stack scrolls */
373    void abortBoundScrollAnimation() {
374        if (mScrollAnimator != null) {
375            mScrollAnimator.cancel();
376        }
377    }
378
379    /** Aborts the scroller and any current fling */
380    void abortScroller() {
381        if (!mScroller.isFinished()) {
382            // Abort the scroller
383            mScroller.abortAnimation();
384            // And disable hw layers on the stack
385            decHwLayersRefCount("flingScroll");
386        }
387    }
388
389    /** Bounds the current scroll if necessary */
390    public boolean boundScroll() {
391        int curScroll = getStackScroll();
392        int newScroll = Math.max(mMinScroll, Math.min(mMaxScroll, curScroll));
393        if (newScroll != curScroll) {
394            setStackScroll(newScroll);
395            return true;
396        }
397        return false;
398    }
399
400    /**
401     * Bounds the current scroll if necessary, but does not synchronize the stack view with the
402     * model.
403     */
404    public boolean boundScrollRaw() {
405        int curScroll = getStackScroll();
406        int newScroll = Math.max(mMinScroll, Math.min(mMaxScroll, curScroll));
407        if (newScroll != curScroll) {
408            setStackScrollRaw(newScroll);
409            return true;
410        }
411        return false;
412    }
413
414
415    /** Returns the amount that the scroll is out of bounds */
416    int getScrollAmountOutOfBounds(int scroll) {
417        if (scroll < mMinScroll) {
418            return mMinScroll - scroll;
419        } else if (scroll > mMaxScroll) {
420            return scroll - mMaxScroll;
421        }
422        return 0;
423    }
424
425    /** Returns whether the specified scroll is out of bounds */
426    boolean isScrollOutOfBounds() {
427        return getScrollAmountOutOfBounds(getStackScroll()) != 0;
428    }
429
430    /** Updates the min and max virtual scroll bounds */
431    void updateMinMaxScroll(boolean boundScrollToNewMinMax) {
432        // Compute the min and max scroll values
433        int numTasks = Math.max(1, mStack.getTaskCount());
434        int taskHeight = mTaskRect.height();
435        int stackHeight = mStackRectSansPeek.height();
436        int maxScrollHeight = taskHeight + (int) ((numTasks - 1) *
437                Constants.Values.TaskStackView.StackOverlapPct * taskHeight);
438
439        if (numTasks <= 1) {
440            // If there is only one task, then center the task in the stack rect (sans peek)
441            mMinScroll = mMaxScroll = -(stackHeight - taskHeight) / 2;
442        } else {
443            mMinScroll = Math.min(stackHeight, maxScrollHeight) - stackHeight;
444            mMaxScroll = maxScrollHeight - stackHeight;
445        }
446
447        // Debug logging
448        if (Constants.Log.UI.MeasureAndLayout) {
449            Console.log("  [TaskStack|minScroll] " + mMinScroll);
450            Console.log("  [TaskStack|maxScroll] " + mMaxScroll);
451        }
452
453        if (boundScrollToNewMinMax) {
454            boundScroll();
455        }
456    }
457
458    /** Closes any open info panes. */
459    boolean closeOpenInfoPanes() {
460        if (!Constants.DebugFlags.App.EnableInfoPane) return false;
461
462        int childCount = getChildCount();
463        for (int i = 0; i < childCount; i++) {
464            TaskView tv = (TaskView) getChildAt(i);
465            if (tv.isInfoPaneVisible()) {
466                tv.hideInfoPane();
467                return true;
468            }
469        }
470        return false;
471    }
472
473    /** Focuses the task at the specified index in the stack */
474    void focusTask(int taskIndex, boolean scrollToNewPosition) {
475        Console.log(Constants.Log.UI.Focus, "[TaskStackView|focusTask]", "" + taskIndex);
476        if (0 <= taskIndex && taskIndex < mStack.getTaskCount()) {
477            mFocusedTaskIndex = taskIndex;
478
479            // Focus the view if possible, otherwise, focus the view after we scroll into position
480            Task t = mStack.getTasks().get(taskIndex);
481            TaskView tv = getChildViewForTask(t);
482            Runnable postScrollRunnable = null;
483            if (tv != null) {
484                tv.setFocusedTask();
485                Console.log(Constants.Log.UI.Focus, "[TaskStackView|focusTask]", "Requesting focus");
486            } else {
487                postScrollRunnable = new Runnable() {
488                    @Override
489                    public void run() {
490                        Task t = mStack.getTasks().get(mFocusedTaskIndex);
491                        TaskView tv = getChildViewForTask(t);
492                        if (tv != null) {
493                            tv.setFocusedTask();
494                            Console.log(Constants.Log.UI.Focus, "[TaskStackView|focusTask]",
495                                    "Requesting focus after scroll animation");
496                        }
497                    }
498                };
499            }
500
501            if (scrollToNewPosition) {
502                // Scroll the view into position
503                int newScroll = Math.max(mMinScroll, Math.min(mMaxScroll,
504                        getStackScrollForTaskIndex(taskIndex)));
505
506                animateScroll(getStackScroll(), newScroll, postScrollRunnable);
507            } else {
508                if (postScrollRunnable != null) {
509                    postScrollRunnable.run();
510                }
511            }
512        }
513    }
514
515    /** Focuses the next task in the stack */
516    void focusNextTask(boolean forward) {
517        Console.log(Constants.Log.UI.Focus, "[TaskStackView|focusNextTask]", "" + mFocusedTaskIndex);
518
519        // Find the next index to focus
520        int numTasks = mStack.getTaskCount();
521        if (mFocusedTaskIndex < 0) {
522            mFocusedTaskIndex = numTasks - 1;
523        }
524        if (0 <= mFocusedTaskIndex && mFocusedTaskIndex < numTasks) {
525            mFocusedTaskIndex = Math.max(0, Math.min(numTasks - 1,
526                    mFocusedTaskIndex + (forward ? -1 : 1)));
527        }
528        focusTask(mFocusedTaskIndex, true);
529    }
530
531    /** Enables the hw layers and increments the hw layer requirement ref count */
532    void addHwLayersRefCount(String reason) {
533        Console.log(Constants.Log.UI.HwLayers,
534                "[TaskStackView|addHwLayersRefCount] refCount: " +
535                        mHwLayersRefCount + "->" + (mHwLayersRefCount + 1) + " " + reason);
536        if (mHwLayersRefCount == 0) {
537            // Enable hw layers on each of the children
538            int childCount = getChildCount();
539            for (int i = 0; i < childCount; i++) {
540                TaskView tv = (TaskView) getChildAt(i);
541                tv.enableHwLayers();
542            }
543        }
544        mHwLayersRefCount++;
545    }
546
547    /** Decrements the hw layer requirement ref count and disables the hw layers when we don't
548        need them anymore. */
549    void decHwLayersRefCount(String reason) {
550        Console.log(Constants.Log.UI.HwLayers,
551                "[TaskStackView|decHwLayersRefCount] refCount: " +
552                        mHwLayersRefCount + "->" + (mHwLayersRefCount - 1) + " " + reason);
553        mHwLayersRefCount--;
554        if (mHwLayersRefCount == 0) {
555            // Disable hw layers on each of the children
556            int childCount = getChildCount();
557            for (int i = 0; i < childCount; i++) {
558                TaskView tv = (TaskView) getChildAt(i);
559                tv.disableHwLayers();
560            }
561        } else if (mHwLayersRefCount < 0) {
562            new Throwable("Invalid hw layers ref count").printStackTrace();
563            Console.logError(getContext(), "Invalid HW layers ref count");
564        }
565    }
566
567    @Override
568    public void computeScroll() {
569        if (mScroller.computeScrollOffset()) {
570            setStackScroll(mScroller.getCurrY());
571            invalidate();
572
573            // If we just finished scrolling, then disable the hw layers
574            if (mScroller.isFinished()) {
575                decHwLayersRefCount("finishedFlingScroll");
576            }
577        }
578    }
579
580    @Override
581    public boolean onInterceptTouchEvent(MotionEvent ev) {
582        return mTouchHandler.onInterceptTouchEvent(ev);
583    }
584
585    @Override
586    public boolean onTouchEvent(MotionEvent ev) {
587        return mTouchHandler.onTouchEvent(ev);
588    }
589
590    @Override
591    public void dispatchDraw(Canvas canvas) {
592        Console.log(Constants.Log.UI.Draw, "[TaskStackView|dispatchDraw]", "",
593                Console.AnsiPurple);
594        synchronizeStackViewsWithModel();
595        super.dispatchDraw(canvas);
596    }
597
598    @Override
599    protected boolean drawChild(Canvas canvas, View child, long drawingTime) {
600        if (Constants.DebugFlags.App.EnableTaskStackClipping) {
601            TaskView tv = (TaskView) child;
602            TaskView nextTv = null;
603            int curIndex = indexOfChild(tv);
604            if ((curIndex > -1) && (curIndex < (getChildCount() - 1))) {
605                // Clip against the next view (if we aren't animating its alpha)
606                nextTv = (TaskView) getChildAt(curIndex + 1);
607                if (nextTv.getAlpha() == 1f) {
608                    Rect curRect = tv.getClippingRect(mTmpRect);
609                    Rect nextRect = nextTv.getClippingRect(mTmpRect2);
610                    RecentsConfiguration config = RecentsConfiguration.getInstance();
611                    // The hit rects are relative to the task view, which needs to be offset by the
612                    // system bar height
613                    curRect.offset(0, config.systemInsets.top);
614                    nextRect.offset(0, config.systemInsets.top);
615                    // Compute the clip region
616                    Region clipRegion = new Region();
617                    clipRegion.op(curRect, Region.Op.UNION);
618                    clipRegion.op(nextRect, Region.Op.DIFFERENCE);
619                    // Clip the canvas
620                    int saveCount = canvas.save(Canvas.CLIP_SAVE_FLAG);
621                    canvas.clipRegion(clipRegion);
622                    boolean invalidate = super.drawChild(canvas, child, drawingTime);
623                    canvas.restoreToCount(saveCount);
624                    return invalidate;
625                }
626            }
627        }
628        return super.drawChild(canvas, child, drawingTime);
629    }
630
631    /** Computes the stack and task rects */
632    public void computeRects(int width, int height, int insetLeft, int insetBottom) {
633        // Note: We let the stack view be the full height because we want the cards to go under the
634        //       navigation bar if possible.  However, the stack rects which we use to calculate
635        //       max scroll, etc. need to take the nav bar into account
636
637        // Compute the stack rects
638        mRect.set(0, 0, width, height);
639        mStackRect.set(mRect);
640        mStackRect.left += insetLeft;
641        mStackRect.bottom -= insetBottom;
642
643        int smallestDimension = Math.min(width, height);
644        int padding = (int) (Constants.Values.TaskStackView.StackPaddingPct * smallestDimension / 2f);
645        if (Constants.DebugFlags.App.EnableSearchLayout) {
646            mStackRect.top += padding;
647            mStackRect.left += padding;
648            mStackRect.right -= padding;
649            mStackRect.bottom -= padding;
650        } else {
651            mStackRect.inset(padding, padding);
652        }
653        mStackRectSansPeek.set(mStackRect);
654        mStackRectSansPeek.top += Constants.Values.TaskStackView.StackPeekHeightPct * mStackRect.height();
655
656        // Compute the task rect
657        int minHeight = (int) (mStackRect.height() -
658                (Constants.Values.TaskStackView.StackPeekHeightPct * mStackRect.height()));
659        int size = Math.min(minHeight, Math.min(mStackRect.width(), mStackRect.height()));
660        int left = mStackRect.left + (mStackRect.width() - size) / 2;
661        mTaskRect.set(left, mStackRectSansPeek.top,
662                left + size, mStackRectSansPeek.top + size);
663
664        // Update the scroll bounds
665        updateMinMaxScroll(false);
666    }
667
668    /**
669     * This is called with the size of the space not including the top or right insets, or the
670     * search bar height in portrait (but including the search bar width in landscape, since we want
671     * to draw under it.
672     */
673    @Override
674    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
675        int width = MeasureSpec.getSize(widthMeasureSpec);
676        int height = MeasureSpec.getSize(heightMeasureSpec);
677        Console.log(Constants.Log.UI.MeasureAndLayout, "[TaskStackView|measure]",
678                "width: " + width + " height: " + height +
679                " awaitingFirstLayout: " + mAwaitingFirstLayout, Console.AnsiGreen);
680
681        // Compute our stack/task rects
682        RecentsConfiguration config = RecentsConfiguration.getInstance();
683        Rect taskStackBounds = new Rect();
684        config.getTaskStackBounds(width, height, taskStackBounds);
685        computeRects(width, height, taskStackBounds.left, config.systemInsets.bottom);
686
687        // Debug logging
688        if (Constants.Log.UI.MeasureAndLayout) {
689            Console.log("  [TaskStack|fullRect] " + mRect);
690            Console.log("  [TaskStack|stackRect] " + mStackRect);
691            Console.log("  [TaskStack|stackRectSansPeek] " + mStackRectSansPeek);
692            Console.log("  [TaskStack|taskRect] " + mTaskRect);
693        }
694
695        // If this is the first layout, then scroll to the front of the stack and synchronize the
696        // stack views immediately
697        if (mAwaitingFirstLayout) {
698            setStackScroll(mMaxScroll);
699            requestSynchronizeStackViewsWithModel();
700            synchronizeStackViewsWithModel();
701
702            // Update the focused task index to be the next item to the top task
703            if (config.launchedFromAltTab) {
704                focusTask(Math.max(0, mStack.getTaskCount() - 2), false);
705            }
706
707            // Animate the task bar of the first task view
708            if (config.launchedWithThumbnailAnimation) {
709                TaskView tv = (TaskView) getChildAt(getChildCount() - 1);
710                if (tv != null) {
711                    tv.animateOnEnterRecents();
712                }
713            }
714        }
715
716        // Measure each of the children
717        int childCount = getChildCount();
718        for (int i = 0; i < childCount; i++) {
719            TaskView t = (TaskView) getChildAt(i);
720            t.measure(MeasureSpec.makeMeasureSpec(mTaskRect.width(), MeasureSpec.EXACTLY),
721                    MeasureSpec.makeMeasureSpec(mTaskRect.height(), MeasureSpec.EXACTLY));
722        }
723
724        setMeasuredDimension(width, height);
725    }
726
727    /**
728     * This is called with the size of the space not including the top or right insets, or the
729     * search bar height in portrait (but including the search bar width in landscape, since we want
730     * to draw under it.
731     */
732    @Override
733    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
734        Console.log(Constants.Log.UI.MeasureAndLayout, "[TaskStackView|layout]",
735                "" + new Rect(left, top, right, bottom), Console.AnsiGreen);
736
737        // Debug logging
738        if (Constants.Log.UI.MeasureAndLayout) {
739            Console.log("  [TaskStack|fullRect] " + mRect);
740            Console.log("  [TaskStack|stackRect] " + mStackRect);
741            Console.log("  [TaskStack|stackRectSansPeek] " + mStackRectSansPeek);
742            Console.log("  [TaskStack|taskRect] " + mTaskRect);
743        }
744
745        // Layout each of the children
746        int childCount = getChildCount();
747        for (int i = 0; i < childCount; i++) {
748            TaskView t = (TaskView) getChildAt(i);
749            t.layout(mTaskRect.left, mStackRectSansPeek.top,
750                    mTaskRect.right, mStackRectSansPeek.top + mTaskRect.height());
751        }
752
753        if (mAwaitingFirstLayout) {
754            mAwaitingFirstLayout = false;
755        }
756    }
757
758    @Override
759    protected void onScrollChanged(int l, int t, int oldl, int oldt) {
760        super.onScrollChanged(l, t, oldl, oldt);
761        requestSynchronizeStackViewsWithModel();
762    }
763
764    public boolean isTransformedTouchPointInView(float x, float y, View child) {
765        return isTransformedTouchPointInView(x, y, child, null);
766    }
767
768    /**** TaskStackCallbacks Implementation ****/
769
770    @Override
771    public void onStackTaskAdded(TaskStack stack, Task t) {
772        requestSynchronizeStackViewsWithModel();
773    }
774
775    @Override
776    public void onStackTaskRemoved(TaskStack stack, Task t) {
777        // Remove the view associated with this task, we can't rely on updateTransforms
778        // to work here because the task is no longer in the list
779        int childCount = getChildCount();
780        for (int i = childCount - 1; i >= 0; i--) {
781            TaskView tv = (TaskView) getChildAt(i);
782            if (tv.getTask() == t) {
783                mViewPool.returnViewToPool(tv);
784                break;
785            }
786        }
787
788        // Update the min/max scroll and animate other task views into their new positions
789        updateMinMaxScroll(true);
790        int movement = (int) (Constants.Values.TaskStackView.StackOverlapPct * mTaskRect.height());
791        requestSynchronizeStackViewsWithModel(Utilities.calculateTranslationAnimationDuration(movement));
792
793        // If there are no remaining tasks, then either unfilter the current stack, or just close
794        // the activity if there are no filtered stacks
795        if (mStack.getTaskCount() == 0) {
796            boolean shouldFinishActivity = true;
797            if (mStack.hasFilteredTasks()) {
798                mStack.unfilterTasks();
799                shouldFinishActivity = (mStack.getTaskCount() == 0);
800            }
801            if (shouldFinishActivity) {
802                Activity activity = (Activity) getContext();
803                activity.finish();
804            }
805        }
806    }
807
808    /**
809     * Creates the animations for all the children views that need to be removed or to move views
810     * to their un/filtered position when we are un/filtering a stack, and returns the duration
811     * for these animations.
812     */
813    int getExitTransformsForFilterAnimation(ArrayList<Task> curTasks,
814                        ArrayList<TaskViewTransform> curTaskTransforms,
815                        ArrayList<Task> tasks, ArrayList<TaskViewTransform> taskTransforms,
816                        HashMap<TaskView, Pair<Integer, TaskViewTransform>> childViewTransformsOut,
817                        ArrayList<TaskView> childrenToRemoveOut,
818                        RecentsConfiguration config) {
819        // Animate all of the existing views out of view (if they are not in the visible range in
820        // the new stack) or to their final positions in the new stack
821        int movement = 0;
822        int childCount = getChildCount();
823        for (int i = 0; i < childCount; i++) {
824            TaskView tv = (TaskView) getChildAt(i);
825            Task task = tv.getTask();
826            int taskIndex = tasks.indexOf(task);
827            TaskViewTransform toTransform;
828
829            // If the view is no longer visible, then we should just animate it out
830            boolean willBeInvisible = taskIndex < 0 || !taskTransforms.get(taskIndex).visible;
831            if (willBeInvisible) {
832                if (taskIndex < 0) {
833                    toTransform = curTaskTransforms.get(curTasks.indexOf(task));
834                } else {
835                    toTransform = new TaskViewTransform(taskTransforms.get(taskIndex));
836                }
837                tv.prepareTaskTransformForFilterTaskVisible(toTransform);
838                childrenToRemoveOut.add(tv);
839            } else {
840                toTransform = taskTransforms.get(taskIndex);
841                // Use the movement of the visible views to calculate the duration of the animation
842                movement = Math.max(movement, Math.abs(toTransform.translationY -
843                        (int) tv.getTranslationY()));
844            }
845            childViewTransformsOut.put(tv, new Pair(0, toTransform));
846        }
847        return Utilities.calculateTranslationAnimationDuration(movement,
848                config.filteringCurrentViewsMinAnimDuration);
849    }
850
851    /**
852     * Creates the animations for all the children views that need to be animated in when we are
853     * un/filtering a stack, and returns the duration for these animations.
854     */
855    int getEnterTransformsForFilterAnimation(ArrayList<Task> tasks,
856                         ArrayList<TaskViewTransform> taskTransforms,
857                         HashMap<TaskView, Pair<Integer, TaskViewTransform>> childViewTransformsOut,
858                         RecentsConfiguration config) {
859        int offset = 0;
860        int movement = 0;
861        int taskCount = tasks.size();
862        for (int i = taskCount - 1; i >= 0; i--) {
863            Task task = tasks.get(i);
864            TaskViewTransform toTransform = taskTransforms.get(i);
865            if (toTransform.visible) {
866                TaskView tv = getChildViewForTask(task);
867                if (tv == null) {
868                    // For views that are not already visible, animate them in
869                    tv = mViewPool.pickUpViewFromPool(task, task);
870
871                    // Compose a new transform to fade and slide the new task in
872                    TaskViewTransform fromTransform = new TaskViewTransform(toTransform);
873                    tv.prepareTaskTransformForFilterTaskHidden(fromTransform);
874                    tv.updateViewPropertiesToTaskTransform(null, fromTransform, 0);
875
876                    int startDelay = offset *
877                            Constants.Values.TaskStackView.FilterStartDelay;
878                    childViewTransformsOut.put(tv, new Pair(startDelay, toTransform));
879
880                    // Use the movement of the new views to calculate the duration of the animation
881                    movement = Math.max(movement,
882                            Math.abs(toTransform.translationY - fromTransform.translationY));
883                    offset++;
884                }
885            }
886        }
887        return Utilities.calculateTranslationAnimationDuration(movement,
888                config.filteringNewViewsMinAnimDuration);
889    }
890
891    /** Orchestrates the animations of the current child views and any new views. */
892    void doFilteringAnimation(ArrayList<Task> curTasks,
893                              ArrayList<TaskViewTransform> curTaskTransforms,
894                              final ArrayList<Task> tasks,
895                              final ArrayList<TaskViewTransform> taskTransforms) {
896        final RecentsConfiguration config = RecentsConfiguration.getInstance();
897
898        // Calculate the transforms to animate out all the existing views if they are not in the
899        // new visible range (or to their final positions in the stack if they are)
900        final ArrayList<TaskView> childrenToRemove = new ArrayList<TaskView>();
901        final HashMap<TaskView, Pair<Integer, TaskViewTransform>> childViewTransforms =
902                new HashMap<TaskView, Pair<Integer, TaskViewTransform>>();
903        int duration = getExitTransformsForFilterAnimation(curTasks, curTaskTransforms, tasks,
904                taskTransforms, childViewTransforms, childrenToRemove, config);
905
906        // If all the current views are in the visible range of the new stack, then don't wait for
907        // views to animate out and animate all the new views into their place
908        final boolean unifyNewViewAnimation = childrenToRemove.isEmpty();
909        if (unifyNewViewAnimation) {
910            int inDuration = getEnterTransformsForFilterAnimation(tasks, taskTransforms,
911                    childViewTransforms, config);
912            duration = Math.max(duration, inDuration);
913        }
914
915        // Animate all the views to their final transforms
916        for (final TaskView tv : childViewTransforms.keySet()) {
917            Pair<Integer, TaskViewTransform> t = childViewTransforms.get(tv);
918            tv.animate().cancel();
919            tv.animate()
920                    .setStartDelay(t.first)
921                    .withEndAction(new Runnable() {
922                        @Override
923                        public void run() {
924                            childViewTransforms.remove(tv);
925                            if (childViewTransforms.isEmpty()) {
926                                // Return all the removed children to the view pool
927                                for (TaskView tv : childrenToRemove) {
928                                    mViewPool.returnViewToPool(tv);
929                                }
930
931                                if (!unifyNewViewAnimation) {
932                                    // For views that are not already visible, animate them in
933                                    childViewTransforms.clear();
934                                    int duration = getEnterTransformsForFilterAnimation(tasks,
935                                            taskTransforms, childViewTransforms, config);
936                                    for (final TaskView tv : childViewTransforms.keySet()) {
937                                        Pair<Integer, TaskViewTransform> t = childViewTransforms.get(tv);
938                                        tv.animate().setStartDelay(t.first);
939                                        tv.updateViewPropertiesToTaskTransform(null, t.second, duration);
940                                    }
941                                }
942                            }
943                        }
944                    });
945            tv.updateViewPropertiesToTaskTransform(null, t.second, duration);
946        }
947    }
948
949    @Override
950    public void onStackFiltered(TaskStack newStack, final ArrayList<Task> curTasks,
951                                Task filteredTask) {
952        // Close any open info panes
953        closeOpenInfoPanes();
954
955        // Stash the scroll and filtered task for us to restore to when we unfilter
956        mStashedScroll = getStackScroll();
957
958        // Calculate the current task transforms
959        ArrayList<TaskViewTransform> curTaskTransforms =
960                getStackTransforms(curTasks, getStackScroll(), null, true);
961
962        // Scroll the item to the top of the stack (sans-peek) rect so that we can see it better
963        updateMinMaxScroll(false);
964        float overlapHeight = Constants.Values.TaskStackView.StackOverlapPct * mTaskRect.height();
965        setStackScrollRaw((int) (newStack.indexOfTask(filteredTask) * overlapHeight));
966        boundScrollRaw();
967
968        // Compute the transforms of the items in the new stack after setting the new scroll
969        final ArrayList<Task> tasks = mStack.getTasks();
970        final ArrayList<TaskViewTransform> taskTransforms =
971                getStackTransforms(mStack.getTasks(), getStackScroll(), null, true);
972
973        // Animate
974        doFilteringAnimation(curTasks, curTaskTransforms, tasks, taskTransforms);
975    }
976
977    @Override
978    public void onStackUnfiltered(TaskStack newStack, final ArrayList<Task> curTasks) {
979        // Close any open info panes
980        closeOpenInfoPanes();
981
982        // Calculate the current task transforms
983        final ArrayList<TaskViewTransform> curTaskTransforms =
984                getStackTransforms(curTasks, getStackScroll(), null, true);
985
986        // Restore the stashed scroll
987        updateMinMaxScroll(false);
988        setStackScrollRaw(mStashedScroll);
989        boundScrollRaw();
990
991        // Compute the transforms of the items in the new stack after restoring the stashed scroll
992        final ArrayList<Task> tasks = mStack.getTasks();
993        final ArrayList<TaskViewTransform> taskTransforms =
994                getStackTransforms(tasks, getStackScroll(), null, true);
995
996        // Animate
997        doFilteringAnimation(curTasks, curTaskTransforms, tasks, taskTransforms);
998
999        // Clear the saved vars
1000        mStashedScroll = 0;
1001    }
1002
1003    /**** ViewPoolConsumer Implementation ****/
1004
1005    @Override
1006    public TaskView createView(Context context) {
1007        Console.log(Constants.Log.ViewPool.PoolCallbacks, "[TaskStackView|createPoolView]");
1008        return (TaskView) mInflater.inflate(R.layout.recents_task_view, this, false);
1009    }
1010
1011    @Override
1012    public void prepareViewToEnterPool(TaskView tv) {
1013        Task task = tv.getTask();
1014        tv.resetViewProperties();
1015        Console.log(Constants.Log.ViewPool.PoolCallbacks, "[TaskStackView|returnToPool]",
1016                tv.getTask() + " tv: " + tv);
1017
1018        // Report that this tasks's data is no longer being used
1019        RecentsTaskLoader loader = RecentsTaskLoader.getInstance();
1020        loader.unloadTaskData(task);
1021
1022        // Detach the view from the hierarchy
1023        detachViewFromParent(tv);
1024
1025        // Disable hw layers on this view
1026        tv.disableHwLayers();
1027    }
1028
1029    @Override
1030    public void prepareViewToLeavePool(TaskView tv, Task prepareData, boolean isNewView) {
1031        Console.log(Constants.Log.ViewPool.PoolCallbacks, "[TaskStackView|leavePool]",
1032                "isNewView: " + isNewView);
1033
1034        // Setup and attach the view to the window
1035        Task task = prepareData;
1036        // We try and rebind the task (this MUST be done before the task filled)
1037        tv.onTaskBound(task);
1038        // Request that this tasks's data be filled
1039        RecentsTaskLoader loader = RecentsTaskLoader.getInstance();
1040        loader.loadTaskData(task);
1041
1042        // Find the index where this task should be placed in the children
1043        int insertIndex = -1;
1044        int childCount = getChildCount();
1045        for (int i = 0; i < childCount; i++) {
1046            Task tvTask = ((TaskView) getChildAt(i)).getTask();
1047            if (mStack.containsTask(task) && (mStack.indexOfTask(task) < mStack.indexOfTask(tvTask))) {
1048                insertIndex = i;
1049                break;
1050            }
1051        }
1052
1053        // Add/attach the view to the hierarchy
1054        Console.log(Constants.Log.ViewPool.PoolCallbacks, "  [TaskStackView|insertIndex]",
1055                "" + insertIndex);
1056        if (isNewView) {
1057            addView(tv, insertIndex);
1058
1059            // Set the callbacks and listeners for this new view
1060            tv.setOnClickListener(this);
1061            if (Constants.DebugFlags.App.EnableInfoPane) {
1062                tv.setOnLongClickListener(this);
1063            }
1064            tv.setCallbacks(this);
1065        } else {
1066            attachViewToParent(tv, insertIndex, tv.getLayoutParams());
1067        }
1068
1069        // Enable hw layers on this view if hw layers are enabled on the stack
1070        if (mHwLayersRefCount > 0) {
1071            tv.enableHwLayers();
1072        }
1073    }
1074
1075    @Override
1076    public boolean hasPreferredData(TaskView tv, Task preferredData) {
1077        return (tv.getTask() == preferredData);
1078    }
1079
1080    /**** TaskViewCallbacks Implementation ****/
1081
1082    @Override
1083    public void onTaskIconClicked(TaskView tv) {
1084        Console.log(Constants.Log.UI.ClickEvents, "[TaskStack|Clicked|Icon]",
1085                tv.getTask() + " is currently filtered: " + mStack.hasFilteredTasks(),
1086                Console.AnsiCyan);
1087        if (Constants.DebugFlags.App.EnableTaskFiltering) {
1088            if (mStack.hasFilteredTasks()) {
1089                mStack.unfilterTasks();
1090            } else {
1091                mStack.filterTasks(tv.getTask());
1092            }
1093        }
1094    }
1095
1096    @Override
1097    public void onTaskInfoPanelShown(TaskView tv) {
1098        // Do nothing
1099    }
1100
1101    @Override
1102    public void onTaskInfoPanelHidden(TaskView tv) {
1103        // Unset the saved scroll
1104        mLastInfoPaneStackScroll = -1;
1105    }
1106
1107    @Override
1108    public void onTaskAppInfoClicked(TaskView tv) {
1109        if (mCb != null) {
1110            mCb.onTaskAppInfoLaunched(tv.getTask());
1111        }
1112    }
1113
1114    @Override
1115    public void onTaskDismissed(TaskView tv) {
1116        Task task = tv.getTask();
1117        // Remove the task from the view
1118        mStack.removeTask(task);
1119        // Notify the callback that we've removed the task and it can clean up after it
1120        mCb.onTaskRemoved(task);
1121    }
1122
1123    /**** View.OnClickListener Implementation ****/
1124
1125    @Override
1126    public void onClick(View v) {
1127        TaskView tv = (TaskView) v;
1128        Task task = tv.getTask();
1129        Console.log(Constants.Log.UI.ClickEvents, "[TaskStack|Clicked|Thumbnail]",
1130                task + " cb: " + mCb);
1131
1132        // Close any open info panes if the user taps on another task
1133        if (closeOpenInfoPanes()) {
1134            return;
1135        }
1136
1137        if (mCb != null) {
1138            mCb.onTaskLaunched(this, tv, mStack, task);
1139        }
1140    }
1141
1142    @Override
1143    public boolean onLongClick(View v) {
1144        if (!Constants.DebugFlags.App.EnableInfoPane) return false;
1145
1146        TaskView tv = (TaskView) v;
1147
1148        // Close any other task info panels if we launch another info pane
1149        closeOpenInfoPanes();
1150
1151        // Scroll the task view so that it is maximally visible
1152        float overlapHeight = Constants.Values.TaskStackView.StackOverlapPct * mTaskRect.height();
1153        int taskIndex = mStack.indexOfTask(tv.getTask());
1154        int curScroll = getStackScroll();
1155        int newScroll = (int) Math.max(mMinScroll, Math.min(mMaxScroll, taskIndex * overlapHeight));
1156        TaskViewTransform transform = getStackTransform(taskIndex, curScroll);
1157        Rect nonOverlapRect = new Rect(transform.rect);
1158        if (taskIndex < (mStack.getTaskCount() - 1)) {
1159            nonOverlapRect.bottom = nonOverlapRect.top + (int) overlapHeight;
1160        }
1161
1162        // XXX: Use HW Layers
1163        if (transform.t < 0f) {
1164            animateScroll(curScroll, newScroll, null);
1165        } else if (nonOverlapRect.bottom > mStackRectSansPeek.bottom) {
1166            // Check if we are out of bounds, if so, just scroll it in such that the bottom of the
1167            // task view is visible
1168            newScroll = curScroll - (mStackRectSansPeek.bottom - nonOverlapRect.bottom);
1169            animateScroll(curScroll, newScroll, null);
1170        }
1171        mLastInfoPaneStackScroll = newScroll;
1172
1173        // Show the info pane for this task view
1174        tv.showInfoPane(new Rect(0, 0, 0, (int) overlapHeight));
1175        return true;
1176    }
1177
1178    /**** RecentsPackageMonitor.PackageCallbacks Implementation ****/
1179
1180    @Override
1181    public void onComponentRemoved(Set<ComponentName> cns) {
1182        RecentsConfiguration config = RecentsConfiguration.getInstance();
1183        // For other tasks, just remove them directly if they no longer exist
1184        ArrayList<Task> tasks = mStack.getTasks();
1185        for (int i = tasks.size() - 1; i >= 0; i--) {
1186            final Task t = tasks.get(i);
1187            if (cns.contains(t.key.baseIntent.getComponent())) {
1188                TaskView tv = getChildViewForTask(t);
1189                if (tv != null) {
1190                    // For visible children, defer removing the task until after the animation
1191                    tv.animateRemoval(new Runnable() {
1192                        @Override
1193                        public void run() {
1194                            mStack.removeTask(t);
1195                        }
1196                    });
1197                } else {
1198                    // Otherwise, remove the task from the stack immediately
1199                    mStack.removeTask(t);
1200                }
1201            }
1202        }
1203    }
1204}
1205
1206/* Handles touch events */
1207class TaskStackViewTouchHandler implements SwipeHelper.Callback {
1208    static int INACTIVE_POINTER_ID = -1;
1209
1210    TaskStackView mSv;
1211    VelocityTracker mVelocityTracker;
1212
1213    boolean mIsScrolling;
1214
1215    int mInitialMotionX, mInitialMotionY;
1216    int mLastMotionX, mLastMotionY;
1217    int mActivePointerId = INACTIVE_POINTER_ID;
1218    TaskView mActiveTaskView = null;
1219
1220    int mTotalScrollMotion;
1221    int mMinimumVelocity;
1222    int mMaximumVelocity;
1223    // The scroll touch slop is used to calculate when we start scrolling
1224    int mScrollTouchSlop;
1225    // The page touch slop is used to calculate when we start swiping
1226    float mPagingTouchSlop;
1227
1228    SwipeHelper mSwipeHelper;
1229    boolean mInterceptedBySwipeHelper;
1230
1231    public TaskStackViewTouchHandler(Context context, TaskStackView sv) {
1232        ViewConfiguration configuration = ViewConfiguration.get(context);
1233        mMinimumVelocity = configuration.getScaledMinimumFlingVelocity();
1234        mMaximumVelocity = configuration.getScaledMaximumFlingVelocity();
1235        mScrollTouchSlop = configuration.getScaledTouchSlop();
1236        mPagingTouchSlop = configuration.getScaledPagingTouchSlop();
1237        mSv = sv;
1238
1239
1240        float densityScale = context.getResources().getDisplayMetrics().density;
1241        mSwipeHelper = new SwipeHelper(SwipeHelper.X, this, densityScale, mPagingTouchSlop);
1242        mSwipeHelper.setMinAlpha(1f);
1243    }
1244
1245    /** Velocity tracker helpers */
1246    void initOrResetVelocityTracker() {
1247        if (mVelocityTracker == null) {
1248            mVelocityTracker = VelocityTracker.obtain();
1249        } else {
1250            mVelocityTracker.clear();
1251        }
1252    }
1253    void initVelocityTrackerIfNotExists() {
1254        if (mVelocityTracker == null) {
1255            mVelocityTracker = VelocityTracker.obtain();
1256        }
1257    }
1258    void recycleVelocityTracker() {
1259        if (mVelocityTracker != null) {
1260            mVelocityTracker.recycle();
1261            mVelocityTracker = null;
1262        }
1263    }
1264
1265    /** Returns the view at the specified coordinates */
1266    TaskView findViewAtPoint(int x, int y) {
1267        int childCount = mSv.getChildCount();
1268        for (int i = childCount - 1; i >= 0; i--) {
1269            TaskView tv = (TaskView) mSv.getChildAt(i);
1270            if (tv.getVisibility() == View.VISIBLE) {
1271                if (mSv.isTransformedTouchPointInView(x, y, tv)) {
1272                    return tv;
1273                }
1274            }
1275        }
1276        return null;
1277    }
1278
1279    /** Touch preprocessing for handling below */
1280    public boolean onInterceptTouchEvent(MotionEvent ev) {
1281        Console.log(Constants.Log.UI.TouchEvents,
1282                "[TaskStackViewTouchHandler|interceptTouchEvent]",
1283                Console.motionEventActionToString(ev.getAction()), Console.AnsiBlue);
1284
1285        // Return early if we have no children
1286        boolean hasChildren = (mSv.getChildCount() > 0);
1287        if (!hasChildren) {
1288            return false;
1289        }
1290
1291        // Pass through to swipe helper if we are swiping
1292        mInterceptedBySwipeHelper = mSwipeHelper.onInterceptTouchEvent(ev);
1293        if (mInterceptedBySwipeHelper) {
1294            return true;
1295        }
1296
1297        boolean wasScrolling = !mSv.mScroller.isFinished() ||
1298                (mSv.mScrollAnimator != null && mSv.mScrollAnimator.isRunning());
1299        int action = ev.getAction();
1300        switch (action & MotionEvent.ACTION_MASK) {
1301            case MotionEvent.ACTION_DOWN: {
1302                // Save the touch down info
1303                mInitialMotionX = mLastMotionX = (int) ev.getX();
1304                mInitialMotionY = mLastMotionY = (int) ev.getY();
1305                mActivePointerId = ev.getPointerId(0);
1306                mActiveTaskView = findViewAtPoint(mLastMotionX, mLastMotionY);
1307                // Stop the current scroll if it is still flinging
1308                mSv.abortScroller();
1309                mSv.abortBoundScrollAnimation();
1310                // Initialize the velocity tracker
1311                initOrResetVelocityTracker();
1312                mVelocityTracker.addMovement(ev);
1313                // Check if the scroller is finished yet
1314                mIsScrolling = !mSv.mScroller.isFinished();
1315                break;
1316            }
1317            case MotionEvent.ACTION_MOVE: {
1318                if (mActivePointerId == INACTIVE_POINTER_ID) break;
1319
1320                int activePointerIndex = ev.findPointerIndex(mActivePointerId);
1321                int y = (int) ev.getY(activePointerIndex);
1322                int x = (int) ev.getX(activePointerIndex);
1323                if (Math.abs(y - mInitialMotionY) > mScrollTouchSlop) {
1324                    // Save the touch move info
1325                    mIsScrolling = true;
1326                    // Initialize the velocity tracker if necessary
1327                    initVelocityTrackerIfNotExists();
1328                    mVelocityTracker.addMovement(ev);
1329                    // Disallow parents from intercepting touch events
1330                    final ViewParent parent = mSv.getParent();
1331                    if (parent != null) {
1332                        parent.requestDisallowInterceptTouchEvent(true);
1333                    }
1334                    // Enable HW layers
1335                    mSv.addHwLayersRefCount("stackScroll");
1336                }
1337
1338                mLastMotionX = x;
1339                mLastMotionY = y;
1340                break;
1341            }
1342            case MotionEvent.ACTION_CANCEL:
1343            case MotionEvent.ACTION_UP: {
1344                // Animate the scroll back if we've cancelled
1345                mSv.animateBoundScroll();
1346                // Disable HW layers
1347                if (mIsScrolling) {
1348                    mSv.decHwLayersRefCount("stackScroll");
1349                }
1350                // Reset the drag state and the velocity tracker
1351                mIsScrolling = false;
1352                mActivePointerId = INACTIVE_POINTER_ID;
1353                mActiveTaskView = null;
1354                mTotalScrollMotion = 0;
1355                recycleVelocityTracker();
1356                break;
1357            }
1358        }
1359
1360        return wasScrolling || mIsScrolling;
1361    }
1362
1363    /** Handles touch events once we have intercepted them */
1364    public boolean onTouchEvent(MotionEvent ev) {
1365        Console.log(Constants.Log.UI.TouchEvents,
1366                "[TaskStackViewTouchHandler|touchEvent]",
1367                Console.motionEventActionToString(ev.getAction()), Console.AnsiBlue);
1368
1369        // Short circuit if we have no children
1370        boolean hasChildren = (mSv.getChildCount() > 0);
1371        if (!hasChildren) {
1372            return false;
1373        }
1374
1375        // Pass through to swipe helper if we are swiping
1376        if (mInterceptedBySwipeHelper && mSwipeHelper.onTouchEvent(ev)) {
1377            return true;
1378        }
1379
1380        // Update the velocity tracker
1381        initVelocityTrackerIfNotExists();
1382        mVelocityTracker.addMovement(ev);
1383
1384        int action = ev.getAction();
1385        switch (action & MotionEvent.ACTION_MASK) {
1386            case MotionEvent.ACTION_DOWN: {
1387                // Save the touch down info
1388                mInitialMotionX = mLastMotionX = (int) ev.getX();
1389                mInitialMotionY = mLastMotionY = (int) ev.getY();
1390                mActivePointerId = ev.getPointerId(0);
1391                mActiveTaskView = findViewAtPoint(mLastMotionX, mLastMotionY);
1392                // Stop the current scroll if it is still flinging
1393                mSv.abortScroller();
1394                mSv.abortBoundScrollAnimation();
1395                // Initialize the velocity tracker
1396                initOrResetVelocityTracker();
1397                mVelocityTracker.addMovement(ev);
1398                // Disallow parents from intercepting touch events
1399                final ViewParent parent = mSv.getParent();
1400                if (parent != null) {
1401                    parent.requestDisallowInterceptTouchEvent(true);
1402                }
1403                break;
1404            }
1405            case MotionEvent.ACTION_POINTER_DOWN: {
1406                final int index = ev.getActionIndex();
1407                mActivePointerId = ev.getPointerId(index);
1408                mLastMotionX = (int) ev.getX(index);
1409                mLastMotionY = (int) ev.getY(index);
1410                break;
1411            }
1412            case MotionEvent.ACTION_MOVE: {
1413                if (mActivePointerId == INACTIVE_POINTER_ID) break;
1414
1415                int activePointerIndex = ev.findPointerIndex(mActivePointerId);
1416                int x = (int) ev.getX(activePointerIndex);
1417                int y = (int) ev.getY(activePointerIndex);
1418                int yTotal = Math.abs(y - mInitialMotionY);
1419                int deltaY = mLastMotionY - y;
1420                if (!mIsScrolling) {
1421                    if (yTotal > mScrollTouchSlop) {
1422                        mIsScrolling = true;
1423                        // Initialize the velocity tracker
1424                        initOrResetVelocityTracker();
1425                        mVelocityTracker.addMovement(ev);
1426                        // Disallow parents from intercepting touch events
1427                        final ViewParent parent = mSv.getParent();
1428                        if (parent != null) {
1429                            parent.requestDisallowInterceptTouchEvent(true);
1430                        }
1431                        // Enable HW layers
1432                        mSv.addHwLayersRefCount("stackScroll");
1433                    }
1434                }
1435                if (mIsScrolling) {
1436                    int curStackScroll = mSv.getStackScroll();
1437                    int overScrollAmount = mSv.getScrollAmountOutOfBounds(curStackScroll + deltaY);
1438                    if (overScrollAmount != 0) {
1439                        // Bound the overscroll to a fixed amount, and inversely scale the y-movement
1440                        // relative to how close we are to the max overscroll
1441                        float maxOverScroll = mSv.mTaskRect.height() / 3f;
1442                        deltaY = Math.round(deltaY * (1f - (Math.min(maxOverScroll, overScrollAmount)
1443                                / maxOverScroll)));
1444                    }
1445                    mSv.setStackScroll(curStackScroll + deltaY);
1446                    if (mSv.isScrollOutOfBounds()) {
1447                        mVelocityTracker.clear();
1448                    }
1449                }
1450                mLastMotionX = x;
1451                mLastMotionY = y;
1452                mTotalScrollMotion += Math.abs(deltaY);
1453                break;
1454            }
1455            case MotionEvent.ACTION_UP: {
1456                final VelocityTracker velocityTracker = mVelocityTracker;
1457                velocityTracker.computeCurrentVelocity(1000, mMaximumVelocity);
1458                int velocity = (int) velocityTracker.getYVelocity(mActivePointerId);
1459
1460                if (mIsScrolling && (Math.abs(velocity) > mMinimumVelocity)) {
1461                    // Enable HW layers on the stack
1462                    mSv.addHwLayersRefCount("flingScroll");
1463                    // XXX: Make this animation a function of the velocity AND distance
1464                    int overscrollRange = (int) (Math.min(1f,
1465                            Math.abs((float) velocity / mMaximumVelocity)) *
1466                            Constants.Values.TaskStackView.TaskStackOverscrollRange);
1467
1468                    Console.log(Constants.Log.UI.TouchEvents,
1469                            "[TaskStackViewTouchHandler|fling]",
1470                            "scroll: " + mSv.getStackScroll() + " velocity: " + velocity +
1471                                    " maxVelocity: " + mMaximumVelocity +
1472                                    " overscrollRange: " + overscrollRange,
1473                            Console.AnsiGreen);
1474
1475                    // Fling scroll
1476                    mSv.mScroller.fling(0, mSv.getStackScroll(),
1477                            0, -velocity,
1478                            0, 0,
1479                            mSv.mMinScroll, mSv.mMaxScroll,
1480                            0, overscrollRange);
1481                    // Invalidate to kick off computeScroll
1482                    mSv.invalidate();
1483                } else if (mSv.isScrollOutOfBounds()) {
1484                    // Animate the scroll back into bounds
1485                    // XXX: Make this animation a function of the velocity OR distance
1486                    mSv.animateBoundScroll();
1487                }
1488
1489                if (mIsScrolling) {
1490                    // Disable HW layers
1491                    mSv.decHwLayersRefCount("stackScroll");
1492                }
1493                mActivePointerId = INACTIVE_POINTER_ID;
1494                mIsScrolling = false;
1495                mTotalScrollMotion = 0;
1496                recycleVelocityTracker();
1497                break;
1498            }
1499            case MotionEvent.ACTION_POINTER_UP: {
1500                int pointerIndex = ev.getActionIndex();
1501                int pointerId = ev.getPointerId(pointerIndex);
1502                if (pointerId == mActivePointerId) {
1503                    // Select a new active pointer id and reset the motion state
1504                    final int newPointerIndex = (pointerIndex == 0) ? 1 : 0;
1505                    mActivePointerId = ev.getPointerId(newPointerIndex);
1506                    mLastMotionX = (int) ev.getX(newPointerIndex);
1507                    mLastMotionY = (int) ev.getY(newPointerIndex);
1508                    mVelocityTracker.clear();
1509                }
1510                break;
1511            }
1512            case MotionEvent.ACTION_CANCEL: {
1513                if (mIsScrolling) {
1514                    // Disable HW layers
1515                    mSv.decHwLayersRefCount("stackScroll");
1516                }
1517                if (mSv.isScrollOutOfBounds()) {
1518                    // Animate the scroll back into bounds
1519                    // XXX: Make this animation a function of the velocity OR distance
1520                    mSv.animateBoundScroll();
1521                }
1522                mActivePointerId = INACTIVE_POINTER_ID;
1523                mIsScrolling = false;
1524                mTotalScrollMotion = 0;
1525                recycleVelocityTracker();
1526                break;
1527            }
1528        }
1529        return true;
1530    }
1531
1532    /**** SwipeHelper Implementation ****/
1533
1534    @Override
1535    public View getChildAtPosition(MotionEvent ev) {
1536        return findViewAtPoint((int) ev.getX(), (int) ev.getY());
1537    }
1538
1539    @Override
1540    public boolean canChildBeDismissed(View v) {
1541        return true;
1542    }
1543
1544    @Override
1545    public void onBeginDrag(View v) {
1546        // Enable HW layers
1547        mSv.addHwLayersRefCount("swipeBegin");
1548        // Disallow parents from intercepting touch events
1549        final ViewParent parent = mSv.getParent();
1550        if (parent != null) {
1551            parent.requestDisallowInterceptTouchEvent(true);
1552        }
1553        // If the info panel is currently showing on this view, then we need to dismiss it
1554        if (Constants.DebugFlags.App.EnableInfoPane) {
1555            TaskView tv = (TaskView) v;
1556            if (tv.isInfoPaneVisible()) {
1557                tv.hideInfoPane();
1558            }
1559        }
1560    }
1561
1562    @Override
1563    public void onChildDismissed(View v) {
1564        TaskView tv = (TaskView) v;
1565        mSv.onTaskDismissed(tv);
1566
1567        // Disable HW layers
1568        mSv.decHwLayersRefCount("swipeComplete");
1569    }
1570
1571    @Override
1572    public void onSnapBackCompleted(View v) {
1573        // Do Nothing
1574    }
1575
1576    @Override
1577    public void onDragCancelled(View v) {
1578        // Disable HW layers
1579        mSv.decHwLayersRefCount("swipeCancelled");
1580    }
1581}
1582