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