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