TaskStackView.java revision 04dfe0d26b944324ee920001f40d74cff47281d6
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.app.ActivityManager;
25import android.content.Context;
26import android.graphics.Canvas;
27import android.graphics.Rect;
28import android.graphics.Region;
29import android.view.MotionEvent;
30import android.view.VelocityTracker;
31import android.view.View;
32import android.view.ViewConfiguration;
33import android.view.ViewParent;
34import android.widget.FrameLayout;
35import android.widget.OverScroller;
36import com.android.systemui.recents.Console;
37import com.android.systemui.recents.Constants;
38import com.android.systemui.recents.RecentsConfiguration;
39import com.android.systemui.recents.RecentsTaskLoader;
40import com.android.systemui.recents.Utilities;
41import com.android.systemui.recents.model.Task;
42import com.android.systemui.recents.model.TaskStack;
43
44import java.util.ArrayList;
45
46
47/* The visual representation of a task stack view */
48public class TaskStackView extends FrameLayout implements TaskStack.TaskStackCallbacks,
49        TaskView.TaskViewCallbacks, ViewPool.ViewPoolConsumer<TaskView, Task>,
50        View.OnClickListener {
51
52    /** The TaskView callbacks */
53    interface TaskStackViewCallbacks {
54        public void onTaskLaunched(TaskStackView stackView, TaskView tv, TaskStack stack, Task t);
55    }
56
57    TaskStack mStack;
58    TaskStackViewTouchHandler mTouchHandler;
59    TaskStackViewCallbacks mCb;
60    ViewPool<TaskView, Task> mViewPool;
61
62    // The various rects that define the stack view
63    Rect mRect = new Rect();
64    Rect mStackRect = new Rect();
65    Rect mStackRectSansPeek = new Rect();
66    Rect mTaskRect = new Rect();
67
68    // The virtual stack scroll that we use for the card layout
69    int mStackScroll;
70    int mMinScroll;
71    int mMaxScroll;
72    OverScroller mScroller;
73    ObjectAnimator mScrollAnimator;
74
75    // Optimizations
76    int mHwLayersRefCount;
77    int mStackViewsAnimationDuration;
78    boolean mStackViewsDirty = true;
79    boolean mAwaitingFirstLayout = true;
80
81    public TaskStackView(Context context, TaskStack stack) {
82        super(context);
83        mStack = stack;
84        mStack.setCallbacks(this);
85        mScroller = new OverScroller(context);
86        mTouchHandler = new TaskStackViewTouchHandler(context, this);
87        mViewPool = new ViewPool<TaskView, Task>(context, this);
88    }
89
90    /** Sets the callbacks */
91    void setCallbacks(TaskStackViewCallbacks cb) {
92        mCb = cb;
93    }
94
95    /** Requests that the views be synchronized with the model */
96    void requestSynchronizeStackViewsWithModel() {
97        requestSynchronizeStackViewsWithModel(0);
98    }
99    void requestSynchronizeStackViewsWithModel(int duration) {
100        Console.log(Constants.DebugFlags.TaskStack.SynchronizeViewsWithModel,
101                "[TaskStackView|requestSynchronize]", "", Console.AnsiYellow);
102        if (!mStackViewsDirty) {
103            invalidate();
104        }
105        if (mAwaitingFirstLayout) {
106            // Skip the animation if we are awaiting first layout
107            mStackViewsAnimationDuration = 0;
108        } else {
109            mStackViewsAnimationDuration = Math.max(mStackViewsAnimationDuration, duration);
110        }
111        mStackViewsDirty = true;
112    }
113
114    // XXX: Optimization: Use a mapping of Task -> View
115    private TaskView getChildViewForTask(Task t) {
116        int childCount = getChildCount();
117        for (int i = 0; i < childCount; i++) {
118            TaskView tv = (TaskView) getChildAt(i);
119            if (tv.getTask() == t) {
120                return tv;
121            }
122        }
123        return null;
124    }
125
126    /** Update/get the transform */
127    public TaskViewTransform getStackTransform(int indexInStack) {
128        TaskViewTransform transform = new TaskViewTransform();
129
130        // Map the items to an continuous position relative to the current scroll
131        int numPeekCards = Constants.Values.TaskStackView.StackPeekNumCards;
132        float overlapHeight = Constants.Values.TaskStackView.StackOverlapPct * mTaskRect.height();
133        float peekHeight = Constants.Values.TaskStackView.StackPeekHeightPct * mStackRect.height();
134        float t = ((indexInStack * overlapHeight) - getStackScroll()) / overlapHeight;
135        float boundedT = Math.max(t, -(numPeekCards + 1));
136
137        // Set the scale relative to its position
138        float minScale = Constants.Values.TaskStackView.StackPeekMinScale;
139        float scaleRange = 1f - minScale;
140        float scaleInc = scaleRange / numPeekCards;
141        float scale = Math.max(minScale, Math.min(1f, 1f + (boundedT * scaleInc)));
142        float scaleYOffset = ((1f - scale) * mTaskRect.height()) / 2;
143        transform.scale = scale;
144
145        // Set the translation
146        if (boundedT < 0f) {
147            transform.translationY = (int) ((Math.max(-numPeekCards, boundedT) /
148                    numPeekCards) * peekHeight - scaleYOffset);
149        } else {
150            transform.translationY = (int) (boundedT * overlapHeight - scaleYOffset);
151        }
152
153        // Update the rect and visibility
154        transform.rect.set(mTaskRect);
155        if (t < -(numPeekCards + 1)) {
156            transform.visible = false;
157        } else {
158            transform.rect.offset(0, transform.translationY);
159            Utilities.scaleRectAboutCenter(transform.rect, scale);
160            transform.visible = Rect.intersects(mRect, transform.rect);
161        }
162        transform.t = t;
163        return transform;
164    }
165
166    /** Synchronizes the views with the model */
167    void synchronizeStackViewsWithModel() {
168        Console.log(Constants.DebugFlags.TaskStack.SynchronizeViewsWithModel,
169                "[TaskStackView|synchronizeViewsWithModel]",
170                "mStackViewsDirty: " + mStackViewsDirty, Console.AnsiYellow);
171        if (mStackViewsDirty) {
172
173            // XXX: Optimization: Use binary search to find the visible range
174            // XXX: Optimize to not call getStackTransform() so many times
175            // XXX: Consider using TaskViewTransform pool to prevent allocations
176            // XXX: Iterate children views, update transforms and remove all that are not visible
177            //      For all remaining tasks, update transforms and if visible add the view
178
179            // Update the visible state of all the tasks
180            ArrayList<Task> tasks = mStack.getTasks();
181            int taskCount = tasks.size();
182            for (int i = 0; i < taskCount; i++) {
183                Task task = tasks.get(i);
184                TaskViewTransform transform = getStackTransform(i);
185                TaskView tv = getChildViewForTask(task);
186
187                if (transform.visible) {
188                    if (tv == null) {
189                        tv = mViewPool.pickUpViewFromPool(task, task);
190                        // When we are picking up a new view from the view pool, prepare it for any
191                        // following animation by putting it in a reasonable place
192                        if (mStackViewsAnimationDuration > 0 && i != 0) {
193                            // XXX: We have to animate when filtering, etc. Maybe we should have a
194                            //      runnable that ensures that tasks are animated in a special way
195                            //      when they are entering the scene?
196                            int fromIndex = (transform.t < 0) ? (i - 1) : (i + 1);
197                            tv.updateViewPropertiesFromTask(null, getStackTransform(fromIndex), 0);
198                        }
199                    }
200                } else {
201                    if (tv != null) {
202                        mViewPool.returnViewToPool(tv);
203                    }
204                }
205            }
206
207            // Update all the current view children
208            // NOTE: We have to iterate in reverse where because we are removing views directly
209            int childCount = getChildCount();
210            for (int i = childCount - 1; i >= 0; i--) {
211                TaskView tv = (TaskView) getChildAt(i);
212                Task task = tv.getTask();
213                TaskViewTransform transform = getStackTransform(mStack.indexOfTask(task));
214                if (!transform.visible) {
215                    mViewPool.returnViewToPool(tv);
216                } else {
217                    tv.updateViewPropertiesFromTask(null, transform, mStackViewsAnimationDuration);
218                }
219            }
220
221            Console.log(Constants.DebugFlags.TaskStack.SynchronizeViewsWithModel,
222                    "  [TaskStackView|viewChildren]", "" + getChildCount());
223
224            mStackViewsAnimationDuration = 0;
225            mStackViewsDirty = false;
226        }
227    }
228
229    /** Sets the current stack scroll */
230    public void setStackScroll(int value) {
231        mStackScroll = value;
232        requestSynchronizeStackViewsWithModel();
233    }
234
235    /** Gets the current stack scroll */
236    public int getStackScroll() {
237        return mStackScroll;
238    }
239
240    /** Animates the stack scroll into bounds */
241    ObjectAnimator animateBoundScroll(int duration) {
242        int curScroll = getStackScroll();
243        int newScroll = Math.max(mMinScroll, Math.min(mMaxScroll, curScroll));
244        if (newScroll != curScroll) {
245            // Enable hw layers on the stack
246            addHwLayersRefCount();
247
248            // Abort any current animations
249            mScroller.abortAnimation();
250            if (mScrollAnimator != null) {
251                mScrollAnimator.cancel();
252                mScrollAnimator.removeAllListeners();
253            }
254
255            // Start a new scroll animation
256            mScrollAnimator = ObjectAnimator.ofInt(this, "stackScroll", curScroll, newScroll);
257            mScrollAnimator.setDuration(duration);
258            mScrollAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
259                @Override
260                public void onAnimationUpdate(ValueAnimator animation) {
261                    setStackScroll((Integer) animation.getAnimatedValue());
262                }
263            });
264            mScrollAnimator.addListener(new AnimatorListenerAdapter() {
265                @Override
266                public void onAnimationEnd(Animator animation) {
267                    // Disable hw layers on the stack
268                    decHwLayersRefCount();
269                }
270            });
271            mScrollAnimator.start();
272        }
273        return mScrollAnimator;
274    }
275
276    /** Aborts any current stack scrolls */
277    void abortBoundScrollAnimation() {
278        if (mScrollAnimator != null) {
279            mScrollAnimator.cancel();
280        }
281    }
282
283    /** Bounds the current scroll if necessary */
284    public boolean boundScroll() {
285        int curScroll = getStackScroll();
286        int newScroll = Math.max(mMinScroll, Math.min(mMaxScroll, curScroll));
287        if (newScroll != curScroll) {
288            setStackScroll(newScroll);
289            return true;
290        }
291        return false;
292    }
293
294    /** Returns whether the current scroll is out of bounds */
295    boolean isScrollOutOfBounds() {
296        return (getStackScroll() < 0) || (getStackScroll() > mMaxScroll);
297    }
298
299    /** Updates the min and max virtual scroll bounds */
300    void updateMinMaxScroll(boolean boundScrollToNewMinMax) {
301        // Compute the min and max scroll values
302        int numTasks = Math.max(1, mStack.getTaskCount());
303        int taskHeight = mTaskRect.height();
304        int stackHeight = mStackRectSansPeek.height();
305        int maxScrollHeight = taskHeight + (int) ((numTasks - 1) *
306                Constants.Values.TaskStackView.StackOverlapPct * taskHeight);
307        mMinScroll = Math.min(stackHeight, maxScrollHeight) - stackHeight;
308        mMaxScroll = maxScrollHeight - stackHeight;
309
310        // Debug logging
311        if (Constants.DebugFlags.UI.MeasureAndLayout) {
312            Console.log("  [TaskStack|minScroll] " + mMinScroll);
313            Console.log("  [TaskStack|maxScroll] " + mMaxScroll);
314        }
315
316        if (boundScrollToNewMinMax) {
317            boundScroll();
318        }
319    }
320
321    /** Enables the hw layers and increments the hw layer requirement ref count */
322    void addHwLayersRefCount() {
323        Console.log(Constants.DebugFlags.UI.HwLayers,
324                "[TaskStackView|addHwLayersRefCount] refCount: " +
325                        mHwLayersRefCount + "->" + (mHwLayersRefCount + 1));
326        if (mHwLayersRefCount == 0) {
327            // Enable hw layers on each of the children
328            int childCount = getChildCount();
329            for (int i = 0; i < childCount; i++) {
330                TaskView tv = (TaskView) getChildAt(i);
331                tv.enableHwLayers();
332            }
333        }
334        mHwLayersRefCount++;
335    }
336
337    /** Decrements the hw layer requirement ref count and disables the hw layers when we don't
338        need them anymore. */
339    void decHwLayersRefCount() {
340        Console.log(Constants.DebugFlags.UI.HwLayers,
341                "[TaskStackView|decHwLayersRefCount] refCount: " +
342                        mHwLayersRefCount + "->" + (mHwLayersRefCount - 1));
343        mHwLayersRefCount--;
344        if (mHwLayersRefCount == 0) {
345            // Disable hw layers on each of the children
346            int childCount = getChildCount();
347            for (int i = 0; i < childCount; i++) {
348                TaskView tv = (TaskView) getChildAt(i);
349                tv.disableHwLayers();
350            }
351        } else if (mHwLayersRefCount < 0) {
352            new Throwable("Invalid hw layers ref count").printStackTrace();
353            Console.logError(getContext(), "Invalid HW layers ref count");
354        }
355    }
356
357    @Override
358    public void computeScroll() {
359        if (mScroller.computeScrollOffset()) {
360            setStackScroll(mScroller.getCurrY());
361            invalidate();
362
363            // If we just finished scrolling, then disable the hw layers
364            if (mScroller.isFinished()) {
365                decHwLayersRefCount();
366            }
367        }
368    }
369
370    @Override
371    public boolean onInterceptTouchEvent(MotionEvent ev) {
372        return mTouchHandler.onInterceptTouchEvent(ev);
373    }
374
375    @Override
376    public boolean onTouchEvent(MotionEvent ev) {
377        return mTouchHandler.onTouchEvent(ev);
378    }
379
380    @Override
381    public void dispatchDraw(Canvas canvas) {
382        Console.log(Constants.DebugFlags.UI.Draw, "[TaskStackView|dispatchDraw]", "",
383                Console.AnsiPurple);
384        synchronizeStackViewsWithModel();
385        super.dispatchDraw(canvas);
386    }
387
388    @Override
389    protected boolean drawChild(Canvas canvas, View child, long drawingTime) {
390        if (Constants.DebugFlags.App.EnableTaskStackClipping) {
391            TaskView tv = (TaskView) child;
392            TaskView nextTv = null;
393            int curIndex = indexOfChild(tv);
394            if (curIndex < (getChildCount() - 1)) {
395                // Clip against the next view (if we aren't animating its alpha)
396                nextTv = (TaskView) getChildAt(curIndex + 1);
397                if (nextTv.getAlpha() == 1f) {
398                    Rect curRect = tv.getClippingRect(Utilities.tmpRect, false);
399                    Rect nextRect = nextTv.getClippingRect(Utilities.tmpRect2, true);
400                    RecentsConfiguration config = RecentsConfiguration.getInstance();
401                    // The hit rects are relative to the task view, which needs to be offset by the
402                    // system bar height
403                    curRect.offset(0, config.systemInsets.top);
404                    nextRect.offset(0, config.systemInsets.top);
405                    // Compute the clip region
406                    Region clipRegion = new Region();
407                    clipRegion.op(curRect, Region.Op.UNION);
408                    clipRegion.op(nextRect, Region.Op.DIFFERENCE);
409                    // Clip the canvas
410                    int saveCount = canvas.save(Canvas.CLIP_SAVE_FLAG);
411                    canvas.clipRegion(clipRegion);
412                    boolean invalidate = super.drawChild(canvas, child, drawingTime);
413                    canvas.restoreToCount(saveCount);
414                    return invalidate;
415                }
416            }
417        }
418        return super.drawChild(canvas, child, drawingTime);
419    }
420
421    /** Computes the stack and task rects */
422    public void computeRects(int width, int height) {
423        // Note: We let the stack view be the full height because we want the cards to go under the
424        //       navigation bar if possible.  However, the stack rects which we use to calculate
425        //       max scroll, etc. need to take the nav bar into account
426
427        // Compute the stack rects
428        RecentsConfiguration config = RecentsConfiguration.getInstance();
429        mRect.set(0, 0, width, height);
430        mStackRect.set(mRect);
431        mStackRect.bottom -= config.systemInsets.bottom;
432
433        int smallestDimension = Math.min(width, height);
434        int padding = (int) (Constants.Values.TaskStackView.StackPaddingPct * smallestDimension / 2f);
435        mStackRect.inset(padding, padding);
436        mStackRectSansPeek.set(mStackRect);
437        mStackRectSansPeek.top += Constants.Values.TaskStackView.StackPeekHeightPct * mStackRect.height();
438
439        // Compute the task rect
440        int minHeight = (int) (mStackRect.height() -
441                (Constants.Values.TaskStackView.StackPeekHeightPct * mStackRect.height()));
442        int size = Math.min(minHeight, Math.min(mStackRect.width(), mStackRect.height()));
443        int centerX = mStackRect.centerX();
444        mTaskRect.set(centerX - size / 2, mStackRectSansPeek.top,
445                centerX + size / 2, mStackRectSansPeek.top + size);
446
447        // Update the scroll bounds
448        updateMinMaxScroll(false);
449    }
450
451    @Override
452    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
453        int width = MeasureSpec.getSize(widthMeasureSpec);
454        int height = MeasureSpec.getSize(heightMeasureSpec);
455        Console.log(Constants.DebugFlags.UI.MeasureAndLayout, "[TaskStackView|measure]",
456                "width: " + width + " height: " + height +
457                " awaitingFirstLayout: " + mAwaitingFirstLayout, Console.AnsiGreen);
458
459        // Compute our stack/task rects
460        computeRects(width, height);
461
462        // Debug logging
463        if (Constants.DebugFlags.UI.MeasureAndLayout) {
464            Console.log("  [TaskStack|fullRect] " + mRect);
465            Console.log("  [TaskStack|stackRect] " + mStackRect);
466            Console.log("  [TaskStack|stackRectSansPeek] " + mStackRectSansPeek);
467            Console.log("  [TaskStack|taskRect] " + mTaskRect);
468        }
469
470        // If this is the first layout, then scroll to the front of the stack and synchronize the
471        // stack views immediately
472        if (mAwaitingFirstLayout) {
473            setStackScroll(mMaxScroll);
474            requestSynchronizeStackViewsWithModel();
475            synchronizeStackViewsWithModel();
476
477            // Animate the icon of the first task view
478            if (Constants.Values.TaskView.AnimateFrontTaskIconOnEnterRecents) {
479                TaskView tv = (TaskView) getChildAt(getChildCount() - 1);
480                if (tv != null) {
481                    tv.animateOnEnterRecents();
482                }
483            }
484        }
485
486        // Measure each of the children
487        int childCount = getChildCount();
488        for (int i = 0; i < childCount; i++) {
489            TaskView t = (TaskView) getChildAt(i);
490            t.measure(MeasureSpec.makeMeasureSpec(mTaskRect.width(), MeasureSpec.EXACTLY),
491                    MeasureSpec.makeMeasureSpec(mTaskRect.height(), MeasureSpec.EXACTLY));
492        }
493
494        setMeasuredDimension(width, height);
495    }
496
497    @Override
498    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
499        Console.log(Constants.DebugFlags.UI.MeasureAndLayout, "[TaskStackView|layout]",
500                "" + new Rect(left, top, right, bottom), Console.AnsiGreen);
501
502        // Debug logging
503        if (Constants.DebugFlags.UI.MeasureAndLayout) {
504            Console.log("  [TaskStack|fullRect] " + mRect);
505            Console.log("  [TaskStack|stackRect] " + mStackRect);
506            Console.log("  [TaskStack|stackRectSansPeek] " + mStackRectSansPeek);
507            Console.log("  [TaskStack|taskRect] " + mTaskRect);
508        }
509
510        // Layout each of the children
511        int childCount = getChildCount();
512        for (int i = 0; i < childCount; i++) {
513            TaskView t = (TaskView) getChildAt(i);
514            t.layout(mTaskRect.left, mStackRectSansPeek.top,
515                    mTaskRect.right, mStackRectSansPeek.top + mTaskRect.height());
516        }
517
518        if (!mAwaitingFirstLayout) {
519            requestSynchronizeStackViewsWithModel();
520        } else {
521            mAwaitingFirstLayout = false;
522        }
523    }
524
525    @Override
526    protected void onScrollChanged(int l, int t, int oldl, int oldt) {
527        super.onScrollChanged(l, t, oldl, oldt);
528        requestSynchronizeStackViewsWithModel();
529    }
530
531    public boolean isTransformedTouchPointInView(float x, float y, View child) {
532        return isTransformedTouchPointInView(x, y, child, null);
533    }
534
535    /**** TaskStackCallbacks Implementation ****/
536
537    @Override
538    public void onStackTaskAdded(TaskStack stack, Task t) {
539        requestSynchronizeStackViewsWithModel();
540    }
541
542    @Override
543    public void onStackTaskRemoved(TaskStack stack, Task t) {
544        // Remove the view associated with this task, we can't rely on updateTransforms
545        // to work here because the task is no longer in the list
546        int childCount = getChildCount();
547        for (int i = childCount - 1; i >= 0; i--) {
548            TaskView tv = (TaskView) getChildAt(i);
549            if (tv.getTask() == t) {
550                mViewPool.returnViewToPool(tv);
551                break;
552            }
553        }
554
555        updateMinMaxScroll(true);
556        requestSynchronizeStackViewsWithModel(Constants.Values.TaskStackView.Animation.TaskRemovedReshuffleDuration);
557    }
558
559    @Override
560    public void onStackFiltered(TaskStack stack) {
561        requestSynchronizeStackViewsWithModel();
562    }
563
564    @Override
565    public void onStackUnfiltered(TaskStack stack) {
566        requestSynchronizeStackViewsWithModel();
567    }
568
569    /**** ViewPoolConsumer Implementation ****/
570
571    @Override
572    public TaskView createView(Context context) {
573        Console.log(Constants.DebugFlags.ViewPool.PoolCallbacks, "[TaskStackView|createPoolView]");
574        return new TaskView(context);
575    }
576
577    @Override
578    public void prepareViewToEnterPool(TaskView tv) {
579        Task task = tv.getTask();
580        tv.resetViewProperties();
581        Console.log(Constants.DebugFlags.ViewPool.PoolCallbacks, "[TaskStackView|returnToPool]",
582                tv.getTask() + " tv: " + tv);
583
584        // Report that this tasks's data is no longer being used
585        RecentsTaskLoader loader = RecentsTaskLoader.getInstance();
586        loader.unloadTaskData(task);
587
588        // Detach the view from the hierarchy
589        detachViewFromParent(tv);
590
591        // Disable hw layers on this view
592        tv.disableHwLayers();
593    }
594
595    @Override
596    public void prepareViewToLeavePool(TaskView tv, Task prepareData, boolean isNewView) {
597        Console.log(Constants.DebugFlags.ViewPool.PoolCallbacks, "[TaskStackView|leavePool]",
598                "isNewView: " + isNewView);
599
600        // Setup and attach the view to the window
601        Task task = prepareData;
602        // We try and rebind the task (this MUST be done before the task filled)
603        tv.onTaskBound(task);
604        // Request that this tasks's data be filled
605        RecentsTaskLoader loader = RecentsTaskLoader.getInstance();
606        loader.loadTaskData(task);
607
608        // Find the index where this task should be placed in the children
609        int insertIndex = -1;
610        int childCount = getChildCount();
611        for (int i = 0; i < childCount; i++) {
612            Task tvTask = ((TaskView) getChildAt(i)).getTask();
613            if (mStack.containsTask(task) && (mStack.indexOfTask(task) < mStack.indexOfTask(tvTask))) {
614                insertIndex = i;
615                break;
616            }
617        }
618
619        // Add/attach the view to the hierarchy
620        Console.log(Constants.DebugFlags.ViewPool.PoolCallbacks, "  [TaskStackView|insertIndex]",
621                "" + insertIndex);
622        if (isNewView) {
623            addView(tv, insertIndex);
624
625            // Set the callbacks and listeners for this new view
626            tv.setOnClickListener(this);
627            tv.setCallbacks(this);
628        } else {
629            attachViewToParent(tv, insertIndex, tv.getLayoutParams());
630        }
631
632        // Enable hw layers on this view if hw layers are enabled on the stack
633        if (mHwLayersRefCount > 0) {
634            tv.enableHwLayers();
635        }
636    }
637
638    @Override
639    public boolean hasPreferredData(TaskView tv, Task preferredData) {
640        return (tv.getTask() == preferredData);
641    }
642
643    /**** TaskViewCallbacks Implementation ****/
644
645    @Override
646    public void onTaskIconClicked(TaskView tv) {
647        Console.log(Constants.DebugFlags.UI.ClickEvents, "[TaskStack|Clicked|Icon]",
648                tv.getTask() + " is currently filtered: " + mStack.hasFilteredTasks(),
649                Console.AnsiCyan);
650        if (Constants.DebugFlags.App.EnableTaskFiltering) {
651            if (mStack.hasFilteredTasks()) {
652                mStack.unfilterTasks();
653            } else {
654                mStack.filterTasks(tv.getTask());
655            }
656        } else {
657            Console.logError(getContext(), "Task Filtering TBD");
658        }
659    }
660
661    /**** View.OnClickListener Implementation ****/
662
663    @Override
664    public void onClick(View v) {
665        TaskView tv = (TaskView) v;
666        Task task = tv.getTask();
667        Console.log(Constants.DebugFlags.UI.ClickEvents, "[TaskStack|Clicked|Thumbnail]",
668                task + " cb: " + mCb);
669
670        if (mCb != null) {
671            mCb.onTaskLaunched(this, tv, mStack, task);
672        }
673    }
674}
675
676/* Handles touch events */
677class TaskStackViewTouchHandler implements SwipeHelper.Callback {
678    static int INACTIVE_POINTER_ID = -1;
679
680    TaskStackView mSv;
681    VelocityTracker mVelocityTracker;
682
683    boolean mIsScrolling;
684
685    int mInitialMotionX, mInitialMotionY;
686    int mLastMotionX, mLastMotionY;
687    int mActivePointerId = INACTIVE_POINTER_ID;
688    TaskView mActiveTaskView = null;
689
690    int mTotalScrollMotion;
691    int mMinimumVelocity;
692    int mMaximumVelocity;
693    // The scroll touch slop is used to calculate when we start scrolling
694    int mScrollTouchSlop;
695    // The page touch slop is used to calculate when we start swiping
696    float mPagingTouchSlop;
697
698    SwipeHelper mSwipeHelper;
699    boolean mInterceptedBySwipeHelper;
700
701    public TaskStackViewTouchHandler(Context context, TaskStackView sv) {
702        ViewConfiguration configuration = ViewConfiguration.get(context);
703        mMinimumVelocity = configuration.getScaledMinimumFlingVelocity();
704        mMaximumVelocity = configuration.getScaledMaximumFlingVelocity();
705        mScrollTouchSlop = configuration.getScaledTouchSlop();
706        mPagingTouchSlop = configuration.getScaledPagingTouchSlop();
707        mSv = sv;
708
709
710        float densityScale = context.getResources().getDisplayMetrics().density;
711        mSwipeHelper = new SwipeHelper(SwipeHelper.X, this, densityScale, mPagingTouchSlop);
712        mSwipeHelper.setMinAlpha(1f);
713    }
714
715    /** Velocity tracker helpers */
716    void initOrResetVelocityTracker() {
717        if (mVelocityTracker == null) {
718            mVelocityTracker = VelocityTracker.obtain();
719        } else {
720            mVelocityTracker.clear();
721        }
722    }
723    void initVelocityTrackerIfNotExists() {
724        if (mVelocityTracker == null) {
725            mVelocityTracker = VelocityTracker.obtain();
726        }
727    }
728    void recycleVelocityTracker() {
729        if (mVelocityTracker != null) {
730            mVelocityTracker.recycle();
731            mVelocityTracker = null;
732        }
733    }
734
735    /** Returns the view at the specified coordinates */
736    TaskView findViewAtPoint(int x, int y) {
737        int childCount = mSv.getChildCount();
738        for (int i = childCount - 1; i >= 0; i--) {
739            TaskView tv = (TaskView) mSv.getChildAt(i);
740            if (tv.getVisibility() == View.VISIBLE) {
741                if (mSv.isTransformedTouchPointInView(x, y, tv)) {
742                    return tv;
743                }
744            }
745        }
746        return null;
747    }
748
749    /** Touch preprocessing for handling below */
750    public boolean onInterceptTouchEvent(MotionEvent ev) {
751        Console.log(Constants.DebugFlags.UI.TouchEvents,
752                "[TaskStackViewTouchHandler|interceptTouchEvent]",
753                Console.motionEventActionToString(ev.getAction()), Console.AnsiBlue);
754
755        // Return early if we have no children
756        boolean hasChildren = (mSv.getChildCount() > 0);
757        if (!hasChildren) {
758            return false;
759        }
760
761        // Pass through to swipe helper if we are swiping
762        mInterceptedBySwipeHelper = mSwipeHelper.onInterceptTouchEvent(ev);
763        if (mInterceptedBySwipeHelper) {
764            return true;
765        }
766
767        boolean wasScrolling = !mSv.mScroller.isFinished() ||
768                (mSv.mScrollAnimator != null && mSv.mScrollAnimator.isRunning());
769        int action = ev.getAction();
770        switch (action & MotionEvent.ACTION_MASK) {
771            case MotionEvent.ACTION_DOWN: {
772                // Save the touch down info
773                mInitialMotionX = mLastMotionX = (int) ev.getX();
774                mInitialMotionY = mLastMotionY = (int) ev.getY();
775                mActivePointerId = ev.getPointerId(0);
776                mActiveTaskView = findViewAtPoint(mLastMotionX, mLastMotionY);
777                // Stop the current scroll if it is still flinging
778                mSv.mScroller.abortAnimation();
779                mSv.abortBoundScrollAnimation();
780                // Initialize the velocity tracker
781                initOrResetVelocityTracker();
782                mVelocityTracker.addMovement(ev);
783                // Check if the scroller is finished yet
784                mIsScrolling = !mSv.mScroller.isFinished();
785                // Enable HW layers
786                mSv.addHwLayersRefCount();
787                break;
788            }
789            case MotionEvent.ACTION_MOVE: {
790                if (mActivePointerId == INACTIVE_POINTER_ID) break;
791
792                int activePointerIndex = ev.findPointerIndex(mActivePointerId);
793                int y = (int) ev.getY(activePointerIndex);
794                int x = (int) ev.getX(activePointerIndex);
795                if (Math.abs(y - mInitialMotionY) > mScrollTouchSlop) {
796                    // Save the touch move info
797                    mIsScrolling = true;
798                    // Initialize the velocity tracker if necessary
799                    initVelocityTrackerIfNotExists();
800                    mVelocityTracker.addMovement(ev);
801                    // Disallow parents from intercepting touch events
802                    final ViewParent parent = mSv.getParent();
803                    if (parent != null) {
804                        parent.requestDisallowInterceptTouchEvent(true);
805                    }
806                }
807
808                mLastMotionX = x;
809                mLastMotionY = y;
810                break;
811            }
812            case MotionEvent.ACTION_CANCEL:
813            case MotionEvent.ACTION_UP: {
814                // Animate the scroll back if we've cancelled
815                mSv.animateBoundScroll(Constants.Values.TaskStackView.Animation.SnapScrollBackDuration);
816                // Reset the drag state and the velocity tracker
817                mIsScrolling = false;
818                mActivePointerId = INACTIVE_POINTER_ID;
819                mActiveTaskView = null;
820                mTotalScrollMotion = 0;
821                recycleVelocityTracker();
822                // Disable HW layers
823                mSv.decHwLayersRefCount();
824                break;
825            }
826        }
827
828        return wasScrolling || mIsScrolling;
829    }
830
831    /** Handles touch events once we have intercepted them */
832    public boolean onTouchEvent(MotionEvent ev) {
833        Console.log(Constants.DebugFlags.TaskStack.SynchronizeViewsWithModel,
834                "[TaskStackViewTouchHandler|touchEvent]",
835                Console.motionEventActionToString(ev.getAction()), Console.AnsiBlue);
836
837        // Short circuit if we have no children
838        boolean hasChildren = (mSv.getChildCount() > 0);
839        if (!hasChildren) {
840            return false;
841        }
842
843        // Pass through to swipe helper if we are swiping
844        if (mInterceptedBySwipeHelper && mSwipeHelper.onTouchEvent(ev)) {
845            return true;
846        }
847
848        // Update the velocity tracker
849        initVelocityTrackerIfNotExists();
850        mVelocityTracker.addMovement(ev);
851
852        int action = ev.getAction();
853        switch (action & MotionEvent.ACTION_MASK) {
854            case MotionEvent.ACTION_DOWN: {
855                // Save the touch down info
856                mInitialMotionX = mLastMotionX = (int) ev.getX();
857                mInitialMotionY = mLastMotionY = (int) ev.getY();
858                mActivePointerId = ev.getPointerId(0);
859                mActiveTaskView = findViewAtPoint(mLastMotionX, mLastMotionY);
860                // Stop the current scroll if it is still flinging
861                mSv.mScroller.abortAnimation();
862                mSv.abortBoundScrollAnimation();
863                // Initialize the velocity tracker
864                initOrResetVelocityTracker();
865                mVelocityTracker.addMovement(ev);
866                // Disallow parents from intercepting touch events
867                final ViewParent parent = mSv.getParent();
868                if (parent != null) {
869                    parent.requestDisallowInterceptTouchEvent(true);
870                }
871                break;
872            }
873            case MotionEvent.ACTION_MOVE: {
874                if (mActivePointerId == INACTIVE_POINTER_ID) break;
875
876                int activePointerIndex = ev.findPointerIndex(mActivePointerId);
877                int x = (int) ev.getX(activePointerIndex);
878                int y = (int) ev.getY(activePointerIndex);
879                int deltaY = mLastMotionY - y;
880                if (!mIsScrolling) {
881                    if (Math.abs(y - mInitialMotionY) > mScrollTouchSlop) {
882                        mIsScrolling = true;
883                        // Initialize the velocity tracker
884                        initOrResetVelocityTracker();
885                        mVelocityTracker.addMovement(ev);
886                        // Disallow parents from intercepting touch events
887                        final ViewParent parent = mSv.getParent();
888                        if (parent != null) {
889                            parent.requestDisallowInterceptTouchEvent(true);
890                        }
891                        // Enable HW layers
892                        mSv.addHwLayersRefCount();
893                    }
894                }
895                if (mIsScrolling) {
896                    mSv.setStackScroll(mSv.getStackScroll() + deltaY);
897                    if (mSv.isScrollOutOfBounds()) {
898                        mVelocityTracker.clear();
899                    }
900                }
901                mLastMotionX = x;
902                mLastMotionY = y;
903                mTotalScrollMotion += Math.abs(deltaY);
904                break;
905            }
906            case MotionEvent.ACTION_UP: {
907                final VelocityTracker velocityTracker = mVelocityTracker;
908                velocityTracker.computeCurrentVelocity(1000, mMaximumVelocity);
909                int velocity = (int) velocityTracker.getYVelocity(mActivePointerId);
910
911                if (mIsScrolling && (Math.abs(velocity) > mMinimumVelocity)) {
912                    Console.log(Constants.DebugFlags.UI.TouchEvents,
913                        "[TaskStackViewTouchHandler|fling]",
914                        "scroll: " + mSv.getStackScroll() + " velocity: " + velocity,
915                            Console.AnsiGreen);
916                    // Enable HW layers on the stack
917                    mSv.addHwLayersRefCount();
918                    // Fling scroll
919                    mSv.mScroller.fling(0, mSv.getStackScroll(),
920                            0, -velocity,
921                            0, 0,
922                            mSv.mMinScroll, mSv.mMaxScroll,
923                            0, 0);
924                    // Invalidate to kick off computeScroll
925                    mSv.invalidate();
926                } else if (mSv.isScrollOutOfBounds()) {
927                    // Animate the scroll back into bounds
928                    // XXX: Make this animation a function of the velocity OR distance
929                    mSv.animateBoundScroll(Constants.Values.TaskStackView.Animation.SnapScrollBackDuration);
930                }
931
932                mActivePointerId = INACTIVE_POINTER_ID;
933                mIsScrolling = false;
934                mTotalScrollMotion = 0;
935                recycleVelocityTracker();
936                // Disable HW layers
937                mSv.decHwLayersRefCount();
938                break;
939            }
940            case MotionEvent.ACTION_CANCEL: {
941                if (mSv.isScrollOutOfBounds()) {
942                    // Animate the scroll back into bounds
943                    // XXX: Make this animation a function of the velocity OR distance
944                    mSv.animateBoundScroll(Constants.Values.TaskStackView.Animation.SnapScrollBackDuration);
945                }
946
947                mActivePointerId = INACTIVE_POINTER_ID;
948                mIsScrolling = false;
949                mTotalScrollMotion = 0;
950                recycleVelocityTracker();
951                // Disable HW layers
952                mSv.decHwLayersRefCount();
953                break;
954            }
955        }
956        return true;
957    }
958
959    /**** SwipeHelper Implementation ****/
960
961    @Override
962    public View getChildAtPosition(MotionEvent ev) {
963        return findViewAtPoint((int) ev.getX(), (int) ev.getY());
964    }
965
966    @Override
967    public boolean canChildBeDismissed(View v) {
968        return true;
969    }
970
971    @Override
972    public void onBeginDrag(View v) {
973        // Enable HW layers
974        mSv.addHwLayersRefCount();
975        // Disallow parents from intercepting touch events
976        final ViewParent parent = mSv.getParent();
977        if (parent != null) {
978            parent.requestDisallowInterceptTouchEvent(true);
979        }
980    }
981
982    @Override
983    public void onChildDismissed(View v) {
984        TaskView tv = (TaskView) v;
985        Task task = tv.getTask();
986        Activity activity = (Activity) mSv.getContext();
987
988        // We have to disable the listener to ensure that we
989        // don't hit this again
990        tv.animate().setListener(null);
991
992        // Remove the task from the view
993        mSv.mStack.removeTask(task);
994
995        // Remove any stored data from the loader
996        RecentsTaskLoader loader = RecentsTaskLoader.getInstance();
997        loader.deleteTaskData(task);
998
999        // Remove the task from activity manager
1000        final ActivityManager am = (ActivityManager)
1001                activity.getSystemService(Context.ACTIVITY_SERVICE);
1002        if (am != null) {
1003            am.removeTask(tv.getTask().key.id,
1004                    ActivityManager.REMOVE_TASK_KILL_PROCESS);
1005        }
1006
1007        // If there are no remaining tasks, then just close the activity
1008        if (mSv.mStack.getTaskCount() == 0) {
1009            activity.finish();
1010        }
1011
1012        // Disable HW layers
1013        mSv.decHwLayersRefCount();
1014    }
1015
1016    @Override
1017    public void onSnapBackCompleted(View v) {
1018        // Do Nothing
1019    }
1020
1021    @Override
1022    public void onDragCancelled(View v) {
1023        // Disable HW layers
1024        mSv.decHwLayersRefCount();
1025    }
1026}
1027