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