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