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