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