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