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