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