1/*
2 * Copyright (C) 2015 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.TimeInterpolator;
22import android.animation.ValueAnimator;
23import android.content.Context;
24import android.content.res.Configuration;
25import android.content.res.Resources;
26import android.util.Log;
27import android.view.View;
28import android.view.animation.Interpolator;
29import android.view.animation.PathInterpolator;
30
31import com.android.systemui.Interpolators;
32import com.android.systemui.R;
33import com.android.systemui.recents.Recents;
34import com.android.systemui.recents.RecentsActivityLaunchState;
35import com.android.systemui.recents.RecentsConfiguration;
36import com.android.systemui.recents.misc.ReferenceCountedTrigger;
37import com.android.systemui.recents.model.Task;
38import com.android.systemui.recents.model.TaskStack;
39
40import java.util.ArrayList;
41import java.util.List;
42
43/**
44 * A helper class to create task view animations for {@link TaskView}s in a {@link TaskStackView},
45 * but not the contents of the {@link TaskView}s.
46 */
47public class TaskStackAnimationHelper {
48
49    /**
50     * Callbacks from the helper to coordinate view-content animations with view animations.
51     */
52    public interface Callbacks {
53        /**
54         * Callback to prepare for the start animation for the launch target {@link TaskView}.
55         */
56        void onPrepareLaunchTargetForEnterAnimation();
57
58        /**
59         * Callback to start the animation for the launch target {@link TaskView}.
60         */
61        void onStartLaunchTargetEnterAnimation(TaskViewTransform transform, int duration,
62                boolean screenPinningEnabled, ReferenceCountedTrigger postAnimationTrigger);
63
64        /**
65         * Callback to start the animation for the launch target {@link TaskView} when it is
66         * launched from Recents.
67         */
68        void onStartLaunchTargetLaunchAnimation(int duration, boolean screenPinningRequested,
69                ReferenceCountedTrigger postAnimationTrigger);
70
71        /**
72         * Callback to start the animation for the front {@link TaskView} if there is no launch
73         * target.
74         */
75        void onStartFrontTaskEnterAnimation(boolean screenPinningEnabled);
76    }
77
78    private static final int DOUBLE_FRAME_OFFSET_MS = 33;
79    private static final int FRAME_OFFSET_MS = 16;
80
81    private static final int ENTER_EXIT_NUM_ANIMATING_TASKS = 5;
82
83    private static final int ENTER_FROM_HOME_ALPHA_DURATION = 100;
84    public static final int ENTER_FROM_HOME_TRANSLATION_DURATION = 300;
85    private static final Interpolator ENTER_FROM_HOME_TRANSLATION_INTERPOLATOR =
86            Interpolators.LINEAR_OUT_SLOW_IN;
87    private static final Interpolator ENTER_FROM_HOME_ALPHA_INTERPOLATOR = Interpolators.LINEAR;
88
89    public static final int EXIT_TO_HOME_TRANSLATION_DURATION = 200;
90    private static final Interpolator EXIT_TO_HOME_TRANSLATION_INTERPOLATOR =
91            new PathInterpolator(0.4f, 0, 0.6f, 1f);
92
93    private static final int DISMISS_TASK_DURATION = 175;
94    private static final int DISMISS_ALL_TASKS_DURATION = 200;
95    private static final Interpolator DISMISS_ALL_TRANSLATION_INTERPOLATOR =
96            new PathInterpolator(0.4f, 0, 1f, 1f);
97
98    private static final Interpolator FOCUS_NEXT_TASK_INTERPOLATOR =
99            new PathInterpolator(0.4f, 0, 0, 1f);
100    private static final Interpolator FOCUS_IN_FRONT_NEXT_TASK_INTERPOLATOR =
101            new PathInterpolator(0, 0, 0, 1f);
102    private static final Interpolator FOCUS_BEHIND_NEXT_TASK_INTERPOLATOR =
103            Interpolators.LINEAR_OUT_SLOW_IN;
104
105    private static final Interpolator ENTER_WHILE_DOCKING_INTERPOLATOR =
106            Interpolators.LINEAR_OUT_SLOW_IN;
107
108    private TaskStackView mStackView;
109
110    private TaskViewTransform mTmpTransform = new TaskViewTransform();
111    private ArrayList<TaskViewTransform> mTmpCurrentTaskTransforms = new ArrayList<>();
112    private ArrayList<TaskViewTransform> mTmpFinalTaskTransforms = new ArrayList<>();
113
114    public TaskStackAnimationHelper(Context context, TaskStackView stackView) {
115        mStackView = stackView;
116    }
117
118    /**
119     * Prepares the stack views and puts them in their initial animation state while visible, before
120     * the in-app enter animations start (after the window-transition completes).
121     */
122    public void prepareForEnterAnimation() {
123        RecentsConfiguration config = Recents.getConfiguration();
124        RecentsActivityLaunchState launchState = config.getLaunchState();
125        Resources res = mStackView.getResources();
126        Resources appResources = mStackView.getContext().getApplicationContext().getResources();
127
128        TaskStackLayoutAlgorithm stackLayout = mStackView.getStackAlgorithm();
129        TaskStackViewScroller stackScroller = mStackView.getScroller();
130        TaskStack stack = mStackView.getStack();
131        Task launchTargetTask = stack.getLaunchTarget();
132
133        // Break early if there are no tasks
134        if (stack.getTaskCount() == 0) {
135            return;
136        }
137
138        int offscreenYOffset = stackLayout.mStackRect.height();
139        int taskViewAffiliateGroupEnterOffset = res.getDimensionPixelSize(
140                R.dimen.recents_task_stack_animation_affiliate_enter_offset);
141        int launchedWhileDockingOffset = res.getDimensionPixelSize(
142                R.dimen.recents_task_stack_animation_launched_while_docking_offset);
143        boolean isLandscape = appResources.getConfiguration().orientation
144                == Configuration.ORIENTATION_LANDSCAPE;
145
146        // Prepare each of the task views for their enter animation from front to back
147        List<TaskView> taskViews = mStackView.getTaskViews();
148        for (int i = taskViews.size() - 1; i >= 0; i--) {
149            TaskView tv = taskViews.get(i);
150            Task task = tv.getTask();
151            boolean currentTaskOccludesLaunchTarget = launchTargetTask != null &&
152                    launchTargetTask.group != null &&
153                    launchTargetTask.group.isTaskAboveTask(task, launchTargetTask);
154            boolean hideTask = launchTargetTask != null &&
155                    launchTargetTask.isFreeformTask() &&
156                    task.isFreeformTask();
157
158            // Get the current transform for the task, which will be used to position it offscreen
159            stackLayout.getStackTransform(task, stackScroller.getStackScroll(), mTmpTransform,
160                    null);
161
162            if (hideTask) {
163                tv.setVisibility(View.INVISIBLE);
164            } else if (launchState.launchedFromApp && !launchState.launchedViaDockGesture) {
165                if (task.isLaunchTarget) {
166                    tv.onPrepareLaunchTargetForEnterAnimation();
167                } else if (currentTaskOccludesLaunchTarget) {
168                    // Move the task view slightly lower so we can animate it in
169                    mTmpTransform.rect.offset(0, taskViewAffiliateGroupEnterOffset);
170                    mTmpTransform.alpha = 0f;
171                    mStackView.updateTaskViewToTransform(tv, mTmpTransform,
172                            AnimationProps.IMMEDIATE);
173                    tv.setClipViewInStack(false);
174                }
175            } else if (launchState.launchedFromHome) {
176                // Move the task view off screen (below) so we can animate it in
177                mTmpTransform.rect.offset(0, offscreenYOffset);
178                mTmpTransform.alpha = 0f;
179                mStackView.updateTaskViewToTransform(tv, mTmpTransform, AnimationProps.IMMEDIATE);
180            } else if (launchState.launchedViaDockGesture) {
181                int offset = isLandscape
182                        ? launchedWhileDockingOffset
183                        : (int) (offscreenYOffset * 0.9f);
184                mTmpTransform.rect.offset(0, offset);
185                mTmpTransform.alpha = 0f;
186                mStackView.updateTaskViewToTransform(tv, mTmpTransform, AnimationProps.IMMEDIATE);
187            }
188        }
189    }
190
191    /**
192     * Starts the in-app enter animation, which animates the {@link TaskView}s to their final places
193     * depending on how Recents was triggered.
194     */
195    public void startEnterAnimation(final ReferenceCountedTrigger postAnimationTrigger) {
196        RecentsConfiguration config = Recents.getConfiguration();
197        RecentsActivityLaunchState launchState = config.getLaunchState();
198        Resources res = mStackView.getResources();
199        Resources appRes = mStackView.getContext().getApplicationContext().getResources();
200
201        TaskStackLayoutAlgorithm stackLayout = mStackView.getStackAlgorithm();
202        TaskStackViewScroller stackScroller = mStackView.getScroller();
203        TaskStack stack = mStackView.getStack();
204        Task launchTargetTask = stack.getLaunchTarget();
205
206        // Break early if there are no tasks
207        if (stack.getTaskCount() == 0) {
208            return;
209        }
210
211        int taskViewEnterFromAppDuration = res.getInteger(
212                R.integer.recents_task_enter_from_app_duration);
213        int taskViewEnterFromAffiliatedAppDuration = res.getInteger(
214                R.integer.recents_task_enter_from_affiliated_app_duration);
215        int dockGestureAnimDuration = appRes.getInteger(
216                R.integer.long_press_dock_anim_duration);
217
218        // Create enter animations for each of the views from front to back
219        List<TaskView> taskViews = mStackView.getTaskViews();
220        int taskViewCount = taskViews.size();
221        for (int i = taskViewCount - 1; i >= 0; i--) {
222            int taskIndexFromFront = taskViewCount - i - 1;
223            int taskIndexFromBack = i;
224            final TaskView tv = taskViews.get(i);
225            Task task = tv.getTask();
226            boolean currentTaskOccludesLaunchTarget = launchTargetTask != null &&
227                    launchTargetTask.group != null &&
228                    launchTargetTask.group.isTaskAboveTask(task, launchTargetTask);
229
230            // Get the current transform for the task, which will be updated to the final transform
231            // to animate to depending on how recents was invoked
232            stackLayout.getStackTransform(task, stackScroller.getStackScroll(), mTmpTransform,
233                    null);
234
235            if (launchState.launchedFromApp && !launchState.launchedViaDockGesture) {
236                if (task.isLaunchTarget) {
237                    tv.onStartLaunchTargetEnterAnimation(mTmpTransform,
238                            taskViewEnterFromAppDuration, mStackView.mScreenPinningEnabled,
239                            postAnimationTrigger);
240                } else {
241                    // Animate the task up if it was occluding the launch target
242                    if (currentTaskOccludesLaunchTarget) {
243                        AnimationProps taskAnimation = new AnimationProps(
244                                taskViewEnterFromAffiliatedAppDuration, Interpolators.ALPHA_IN,
245                                new AnimatorListenerAdapter() {
246                                    @Override
247                                    public void onAnimationEnd(Animator animation) {
248                                        postAnimationTrigger.decrement();
249                                        tv.setClipViewInStack(true);
250                                    }
251                                });
252                        postAnimationTrigger.increment();
253                        mStackView.updateTaskViewToTransform(tv, mTmpTransform, taskAnimation);
254                    }
255                }
256
257            } else if (launchState.launchedFromHome) {
258                // Animate the tasks up, but offset the animations to be relative to the front-most
259                // task animation
260                AnimationProps taskAnimation = new AnimationProps()
261                        .setInitialPlayTime(AnimationProps.BOUNDS,
262                                Math.min(ENTER_EXIT_NUM_ANIMATING_TASKS, taskIndexFromFront) *
263                                        DOUBLE_FRAME_OFFSET_MS)
264                        .setStartDelay(AnimationProps.ALPHA,
265                                Math.min(ENTER_EXIT_NUM_ANIMATING_TASKS, taskIndexFromFront) *
266                                        FRAME_OFFSET_MS)
267                        .setDuration(AnimationProps.BOUNDS, ENTER_FROM_HOME_TRANSLATION_DURATION)
268                        .setDuration(AnimationProps.ALPHA, ENTER_FROM_HOME_ALPHA_DURATION)
269                        .setInterpolator(AnimationProps.BOUNDS,
270                                ENTER_FROM_HOME_TRANSLATION_INTERPOLATOR)
271                        .setInterpolator(AnimationProps.ALPHA, ENTER_FROM_HOME_ALPHA_INTERPOLATOR)
272                        .setListener(postAnimationTrigger.decrementOnAnimationEnd());
273                postAnimationTrigger.increment();
274                mStackView.updateTaskViewToTransform(tv, mTmpTransform, taskAnimation);
275                if (i == taskViewCount - 1) {
276                    tv.onStartFrontTaskEnterAnimation(mStackView.mScreenPinningEnabled);
277                }
278            } else if (launchState.launchedViaDockGesture) {
279                // Animate the tasks up - add some delay to match the divider animation
280                AnimationProps taskAnimation = new AnimationProps()
281                        .setDuration(AnimationProps.BOUNDS, dockGestureAnimDuration +
282                                (taskIndexFromBack * DOUBLE_FRAME_OFFSET_MS))
283                        .setInterpolator(AnimationProps.BOUNDS,
284                                ENTER_WHILE_DOCKING_INTERPOLATOR)
285                        .setStartDelay(AnimationProps.BOUNDS, 48)
286                        .setListener(postAnimationTrigger.decrementOnAnimationEnd());
287                postAnimationTrigger.increment();
288                mStackView.updateTaskViewToTransform(tv, mTmpTransform, taskAnimation);
289            }
290        }
291    }
292
293    /**
294     * Starts an in-app animation to hide all the task views so that we can transition back home.
295     */
296    public void startExitToHomeAnimation(boolean animated,
297            ReferenceCountedTrigger postAnimationTrigger) {
298        TaskStackLayoutAlgorithm stackLayout = mStackView.getStackAlgorithm();
299        TaskStack stack = mStackView.getStack();
300
301        // Break early if there are no tasks
302        if (stack.getTaskCount() == 0) {
303            return;
304        }
305
306        int offscreenYOffset = stackLayout.mStackRect.height();
307
308        // Create the animations for each of the tasks
309        List<TaskView> taskViews = mStackView.getTaskViews();
310        int taskViewCount = taskViews.size();
311        for (int i = 0; i < taskViewCount; i++) {
312            int taskIndexFromFront = taskViewCount - i - 1;
313            TaskView tv = taskViews.get(i);
314            Task task = tv.getTask();
315
316            if (mStackView.isIgnoredTask(task)) {
317                continue;
318            }
319
320            // Animate the tasks down
321            AnimationProps taskAnimation;
322            if (animated) {
323                int delay = Math.min(ENTER_EXIT_NUM_ANIMATING_TASKS , taskIndexFromFront) *
324                        DOUBLE_FRAME_OFFSET_MS;
325                taskAnimation = new AnimationProps()
326                        .setStartDelay(AnimationProps.BOUNDS, delay)
327                        .setDuration(AnimationProps.BOUNDS, EXIT_TO_HOME_TRANSLATION_DURATION)
328                        .setInterpolator(AnimationProps.BOUNDS,
329                                EXIT_TO_HOME_TRANSLATION_INTERPOLATOR)
330                        .setListener(postAnimationTrigger.decrementOnAnimationEnd());
331                postAnimationTrigger.increment();
332            } else {
333                taskAnimation = AnimationProps.IMMEDIATE;
334            }
335
336            mTmpTransform.fillIn(tv);
337            mTmpTransform.rect.offset(0, offscreenYOffset);
338            mStackView.updateTaskViewToTransform(tv, mTmpTransform, taskAnimation);
339        }
340    }
341
342    /**
343     * Starts the animation for the launching task view, hiding any tasks that might occlude the
344     * window transition for the launching task.
345     */
346    public void startLaunchTaskAnimation(TaskView launchingTaskView, boolean screenPinningRequested,
347            final ReferenceCountedTrigger postAnimationTrigger) {
348        Resources res = mStackView.getResources();
349
350        int taskViewExitToAppDuration = res.getInteger(
351                R.integer.recents_task_exit_to_app_duration);
352        int taskViewAffiliateGroupEnterOffset = res.getDimensionPixelSize(
353                R.dimen.recents_task_stack_animation_affiliate_enter_offset);
354
355        Task launchingTask = launchingTaskView.getTask();
356        List<TaskView> taskViews = mStackView.getTaskViews();
357        int taskViewCount = taskViews.size();
358        for (int i = 0; i < taskViewCount; i++) {
359            TaskView tv = taskViews.get(i);
360            Task task = tv.getTask();
361            boolean currentTaskOccludesLaunchTarget = launchingTask != null &&
362                    launchingTask.group != null &&
363                    launchingTask.group.isTaskAboveTask(task, launchingTask);
364
365            if (tv == launchingTaskView) {
366                tv.setClipViewInStack(false);
367                postAnimationTrigger.addLastDecrementRunnable(new Runnable() {
368                    @Override
369                    public void run() {
370                        tv.setClipViewInStack(true);
371                    }
372                });
373                tv.onStartLaunchTargetLaunchAnimation(taskViewExitToAppDuration,
374                        screenPinningRequested, postAnimationTrigger);
375            } else if (currentTaskOccludesLaunchTarget) {
376                // Animate this task out of view
377                AnimationProps taskAnimation = new AnimationProps(
378                        taskViewExitToAppDuration, Interpolators.ALPHA_OUT,
379                        postAnimationTrigger.decrementOnAnimationEnd());
380                postAnimationTrigger.increment();
381
382                mTmpTransform.fillIn(tv);
383                mTmpTransform.alpha = 0f;
384                mTmpTransform.rect.offset(0, taskViewAffiliateGroupEnterOffset);
385                mStackView.updateTaskViewToTransform(tv, mTmpTransform, taskAnimation);
386            }
387        }
388    }
389
390    /**
391     * Starts the delete animation for the specified {@link TaskView}.
392     */
393    public void startDeleteTaskAnimation(final TaskView deleteTaskView,
394            final ReferenceCountedTrigger postAnimationTrigger) {
395        TaskStackViewTouchHandler touchHandler = mStackView.getTouchHandler();
396        touchHandler.onBeginManualDrag(deleteTaskView);
397
398        postAnimationTrigger.increment();
399        postAnimationTrigger.addLastDecrementRunnable(() -> {
400            touchHandler.onChildDismissed(deleteTaskView);
401        });
402
403        final float dismissSize = touchHandler.getScaledDismissSize();
404        ValueAnimator animator = ValueAnimator.ofFloat(0f, 1f);
405        animator.setDuration(400);
406        animator.addUpdateListener((animation) -> {
407            float progress = (Float) animation.getAnimatedValue();
408            deleteTaskView.setTranslationX(progress * dismissSize);
409            touchHandler.updateSwipeProgress(deleteTaskView, true, progress);
410        });
411        animator.addListener(new AnimatorListenerAdapter() {
412            @Override
413            public void onAnimationEnd(Animator animation) {
414                postAnimationTrigger.decrement();
415            }
416        });
417        animator.start();
418    }
419
420    /**
421     * Starts the delete animation for all the {@link TaskView}s.
422     */
423    public void startDeleteAllTasksAnimation(final List<TaskView> taskViews,
424                                             final ReferenceCountedTrigger postAnimationTrigger) {
425        TaskStackLayoutAlgorithm stackLayout = mStackView.getStackAlgorithm();
426
427        int offscreenXOffset = mStackView.getMeasuredWidth() - stackLayout.mTaskRect.left;
428
429        int taskViewCount = taskViews.size();
430        for (int i = taskViewCount - 1; i >= 0; i--) {
431            TaskView tv = taskViews.get(i);
432            int taskIndexFromFront = taskViewCount - i - 1;
433            int startDelay = taskIndexFromFront * DOUBLE_FRAME_OFFSET_MS;
434
435            // Disabling clipping with the stack while the view is animating away
436            tv.setClipViewInStack(false);
437
438            // Compose the new animation and transform and star the animation
439            AnimationProps taskAnimation = new AnimationProps(startDelay,
440                    DISMISS_ALL_TASKS_DURATION, DISMISS_ALL_TRANSLATION_INTERPOLATOR,
441                    new AnimatorListenerAdapter() {
442                @Override
443                public void onAnimationEnd(Animator animation) {
444                    postAnimationTrigger.decrement();
445
446                    // Re-enable clipping with the stack (we will reuse this view)
447                    tv.setClipViewInStack(true);
448                }
449            });
450            postAnimationTrigger.increment();
451
452            mTmpTransform.fillIn(tv);
453            mTmpTransform.rect.offset(offscreenXOffset, 0);
454            mStackView.updateTaskViewToTransform(tv, mTmpTransform, taskAnimation);
455        }
456    }
457
458    /**
459     * Starts the animation to focus the next {@link TaskView} when paging through recents.
460     *
461     * @return whether or not this will trigger a scroll in the stack
462     */
463    public boolean startScrollToFocusedTaskAnimation(Task newFocusedTask,
464            boolean requestViewFocus) {
465        TaskStackLayoutAlgorithm stackLayout = mStackView.getStackAlgorithm();
466        TaskStackViewScroller stackScroller = mStackView.getScroller();
467        TaskStack stack = mStackView.getStack();
468
469        final float curScroll = stackScroller.getStackScroll();
470        final float newScroll = stackScroller.getBoundedStackScroll(
471                stackLayout.getStackScrollForTask(newFocusedTask));
472        boolean willScrollToFront = newScroll > curScroll;
473        boolean willScroll = Float.compare(newScroll, curScroll) != 0;
474
475        // Get the current set of task transforms
476        int taskViewCount = mStackView.getTaskViews().size();
477        ArrayList<Task> stackTasks = stack.getStackTasks();
478        mStackView.getCurrentTaskTransforms(stackTasks, mTmpCurrentTaskTransforms);
479
480        // Pick up the newly visible views after the scroll
481        mStackView.bindVisibleTaskViews(newScroll);
482
483        // Update the internal state
484        stackLayout.setFocusState(TaskStackLayoutAlgorithm.STATE_FOCUSED);
485        stackScroller.setStackScroll(newScroll, null /* animation */);
486        mStackView.cancelDeferredTaskViewLayoutAnimation();
487
488        // Get the final set of task transforms
489        mStackView.getLayoutTaskTransforms(newScroll, stackLayout.getFocusState(), stackTasks,
490                true /* ignoreTaskOverrides */, mTmpFinalTaskTransforms);
491
492        // Focus the task view
493        TaskView newFocusedTaskView = mStackView.getChildViewForTask(newFocusedTask);
494        if (newFocusedTaskView == null) {
495            // Log the error if we have no task view, and skip the animation
496            Log.e("TaskStackAnimationHelper", "b/27389156 null-task-view prebind:" + taskViewCount +
497                    " postbind:" + mStackView.getTaskViews().size() + " prescroll:" + curScroll +
498                    " postscroll: " + newScroll);
499            return false;
500        }
501        newFocusedTaskView.setFocusedState(true, requestViewFocus);
502
503        // Setup the end listener to return all the hidden views to the view pool after the
504        // focus animation
505        ReferenceCountedTrigger postAnimTrigger = new ReferenceCountedTrigger();
506        postAnimTrigger.addLastDecrementRunnable(new Runnable() {
507            @Override
508            public void run() {
509                mStackView.bindVisibleTaskViews(newScroll);
510            }
511        });
512
513        List<TaskView> taskViews = mStackView.getTaskViews();
514        taskViewCount = taskViews.size();
515        int newFocusTaskViewIndex = taskViews.indexOf(newFocusedTaskView);
516        for (int i = 0; i < taskViewCount; i++) {
517            TaskView tv = taskViews.get(i);
518            Task task = tv.getTask();
519
520            if (mStackView.isIgnoredTask(task)) {
521                continue;
522            }
523
524            int taskIndex = stackTasks.indexOf(task);
525            TaskViewTransform fromTransform = mTmpCurrentTaskTransforms.get(taskIndex);
526            TaskViewTransform toTransform = mTmpFinalTaskTransforms.get(taskIndex);
527
528            // Update the task to the initial state (for the newly picked up tasks)
529            mStackView.updateTaskViewToTransform(tv, fromTransform, AnimationProps.IMMEDIATE);
530
531            int duration;
532            Interpolator interpolator;
533            if (willScrollToFront) {
534                duration = calculateStaggeredAnimDuration(i);
535                interpolator = FOCUS_BEHIND_NEXT_TASK_INTERPOLATOR;
536            } else {
537                if (i < newFocusTaskViewIndex) {
538                    duration = 150 + ((newFocusTaskViewIndex - i - 1) * 50);
539                    interpolator = FOCUS_BEHIND_NEXT_TASK_INTERPOLATOR;
540                } else if (i > newFocusTaskViewIndex) {
541                    duration = Math.max(100, 150 - ((i - newFocusTaskViewIndex - 1) * 50));
542                    interpolator = FOCUS_IN_FRONT_NEXT_TASK_INTERPOLATOR;
543                } else {
544                    duration = 200;
545                    interpolator = FOCUS_NEXT_TASK_INTERPOLATOR;
546                }
547            }
548
549            AnimationProps anim = new AnimationProps()
550                    .setDuration(AnimationProps.BOUNDS, duration)
551                    .setInterpolator(AnimationProps.BOUNDS, interpolator)
552                    .setListener(postAnimTrigger.decrementOnAnimationEnd());
553            postAnimTrigger.increment();
554            mStackView.updateTaskViewToTransform(tv, toTransform, anim);
555        }
556        return willScroll;
557    }
558
559    /**
560     * Starts the animation to go to the initial stack layout with a task focused.  In addition, the
561     * previous task will be animated in after the scroll completes.
562     */
563    public void startNewStackScrollAnimation(TaskStack newStack,
564            ReferenceCountedTrigger animationTrigger) {
565        TaskStackLayoutAlgorithm stackLayout = mStackView.getStackAlgorithm();
566        TaskStackViewScroller stackScroller = mStackView.getScroller();
567
568        // Get the current set of task transforms
569        ArrayList<Task> stackTasks = newStack.getStackTasks();
570        mStackView.getCurrentTaskTransforms(stackTasks, mTmpCurrentTaskTransforms);
571
572        // Update the stack
573        mStackView.setTasks(newStack, false /* allowNotifyStackChanges */);
574        mStackView.updateLayoutAlgorithm(false /* boundScroll */);
575
576        // Pick up the newly visible views after the scroll
577        final float newScroll = stackLayout.mInitialScrollP;
578        mStackView.bindVisibleTaskViews(newScroll);
579
580        // Update the internal state
581        stackLayout.setFocusState(TaskStackLayoutAlgorithm.STATE_UNFOCUSED);
582        stackLayout.setTaskOverridesForInitialState(newStack, true /* ignoreScrollToFront */);
583        stackScroller.setStackScroll(newScroll);
584        mStackView.cancelDeferredTaskViewLayoutAnimation();
585
586        // Get the final set of task transforms
587        mStackView.getLayoutTaskTransforms(newScroll, stackLayout.getFocusState(), stackTasks,
588                false /* ignoreTaskOverrides */, mTmpFinalTaskTransforms);
589
590        // Hide the front most task view until the scroll is complete
591        Task frontMostTask = newStack.getStackFrontMostTask(false /* includeFreeform */);
592        final TaskView frontMostTaskView = mStackView.getChildViewForTask(frontMostTask);
593        final TaskViewTransform frontMostTransform = mTmpFinalTaskTransforms.get(
594                stackTasks.indexOf(frontMostTask));
595        if (frontMostTaskView != null) {
596            mStackView.updateTaskViewToTransform(frontMostTaskView,
597                    stackLayout.getFrontOfStackTransform(), AnimationProps.IMMEDIATE);
598        }
599
600        // Setup the end listener to return all the hidden views to the view pool after the
601        // focus animation
602        animationTrigger.addLastDecrementRunnable(new Runnable() {
603            @Override
604            public void run() {
605                mStackView.bindVisibleTaskViews(newScroll);
606
607                // Now, animate in the front-most task
608                if (frontMostTaskView != null) {
609                    mStackView.updateTaskViewToTransform(frontMostTaskView, frontMostTransform,
610                            new AnimationProps(75, 250, FOCUS_BEHIND_NEXT_TASK_INTERPOLATOR));
611                }
612            }
613        });
614
615        List<TaskView> taskViews = mStackView.getTaskViews();
616        int taskViewCount = taskViews.size();
617        for (int i = 0; i < taskViewCount; i++) {
618            TaskView tv = taskViews.get(i);
619            Task task = tv.getTask();
620
621            if (mStackView.isIgnoredTask(task)) {
622                continue;
623            }
624            if (task == frontMostTask && frontMostTaskView != null) {
625                continue;
626            }
627
628            int taskIndex = stackTasks.indexOf(task);
629            TaskViewTransform fromTransform = mTmpCurrentTaskTransforms.get(taskIndex);
630            TaskViewTransform toTransform = mTmpFinalTaskTransforms.get(taskIndex);
631
632            // Update the task to the initial state (for the newly picked up tasks)
633            mStackView.updateTaskViewToTransform(tv, fromTransform, AnimationProps.IMMEDIATE);
634
635            int duration = calculateStaggeredAnimDuration(i);
636            Interpolator interpolator = FOCUS_BEHIND_NEXT_TASK_INTERPOLATOR;
637
638            AnimationProps anim = new AnimationProps()
639                    .setDuration(AnimationProps.BOUNDS, duration)
640                    .setInterpolator(AnimationProps.BOUNDS, interpolator)
641                    .setListener(animationTrigger.decrementOnAnimationEnd());
642            animationTrigger.increment();
643            mStackView.updateTaskViewToTransform(tv, toTransform, anim);
644        }
645    }
646
647    /**
648     * Caclulates a staggered duration for {@link #startScrollToFocusedTaskAnimation} and
649     * {@link #startNewStackScrollAnimation}.
650     */
651    private int calculateStaggeredAnimDuration(int i) {
652        return Math.max(100, 100 + ((i - 1) * 50));
653    }
654}
655