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