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