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