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