TaskStackView.java revision bb5278b663a0a5b05bf7f8d2dfe27f1aa5f01142
189fa4ad53f2f4d57adbc97ae1149fc00c9b6f3c5The Android Open Source Project/*
2db5cb14318bb24cd6ea14ff7ceea0d5e1f83d903Dima Zavin * Copyright (C) 2014 The Android Open Source Project
3db5cb14318bb24cd6ea14ff7ceea0d5e1f83d903Dima Zavin *
4db5cb14318bb24cd6ea14ff7ceea0d5e1f83d903Dima Zavin * Licensed under the Apache License, Version 2.0 (the "License");
5db5cb14318bb24cd6ea14ff7ceea0d5e1f83d903Dima Zavin * you may not use this file except in compliance with the License.
678b5e82f421bfdf5c1c72bd0ca2fb7f3bc1a45ffGlenn Kasten * You may obtain a copy of the License at
778b5e82f421bfdf5c1c72bd0ca2fb7f3bc1a45ffGlenn Kasten *
878b5e82f421bfdf5c1c72bd0ca2fb7f3bc1a45ffGlenn Kasten *      http://www.apache.org/licenses/LICENSE-2.0
978b5e82f421bfdf5c1c72bd0ca2fb7f3bc1a45ffGlenn Kasten *
1078b5e82f421bfdf5c1c72bd0ca2fb7f3bc1a45ffGlenn Kasten * Unless required by applicable law or agreed to in writing, software
1178b5e82f421bfdf5c1c72bd0ca2fb7f3bc1a45ffGlenn Kasten * distributed under the License is distributed on an "AS IS" BASIS,
1278b5e82f421bfdf5c1c72bd0ca2fb7f3bc1a45ffGlenn Kasten * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
1378b5e82f421bfdf5c1c72bd0ca2fb7f3bc1a45ffGlenn Kasten * See the License for the specific language governing permissions and
1478b5e82f421bfdf5c1c72bd0ca2fb7f3bc1a45ffGlenn Kasten * limitations under the License.
1578b5e82f421bfdf5c1c72bd0ca2fb7f3bc1a45ffGlenn Kasten */
16a8190fc518b6769257896605f3aee091aeb60b50Glenn Kasten
1778b5e82f421bfdf5c1c72bd0ca2fb7f3bc1a45ffGlenn Kastenpackage com.android.systemui.recents.views;
1878b5e82f421bfdf5c1c72bd0ca2fb7f3bc1a45ffGlenn Kasten
1978b5e82f421bfdf5c1c72bd0ca2fb7f3bc1a45ffGlenn Kastenimport android.animation.Animator;
2078b5e82f421bfdf5c1c72bd0ca2fb7f3bc1a45ffGlenn Kastenimport android.animation.AnimatorListenerAdapter;
21ed3e3e046840d5bf1ca84a8c0cc097425e89d6d6Andreas Huberimport android.animation.ObjectAnimator;
22441a78d5e224e0d67f9b52fa9adc795c6944159bJeff Tinkerimport android.animation.ValueAnimator;
23c0d5f1f8405de861ed6f1725f26cd6601e7103abJeff Tinkerimport android.app.Activity;
241b19c9d120869c3182373a9b06a1ed98898df882Andreas Huberimport android.app.ActivityManager;
2578b5e82f421bfdf5c1c72bd0ca2fb7f3bc1a45ffGlenn Kastenimport android.content.Context;
2678b5e82f421bfdf5c1c72bd0ca2fb7f3bc1a45ffGlenn Kastenimport android.graphics.Canvas;
2778b5e82f421bfdf5c1c72bd0ca2fb7f3bc1a45ffGlenn Kastenimport android.graphics.Rect;
281b86fe063badb5f28c467ade39be0f4008688947Andreas Huberimport android.graphics.Region;
291b86fe063badb5f28c467ade39be0f4008688947Andreas Huberimport android.view.MotionEvent;
306f1c1918d0dfece10f728711b055441e4d135c73Glenn Kastenimport android.view.VelocityTracker;
31c524ffda17017d8467a237a1eddfd7e7c03c6617Glenn Kastenimport android.view.View;
3278b5e82f421bfdf5c1c72bd0ca2fb7f3bc1a45ffGlenn Kastenimport android.view.ViewConfiguration;
33c524ffda17017d8467a237a1eddfd7e7c03c6617Glenn Kastenimport android.view.ViewParent;
3478b5e82f421bfdf5c1c72bd0ca2fb7f3bc1a45ffGlenn Kastenimport android.widget.FrameLayout;
35a64c8c79af1a15911c55306d83a797fa50969f77nikoimport android.widget.OverScroller;
36e104596061b219e9bce6d4db49a9d15242f8d2e5Jeff Brownimport com.android.systemui.recents.Console;
37e104596061b219e9bce6d4db49a9d15242f8d2e5Jeff Brownimport com.android.systemui.recents.Constants;
38e2b1028852120bcfded33b8f06f66b780437fe92Andreas Huberimport com.android.systemui.recents.RecentsConfiguration;
3978b5e82f421bfdf5c1c72bd0ca2fb7f3bc1a45ffGlenn Kastenimport com.android.systemui.recents.RecentsTaskLoader;
40c524ffda17017d8467a237a1eddfd7e7c03c6617Glenn Kastenimport com.android.systemui.recents.Utilities;
4178b5e82f421bfdf5c1c72bd0ca2fb7f3bc1a45ffGlenn Kastenimport com.android.systemui.recents.model.Task;
4278b5e82f421bfdf5c1c72bd0ca2fb7f3bc1a45ffGlenn Kastenimport com.android.systemui.recents.model.TaskStack;
4378b5e82f421bfdf5c1c72bd0ca2fb7f3bc1a45ffGlenn Kasten
4478b5e82f421bfdf5c1c72bd0ca2fb7f3bc1a45ffGlenn Kastenimport java.util.ArrayList;
4578b5e82f421bfdf5c1c72bd0ca2fb7f3bc1a45ffGlenn Kasten
4678b5e82f421bfdf5c1c72bd0ca2fb7f3bc1a45ffGlenn Kasten
47c524ffda17017d8467a237a1eddfd7e7c03c6617Glenn Kasten/* The visual representation of a task stack view */
4878b5e82f421bfdf5c1c72bd0ca2fb7f3bc1a45ffGlenn Kastenpublic class TaskStackView extends FrameLayout implements TaskStack.TaskStackCallbacks,
49544ad2be674423238c47650d2c8588ba7dfc9ed2Marco Nelissen        TaskView.TaskViewCallbacks, ViewPool.ViewPoolConsumer<TaskView, Task>,
5078b5e82f421bfdf5c1c72bd0ca2fb7f3bc1a45ffGlenn Kasten        View.OnClickListener {
5178b5e82f421bfdf5c1c72bd0ca2fb7f3bc1a45ffGlenn Kasten
5278b5e82f421bfdf5c1c72bd0ca2fb7f3bc1a45ffGlenn Kasten    /** The TaskView callbacks */
5378b5e82f421bfdf5c1c72bd0ca2fb7f3bc1a45ffGlenn Kasten    interface TaskStackViewCallbacks {
5478b5e82f421bfdf5c1c72bd0ca2fb7f3bc1a45ffGlenn Kasten        public void onTaskLaunched(TaskStackView stackView, TaskView tv, TaskStack stack, Task t);
5578b5e82f421bfdf5c1c72bd0ca2fb7f3bc1a45ffGlenn Kasten    }
562e66a7896c9a9da3a15fc6cff9be28b4174d8719Eric Laurent
572e66a7896c9a9da3a15fc6cff9be28b4174d8719Eric Laurent    TaskStack mStack;
581f7d356fa094b975ad2ebf9217be6abba2c70825Mathias Agopian    TaskStackViewTouchHandler mTouchHandler;
591f7d356fa094b975ad2ebf9217be6abba2c70825Mathias Agopian    TaskStackViewCallbacks mCb;
60413f523afe96aff02d2b0a7459127b8f67b2b43cAndreas Huber    ViewPool<TaskView, Task> mViewPool;
615c4cc0d99d3b1cb35c5d7c237272ee53142745fbGlenn Kasten
625c4cc0d99d3b1cb35c5d7c237272ee53142745fbGlenn Kasten    // The various rects that define the stack view
635c4cc0d99d3b1cb35c5d7c237272ee53142745fbGlenn Kasten    Rect mRect = new Rect();
645c4cc0d99d3b1cb35c5d7c237272ee53142745fbGlenn Kasten    Rect mStackRect = new Rect();
655c4cc0d99d3b1cb35c5d7c237272ee53142745fbGlenn Kasten    Rect mStackRectSansPeek = new Rect();
665c4cc0d99d3b1cb35c5d7c237272ee53142745fbGlenn Kasten    Rect mTaskRect = new Rect();
67a07a1c2c91dc7ee6ded319262499f20cd01edcf7Glenn Kasten
685c4cc0d99d3b1cb35c5d7c237272ee53142745fbGlenn Kasten    // The virtual stack scroll that we use for the card layout
6989fa4ad53f2f4d57adbc97ae1149fc00c9b6f3c5The Android Open Source Project    int mStackScroll;
70544ad2be674423238c47650d2c8588ba7dfc9ed2Marco Nelissen    int mMinScroll;
715f7fcf29a7475a20cf38bf72da67746135d504c3Mathias Agopian    int mMaxScroll;
72c41590251aa84c078c942d258e838aad814b73a5Glenn Kasten    OverScroller mScroller;
7389fa4ad53f2f4d57adbc97ae1149fc00c9b6f3c5The Android Open Source Project    ObjectAnimator mScrollAnimator;
74db5cb14318bb24cd6ea14ff7ceea0d5e1f83d903Dima Zavin
75db5cb14318bb24cd6ea14ff7ceea0d5e1f83d903Dima Zavin    // Optimizations
7689fa4ad53f2f4d57adbc97ae1149fc00c9b6f3c5The Android Open Source Project    int mHwLayersRefCount;
7789fa4ad53f2f4d57adbc97ae1149fc00c9b6f3c5The Android Open Source Project    int mStackViewsAnimationDuration;
7889fa4ad53f2f4d57adbc97ae1149fc00c9b6f3c5The Android Open Source Project    boolean mStackViewsDirty = true;
7910dbb8e97e7a81ca4867663b5517f048820b3094Marco Nelissen    boolean mAwaitingFirstLayout = true;
806c6b4d0d2b98a7ceee8b697daaf611f8df3254fbJames Dong
811d7491b19516505e0754c66a3c8cd61811c9b6a6James Dong    public TaskStackView(Context context, TaskStack stack) {
82544ad2be674423238c47650d2c8588ba7dfc9ed2Marco Nelissen        super(context);
8333b383948e8f270bff30378476f00dce289004ebGlenn Kasten        mStack = stack;
8433b383948e8f270bff30378476f00dce289004ebGlenn Kasten        mStack.setCallbacks(this);
8510dbb8e97e7a81ca4867663b5517f048820b3094Marco Nelissen        mScroller = new OverScroller(context);
8689fa4ad53f2f4d57adbc97ae1149fc00c9b6f3c5The Android Open Source Project        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("animateBoundScroll");
247
248            // Abort any current animations
249            abortScroller();
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("animateBoundScroll");
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    void abortScroller() {
284        if (!mScroller.isFinished()) {
285            // Abort the scroller
286            mScroller.abortAnimation();
287            // And disable hw layers on the stack
288            decHwLayersRefCount("flingScroll");
289        }
290    }
291
292    /** Bounds the current scroll if necessary */
293    public boolean boundScroll() {
294        int curScroll = getStackScroll();
295        int newScroll = Math.max(mMinScroll, Math.min(mMaxScroll, curScroll));
296        if (newScroll != curScroll) {
297            setStackScroll(newScroll);
298            return true;
299        }
300        return false;
301    }
302
303    /** Returns whether the current scroll is out of bounds */
304    boolean isScrollOutOfBounds() {
305        return (getStackScroll() < 0) || (getStackScroll() > mMaxScroll);
306    }
307
308    /** Updates the min and max virtual scroll bounds */
309    void updateMinMaxScroll(boolean boundScrollToNewMinMax) {
310        // Compute the min and max scroll values
311        int numTasks = Math.max(1, mStack.getTaskCount());
312        int taskHeight = mTaskRect.height();
313        int stackHeight = mStackRectSansPeek.height();
314        int maxScrollHeight = taskHeight + (int) ((numTasks - 1) *
315                Constants.Values.TaskStackView.StackOverlapPct * taskHeight);
316        mMinScroll = Math.min(stackHeight, maxScrollHeight) - stackHeight;
317        mMaxScroll = maxScrollHeight - stackHeight;
318
319        // Debug logging
320        if (Constants.DebugFlags.UI.MeasureAndLayout) {
321            Console.log("  [TaskStack|minScroll] " + mMinScroll);
322            Console.log("  [TaskStack|maxScroll] " + mMaxScroll);
323        }
324
325        if (boundScrollToNewMinMax) {
326            boundScroll();
327        }
328    }
329
330    /** Enables the hw layers and increments the hw layer requirement ref count */
331    void addHwLayersRefCount(String reason) {
332        Console.log(Constants.DebugFlags.UI.HwLayers,
333                "[TaskStackView|addHwLayersRefCount] refCount: " +
334                        mHwLayersRefCount + "->" + (mHwLayersRefCount + 1) + " " + reason);
335        if (mHwLayersRefCount == 0) {
336            // Enable hw layers on each of the children
337            int childCount = getChildCount();
338            for (int i = 0; i < childCount; i++) {
339                TaskView tv = (TaskView) getChildAt(i);
340                tv.enableHwLayers();
341            }
342        }
343        mHwLayersRefCount++;
344    }
345
346    /** Decrements the hw layer requirement ref count and disables the hw layers when we don't
347        need them anymore. */
348    void decHwLayersRefCount(String reason) {
349        Console.log(Constants.DebugFlags.UI.HwLayers,
350                "[TaskStackView|decHwLayersRefCount] refCount: " +
351                        mHwLayersRefCount + "->" + (mHwLayersRefCount - 1) + " " + reason);
352        mHwLayersRefCount--;
353        if (mHwLayersRefCount == 0) {
354            // Disable hw layers on each of the children
355            int childCount = getChildCount();
356            for (int i = 0; i < childCount; i++) {
357                TaskView tv = (TaskView) getChildAt(i);
358                tv.disableHwLayers();
359            }
360        } else if (mHwLayersRefCount < 0) {
361            new Throwable("Invalid hw layers ref count").printStackTrace();
362            Console.logError(getContext(), "Invalid HW layers ref count");
363        }
364    }
365
366    @Override
367    public void computeScroll() {
368        if (mScroller.computeScrollOffset()) {
369            setStackScroll(mScroller.getCurrY());
370            invalidate();
371
372            // If we just finished scrolling, then disable the hw layers
373            if (mScroller.isFinished()) {
374                decHwLayersRefCount("finishedFlingScroll");
375            }
376        }
377    }
378
379    @Override
380    public boolean onInterceptTouchEvent(MotionEvent ev) {
381        return mTouchHandler.onInterceptTouchEvent(ev);
382    }
383
384    @Override
385    public boolean onTouchEvent(MotionEvent ev) {
386        return mTouchHandler.onTouchEvent(ev);
387    }
388
389    @Override
390    public void dispatchDraw(Canvas canvas) {
391        Console.log(Constants.DebugFlags.UI.Draw, "[TaskStackView|dispatchDraw]", "",
392                Console.AnsiPurple);
393        synchronizeStackViewsWithModel();
394        super.dispatchDraw(canvas);
395    }
396
397    @Override
398    protected boolean drawChild(Canvas canvas, View child, long drawingTime) {
399        if (Constants.DebugFlags.App.EnableTaskStackClipping) {
400            TaskView tv = (TaskView) child;
401            TaskView nextTv = null;
402            int curIndex = indexOfChild(tv);
403            if (curIndex < (getChildCount() - 1)) {
404                // Clip against the next view (if we aren't animating its alpha)
405                nextTv = (TaskView) getChildAt(curIndex + 1);
406                if (nextTv.getAlpha() == 1f) {
407                    Rect curRect = tv.getClippingRect(Utilities.tmpRect, false);
408                    Rect nextRect = nextTv.getClippingRect(Utilities.tmpRect2, true);
409                    RecentsConfiguration config = RecentsConfiguration.getInstance();
410                    // The hit rects are relative to the task view, which needs to be offset by the
411                    // system bar height
412                    curRect.offset(0, config.systemInsets.top);
413                    nextRect.offset(0, config.systemInsets.top);
414                    // Compute the clip region
415                    Region clipRegion = new Region();
416                    clipRegion.op(curRect, Region.Op.UNION);
417                    clipRegion.op(nextRect, Region.Op.DIFFERENCE);
418                    // Clip the canvas
419                    int saveCount = canvas.save(Canvas.CLIP_SAVE_FLAG);
420                    canvas.clipRegion(clipRegion);
421                    boolean invalidate = super.drawChild(canvas, child, drawingTime);
422                    canvas.restoreToCount(saveCount);
423                    return invalidate;
424                }
425            }
426        }
427        return super.drawChild(canvas, child, drawingTime);
428    }
429
430    /** Computes the stack and task rects */
431    public void computeRects(int width, int height, int insetBottom) {
432        // Note: We let the stack view be the full height because we want the cards to go under the
433        //       navigation bar if possible.  However, the stack rects which we use to calculate
434        //       max scroll, etc. need to take the nav bar into account
435
436        // Compute the stack rects
437        mRect.set(0, 0, width, height);
438        mStackRect.set(mRect);
439        mStackRect.bottom -= insetBottom;
440
441        int smallestDimension = Math.min(width, height);
442        int padding = (int) (Constants.Values.TaskStackView.StackPaddingPct * smallestDimension / 2f);
443        mStackRect.inset(padding, padding);
444        mStackRectSansPeek.set(mStackRect);
445        mStackRectSansPeek.top += Constants.Values.TaskStackView.StackPeekHeightPct * mStackRect.height();
446
447        // Compute the task rect
448        int minHeight = (int) (mStackRect.height() -
449                (Constants.Values.TaskStackView.StackPeekHeightPct * mStackRect.height()));
450        int size = Math.min(minHeight, Math.min(mStackRect.width(), mStackRect.height()));
451        int centerX = mStackRect.centerX();
452        mTaskRect.set(centerX - size / 2, mStackRectSansPeek.top,
453                centerX + size / 2, mStackRectSansPeek.top + size);
454
455        // Update the scroll bounds
456        updateMinMaxScroll(false);
457    }
458
459    @Override
460    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
461        int width = MeasureSpec.getSize(widthMeasureSpec);
462        int height = MeasureSpec.getSize(heightMeasureSpec);
463        Console.log(Constants.DebugFlags.UI.MeasureAndLayout, "[TaskStackView|measure]",
464                "width: " + width + " height: " + height +
465                " awaitingFirstLayout: " + mAwaitingFirstLayout, Console.AnsiGreen);
466
467        // Compute our stack/task rects
468        RecentsConfiguration config = RecentsConfiguration.getInstance();
469        computeRects(width, height, config.systemInsets.bottom);
470
471        // Debug logging
472        if (Constants.DebugFlags.UI.MeasureAndLayout) {
473            Console.log("  [TaskStack|fullRect] " + mRect);
474            Console.log("  [TaskStack|stackRect] " + mStackRect);
475            Console.log("  [TaskStack|stackRectSansPeek] " + mStackRectSansPeek);
476            Console.log("  [TaskStack|taskRect] " + mTaskRect);
477        }
478
479        // If this is the first layout, then scroll to the front of the stack and synchronize the
480        // stack views immediately
481        if (mAwaitingFirstLayout) {
482            setStackScroll(mMaxScroll);
483            requestSynchronizeStackViewsWithModel();
484            synchronizeStackViewsWithModel();
485
486            // Animate the icon of the first task view
487            if (Constants.Values.TaskView.AnimateFrontTaskIconOnEnterRecents) {
488                TaskView tv = (TaskView) getChildAt(getChildCount() - 1);
489                if (tv != null) {
490                    tv.animateOnEnterRecents();
491                }
492            }
493        }
494
495        // Measure each of the children
496        int childCount = getChildCount();
497        for (int i = 0; i < childCount; i++) {
498            TaskView t = (TaskView) getChildAt(i);
499            t.measure(MeasureSpec.makeMeasureSpec(mTaskRect.width(), MeasureSpec.EXACTLY),
500                    MeasureSpec.makeMeasureSpec(mTaskRect.height(), MeasureSpec.EXACTLY));
501        }
502
503        setMeasuredDimension(width, height);
504    }
505
506    @Override
507    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
508        Console.log(Constants.DebugFlags.UI.MeasureAndLayout, "[TaskStackView|layout]",
509                "" + new Rect(left, top, right, bottom), Console.AnsiGreen);
510
511        // Debug logging
512        if (Constants.DebugFlags.UI.MeasureAndLayout) {
513            Console.log("  [TaskStack|fullRect] " + mRect);
514            Console.log("  [TaskStack|stackRect] " + mStackRect);
515            Console.log("  [TaskStack|stackRectSansPeek] " + mStackRectSansPeek);
516            Console.log("  [TaskStack|taskRect] " + mTaskRect);
517        }
518
519        // Layout each of the children
520        int childCount = getChildCount();
521        for (int i = 0; i < childCount; i++) {
522            TaskView t = (TaskView) getChildAt(i);
523            t.layout(mTaskRect.left, mStackRectSansPeek.top,
524                    mTaskRect.right, mStackRectSansPeek.top + mTaskRect.height());
525        }
526
527        if (!mAwaitingFirstLayout) {
528            requestSynchronizeStackViewsWithModel();
529        } else {
530            mAwaitingFirstLayout = false;
531        }
532    }
533
534    @Override
535    protected void onScrollChanged(int l, int t, int oldl, int oldt) {
536        super.onScrollChanged(l, t, oldl, oldt);
537        requestSynchronizeStackViewsWithModel();
538    }
539
540    public boolean isTransformedTouchPointInView(float x, float y, View child) {
541        return isTransformedTouchPointInView(x, y, child, null);
542    }
543
544    /**** TaskStackCallbacks Implementation ****/
545
546    @Override
547    public void onStackTaskAdded(TaskStack stack, Task t) {
548        requestSynchronizeStackViewsWithModel();
549    }
550
551    @Override
552    public void onStackTaskRemoved(TaskStack stack, Task t) {
553        // Remove the view associated with this task, we can't rely on updateTransforms
554        // to work here because the task is no longer in the list
555        int childCount = getChildCount();
556        for (int i = childCount - 1; i >= 0; i--) {
557            TaskView tv = (TaskView) getChildAt(i);
558            if (tv.getTask() == t) {
559                mViewPool.returnViewToPool(tv);
560                break;
561            }
562        }
563
564        updateMinMaxScroll(true);
565        requestSynchronizeStackViewsWithModel(Constants.Values.TaskStackView.Animation.TaskRemovedReshuffleDuration);
566    }
567
568    @Override
569    public void onStackFiltered(TaskStack stack) {
570        requestSynchronizeStackViewsWithModel();
571    }
572
573    @Override
574    public void onStackUnfiltered(TaskStack stack) {
575        requestSynchronizeStackViewsWithModel();
576    }
577
578    /**** ViewPoolConsumer Implementation ****/
579
580    @Override
581    public TaskView createView(Context context) {
582        Console.log(Constants.DebugFlags.ViewPool.PoolCallbacks, "[TaskStackView|createPoolView]");
583        return new TaskView(context);
584    }
585
586    @Override
587    public void prepareViewToEnterPool(TaskView tv) {
588        Task task = tv.getTask();
589        tv.resetViewProperties();
590        Console.log(Constants.DebugFlags.ViewPool.PoolCallbacks, "[TaskStackView|returnToPool]",
591                tv.getTask() + " tv: " + tv);
592
593        // Report that this tasks's data is no longer being used
594        RecentsTaskLoader loader = RecentsTaskLoader.getInstance();
595        loader.unloadTaskData(task);
596
597        // Detach the view from the hierarchy
598        detachViewFromParent(tv);
599
600        // Disable hw layers on this view
601        tv.disableHwLayers();
602    }
603
604    @Override
605    public void prepareViewToLeavePool(TaskView tv, Task prepareData, boolean isNewView) {
606        Console.log(Constants.DebugFlags.ViewPool.PoolCallbacks, "[TaskStackView|leavePool]",
607                "isNewView: " + isNewView);
608
609        // Setup and attach the view to the window
610        Task task = prepareData;
611        // We try and rebind the task (this MUST be done before the task filled)
612        tv.onTaskBound(task);
613        // Request that this tasks's data be filled
614        RecentsTaskLoader loader = RecentsTaskLoader.getInstance();
615        loader.loadTaskData(task);
616
617        // Find the index where this task should be placed in the children
618        int insertIndex = -1;
619        int childCount = getChildCount();
620        for (int i = 0; i < childCount; i++) {
621            Task tvTask = ((TaskView) getChildAt(i)).getTask();
622            if (mStack.containsTask(task) && (mStack.indexOfTask(task) < mStack.indexOfTask(tvTask))) {
623                insertIndex = i;
624                break;
625            }
626        }
627
628        // Add/attach the view to the hierarchy
629        Console.log(Constants.DebugFlags.ViewPool.PoolCallbacks, "  [TaskStackView|insertIndex]",
630                "" + insertIndex);
631        if (isNewView) {
632            addView(tv, insertIndex);
633
634            // Set the callbacks and listeners for this new view
635            tv.setOnClickListener(this);
636            tv.setCallbacks(this);
637        } else {
638            attachViewToParent(tv, insertIndex, tv.getLayoutParams());
639        }
640
641        // Enable hw layers on this view if hw layers are enabled on the stack
642        if (mHwLayersRefCount > 0) {
643            tv.enableHwLayers();
644        }
645    }
646
647    @Override
648    public boolean hasPreferredData(TaskView tv, Task preferredData) {
649        return (tv.getTask() == preferredData);
650    }
651
652    /**** TaskViewCallbacks Implementation ****/
653
654    @Override
655    public void onTaskIconClicked(TaskView tv) {
656        Console.log(Constants.DebugFlags.UI.ClickEvents, "[TaskStack|Clicked|Icon]",
657                tv.getTask() + " is currently filtered: " + mStack.hasFilteredTasks(),
658                Console.AnsiCyan);
659        if (Constants.DebugFlags.App.EnableTaskFiltering) {
660            if (mStack.hasFilteredTasks()) {
661                mStack.unfilterTasks();
662            } else {
663                mStack.filterTasks(tv.getTask());
664            }
665        } else {
666            Console.logError(getContext(), "Task Filtering TBD");
667        }
668    }
669
670    /**** View.OnClickListener Implementation ****/
671
672    @Override
673    public void onClick(View v) {
674        TaskView tv = (TaskView) v;
675        Task task = tv.getTask();
676        Console.log(Constants.DebugFlags.UI.ClickEvents, "[TaskStack|Clicked|Thumbnail]",
677                task + " cb: " + mCb);
678
679        if (mCb != null) {
680            mCb.onTaskLaunched(this, tv, mStack, task);
681        }
682    }
683}
684
685/* Handles touch events */
686class TaskStackViewTouchHandler implements SwipeHelper.Callback {
687    static int INACTIVE_POINTER_ID = -1;
688
689    TaskStackView mSv;
690    VelocityTracker mVelocityTracker;
691
692    boolean mIsScrolling;
693
694    int mInitialMotionX, mInitialMotionY;
695    int mLastMotionX, mLastMotionY;
696    int mActivePointerId = INACTIVE_POINTER_ID;
697    TaskView mActiveTaskView = null;
698
699    int mTotalScrollMotion;
700    int mMinimumVelocity;
701    int mMaximumVelocity;
702    // The scroll touch slop is used to calculate when we start scrolling
703    int mScrollTouchSlop;
704    // The page touch slop is used to calculate when we start swiping
705    float mPagingTouchSlop;
706
707    SwipeHelper mSwipeHelper;
708    boolean mInterceptedBySwipeHelper;
709
710    public TaskStackViewTouchHandler(Context context, TaskStackView sv) {
711        ViewConfiguration configuration = ViewConfiguration.get(context);
712        mMinimumVelocity = configuration.getScaledMinimumFlingVelocity();
713        mMaximumVelocity = configuration.getScaledMaximumFlingVelocity();
714        mScrollTouchSlop = configuration.getScaledTouchSlop();
715        mPagingTouchSlop = configuration.getScaledPagingTouchSlop();
716        mSv = sv;
717
718
719        float densityScale = context.getResources().getDisplayMetrics().density;
720        mSwipeHelper = new SwipeHelper(SwipeHelper.X, this, densityScale, mPagingTouchSlop);
721        mSwipeHelper.setMinAlpha(1f);
722    }
723
724    /** Velocity tracker helpers */
725    void initOrResetVelocityTracker() {
726        if (mVelocityTracker == null) {
727            mVelocityTracker = VelocityTracker.obtain();
728        } else {
729            mVelocityTracker.clear();
730        }
731    }
732    void initVelocityTrackerIfNotExists() {
733        if (mVelocityTracker == null) {
734            mVelocityTracker = VelocityTracker.obtain();
735        }
736    }
737    void recycleVelocityTracker() {
738        if (mVelocityTracker != null) {
739            mVelocityTracker.recycle();
740            mVelocityTracker = null;
741        }
742    }
743
744    /** Returns the view at the specified coordinates */
745    TaskView findViewAtPoint(int x, int y) {
746        int childCount = mSv.getChildCount();
747        for (int i = childCount - 1; i >= 0; i--) {
748            TaskView tv = (TaskView) mSv.getChildAt(i);
749            if (tv.getVisibility() == View.VISIBLE) {
750                if (mSv.isTransformedTouchPointInView(x, y, tv)) {
751                    return tv;
752                }
753            }
754        }
755        return null;
756    }
757
758    /** Touch preprocessing for handling below */
759    public boolean onInterceptTouchEvent(MotionEvent ev) {
760        Console.log(Constants.DebugFlags.UI.TouchEvents,
761                "[TaskStackViewTouchHandler|interceptTouchEvent]",
762                Console.motionEventActionToString(ev.getAction()), Console.AnsiBlue);
763
764        // Return early if we have no children
765        boolean hasChildren = (mSv.getChildCount() > 0);
766        if (!hasChildren) {
767            return false;
768        }
769
770        // Pass through to swipe helper if we are swiping
771        mInterceptedBySwipeHelper = mSwipeHelper.onInterceptTouchEvent(ev);
772        if (mInterceptedBySwipeHelper) {
773            return true;
774        }
775
776        boolean wasScrolling = !mSv.mScroller.isFinished() ||
777                (mSv.mScrollAnimator != null && mSv.mScrollAnimator.isRunning());
778        int action = ev.getAction();
779        switch (action & MotionEvent.ACTION_MASK) {
780            case MotionEvent.ACTION_DOWN: {
781                // Save the touch down info
782                mInitialMotionX = mLastMotionX = (int) ev.getX();
783                mInitialMotionY = mLastMotionY = (int) ev.getY();
784                mActivePointerId = ev.getPointerId(0);
785                mActiveTaskView = findViewAtPoint(mLastMotionX, mLastMotionY);
786                // Stop the current scroll if it is still flinging
787                mSv.abortScroller();
788                mSv.abortBoundScrollAnimation();
789                // Initialize the velocity tracker
790                initOrResetVelocityTracker();
791                mVelocityTracker.addMovement(ev);
792                // Check if the scroller is finished yet
793                mIsScrolling = !mSv.mScroller.isFinished();
794                break;
795            }
796            case MotionEvent.ACTION_MOVE: {
797                if (mActivePointerId == INACTIVE_POINTER_ID) break;
798
799                int activePointerIndex = ev.findPointerIndex(mActivePointerId);
800                int y = (int) ev.getY(activePointerIndex);
801                int x = (int) ev.getX(activePointerIndex);
802                if (Math.abs(y - mInitialMotionY) > mScrollTouchSlop) {
803                    // Save the touch move info
804                    mIsScrolling = true;
805                    // Initialize the velocity tracker if necessary
806                    initVelocityTrackerIfNotExists();
807                    mVelocityTracker.addMovement(ev);
808                    // Disallow parents from intercepting touch events
809                    final ViewParent parent = mSv.getParent();
810                    if (parent != null) {
811                        parent.requestDisallowInterceptTouchEvent(true);
812                    }
813                    // Enable HW layers
814                    mSv.addHwLayersRefCount("stackScroll");
815                }
816
817                mLastMotionX = x;
818                mLastMotionY = y;
819                break;
820            }
821            case MotionEvent.ACTION_CANCEL:
822            case MotionEvent.ACTION_UP: {
823                // Animate the scroll back if we've cancelled
824                mSv.animateBoundScroll(Constants.Values.TaskStackView.Animation.SnapScrollBackDuration);
825                // Disable HW layers
826                if (mIsScrolling) {
827                    mSv.decHwLayersRefCount("stackScroll");
828                }
829                // Reset the drag state and the velocity tracker
830                mIsScrolling = false;
831                mActivePointerId = INACTIVE_POINTER_ID;
832                mActiveTaskView = null;
833                mTotalScrollMotion = 0;
834                recycleVelocityTracker();
835                break;
836            }
837        }
838
839        return wasScrolling || mIsScrolling;
840    }
841
842    /** Handles touch events once we have intercepted them */
843    public boolean onTouchEvent(MotionEvent ev) {
844        Console.log(Constants.DebugFlags.TaskStack.SynchronizeViewsWithModel,
845                "[TaskStackViewTouchHandler|touchEvent]",
846                Console.motionEventActionToString(ev.getAction()), Console.AnsiBlue);
847
848        // Short circuit if we have no children
849        boolean hasChildren = (mSv.getChildCount() > 0);
850        if (!hasChildren) {
851            return false;
852        }
853
854        // Pass through to swipe helper if we are swiping
855        if (mInterceptedBySwipeHelper && mSwipeHelper.onTouchEvent(ev)) {
856            return true;
857        }
858
859        // Update the velocity tracker
860        initVelocityTrackerIfNotExists();
861        mVelocityTracker.addMovement(ev);
862
863        int action = ev.getAction();
864        switch (action & MotionEvent.ACTION_MASK) {
865            case MotionEvent.ACTION_DOWN: {
866                // Save the touch down info
867                mInitialMotionX = mLastMotionX = (int) ev.getX();
868                mInitialMotionY = mLastMotionY = (int) ev.getY();
869                mActivePointerId = ev.getPointerId(0);
870                mActiveTaskView = findViewAtPoint(mLastMotionX, mLastMotionY);
871                // Stop the current scroll if it is still flinging
872                mSv.abortScroller();
873                mSv.abortBoundScrollAnimation();
874                // Initialize the velocity tracker
875                initOrResetVelocityTracker();
876                mVelocityTracker.addMovement(ev);
877                // Disallow parents from intercepting touch events
878                final ViewParent parent = mSv.getParent();
879                if (parent != null) {
880                    parent.requestDisallowInterceptTouchEvent(true);
881                }
882                break;
883            }
884            case MotionEvent.ACTION_MOVE: {
885                if (mActivePointerId == INACTIVE_POINTER_ID) break;
886
887                int activePointerIndex = ev.findPointerIndex(mActivePointerId);
888                int x = (int) ev.getX(activePointerIndex);
889                int y = (int) ev.getY(activePointerIndex);
890                int deltaY = mLastMotionY - y;
891                if (!mIsScrolling) {
892                    if (Math.abs(y - mInitialMotionY) > mScrollTouchSlop) {
893                        mIsScrolling = true;
894                        // Initialize the velocity tracker
895                        initOrResetVelocityTracker();
896                        mVelocityTracker.addMovement(ev);
897                        // Disallow parents from intercepting touch events
898                        final ViewParent parent = mSv.getParent();
899                        if (parent != null) {
900                            parent.requestDisallowInterceptTouchEvent(true);
901                        }
902                        // Enable HW layers
903                        mSv.addHwLayersRefCount("stackScroll");
904                    }
905                }
906                if (mIsScrolling) {
907                    mSv.setStackScroll(mSv.getStackScroll() + deltaY);
908                    if (mSv.isScrollOutOfBounds()) {
909                        mVelocityTracker.clear();
910                    }
911                }
912                mLastMotionX = x;
913                mLastMotionY = y;
914                mTotalScrollMotion += Math.abs(deltaY);
915                break;
916            }
917            case MotionEvent.ACTION_UP: {
918                final VelocityTracker velocityTracker = mVelocityTracker;
919                velocityTracker.computeCurrentVelocity(1000, mMaximumVelocity);
920                int velocity = (int) velocityTracker.getYVelocity(mActivePointerId);
921
922                if (mIsScrolling && (Math.abs(velocity) > mMinimumVelocity)) {
923                    // Enable HW layers on the stack
924                    mSv.addHwLayersRefCount("flingScroll");
925                    int overscrollRange = (int) (Math.min(1f,
926                            Math.abs((float) velocity / mMaximumVelocity)) *
927                            Constants.Values.TaskStackView.TaskStackOverscrollRange);
928
929                    Console.log(Constants.DebugFlags.UI.TouchEvents,
930                            "[TaskStackViewTouchHandler|fling]",
931                            "scroll: " + mSv.getStackScroll() + " velocity: " + velocity +
932                                    " maxVelocity: " + mMaximumVelocity +
933                                    " overscrollRange: " + overscrollRange,
934                            Console.AnsiGreen);
935
936                    // Fling scroll
937                    mSv.mScroller.fling(0, mSv.getStackScroll(),
938                            0, -velocity,
939                            0, 0,
940                            mSv.mMinScroll, mSv.mMaxScroll,
941                            0, overscrollRange);
942                    // Invalidate to kick off computeScroll
943                    mSv.invalidate();
944                } else if (mSv.isScrollOutOfBounds()) {
945                    // Animate the scroll back into bounds
946                    // XXX: Make this animation a function of the velocity OR distance
947                    mSv.animateBoundScroll(Constants.Values.TaskStackView.Animation.SnapScrollBackDuration);
948                }
949
950                if (mIsScrolling) {
951                    // Disable HW layers
952                    mSv.decHwLayersRefCount("stackScroll");
953                }
954                mActivePointerId = INACTIVE_POINTER_ID;
955                mIsScrolling = false;
956                mTotalScrollMotion = 0;
957                recycleVelocityTracker();
958                break;
959            }
960            case MotionEvent.ACTION_CANCEL: {
961                if (mIsScrolling) {
962                    // Disable HW layers
963                    mSv.decHwLayersRefCount("stackScroll");
964                }
965                if (mSv.isScrollOutOfBounds()) {
966                    // Animate the scroll back into bounds
967                    // XXX: Make this animation a function of the velocity OR distance
968                    mSv.animateBoundScroll(Constants.Values.TaskStackView.Animation.SnapScrollBackDuration);
969                }
970                mActivePointerId = INACTIVE_POINTER_ID;
971                mIsScrolling = false;
972                mTotalScrollMotion = 0;
973                recycleVelocityTracker();
974                break;
975            }
976        }
977        return true;
978    }
979
980    /**** SwipeHelper Implementation ****/
981
982    @Override
983    public View getChildAtPosition(MotionEvent ev) {
984        return findViewAtPoint((int) ev.getX(), (int) ev.getY());
985    }
986
987    @Override
988    public boolean canChildBeDismissed(View v) {
989        return true;
990    }
991
992    @Override
993    public void onBeginDrag(View v) {
994        // Enable HW layers
995        mSv.addHwLayersRefCount("swipeBegin");
996        // Disallow parents from intercepting touch events
997        final ViewParent parent = mSv.getParent();
998        if (parent != null) {
999            parent.requestDisallowInterceptTouchEvent(true);
1000        }
1001    }
1002
1003    @Override
1004    public void onChildDismissed(View v) {
1005        TaskView tv = (TaskView) v;
1006        Task task = tv.getTask();
1007        Activity activity = (Activity) mSv.getContext();
1008
1009        // We have to disable the listener to ensure that we
1010        // don't hit this again
1011        tv.animate().setListener(null);
1012
1013        // Remove the task from the view
1014        mSv.mStack.removeTask(task);
1015
1016        // Remove any stored data from the loader
1017        RecentsTaskLoader loader = RecentsTaskLoader.getInstance();
1018        loader.deleteTaskData(task);
1019
1020        // Remove the task from activity manager
1021        final ActivityManager am = (ActivityManager)
1022                activity.getSystemService(Context.ACTIVITY_SERVICE);
1023        if (am != null) {
1024            am.removeTask(tv.getTask().key.id,
1025                    ActivityManager.REMOVE_TASK_KILL_PROCESS);
1026        }
1027
1028        // If there are no remaining tasks, then just close the activity
1029        if (mSv.mStack.getTaskCount() == 0) {
1030            activity.finish();
1031        }
1032
1033        // Disable HW layers
1034        mSv.decHwLayersRefCount("swipeComplete");
1035    }
1036
1037    @Override
1038    public void onSnapBackCompleted(View v) {
1039        // Do Nothing
1040    }
1041
1042    @Override
1043    public void onDragCancelled(View v) {
1044        // Disable HW layers
1045        mSv.decHwLayersRefCount("swipeCancelled");
1046    }
1047}
1048