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 
17 package com.android.systemui.recents.views;
18 
19 import android.animation.Animator;
20 import android.animation.AnimatorListenerAdapter;
21 import android.animation.ValueAnimator;
22 import android.content.Context;
23 import android.content.res.Configuration;
24 import android.content.res.Resources;
25 import android.util.Log;
26 import android.view.animation.Interpolator;
27 import android.view.animation.PathInterpolator;
28 
29 import com.android.systemui.Interpolators;
30 import com.android.systemui.R;
31 import com.android.systemui.recents.LegacyRecentsImpl;
32 import com.android.systemui.recents.RecentsActivityLaunchState;
33 import com.android.systemui.recents.RecentsConfiguration;
34 import com.android.systemui.recents.RecentsDebugFlags;
35 import com.android.systemui.recents.events.EventBus;
36 import com.android.systemui.recents.events.component.SetWaitingForTransitionStartEvent;
37 import com.android.systemui.recents.misc.ReferenceCountedTrigger;
38 import com.android.systemui.shared.recents.model.Task;
39 import com.android.systemui.recents.model.TaskStack;
40 import com.android.systemui.recents.views.lowram.TaskStackLowRamLayoutAlgorithm;
41 import com.android.systemui.recents.utilities.AnimationProps;
42 
43 import java.util.ArrayList;
44 import 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  */
50 public 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          */
onPrepareLaunchTargetForEnterAnimation()59         void onPrepareLaunchTargetForEnterAnimation();
60 
61         /**
62          * Callback to start the animation for the launch target {@link TaskView}.
63          */
onStartLaunchTargetEnterAnimation(TaskViewTransform transform, int duration, boolean screenPinningEnabled, ReferenceCountedTrigger postAnimationTrigger)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          */
onStartLaunchTargetLaunchAnimation(int duration, boolean screenPinningRequested, ReferenceCountedTrigger postAnimationTrigger)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          */
onStartFrontTaskEnterAnimation(boolean screenPinningEnabled)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 
TaskStackAnimationHelper(Context context, TaskStackView stackView)116     public TaskStackAnimationHelper(Context context, TaskStackView stackView) {
117         mStackView = stackView;
118         mEnterAndExitFromHomeTranslationOffset = LegacyRecentsImpl.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      */
prepareForEnterAnimation()126     public void prepareForEnterAnimation() {
127         RecentsConfiguration config = LegacyRecentsImpl.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 = LegacyRecentsImpl.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      */
startEnterAnimation(final ReferenceCountedTrigger postAnimationTrigger)214     public void startEnterAnimation(final ReferenceCountedTrigger postAnimationTrigger) {
215         RecentsConfiguration config = LegacyRecentsImpl.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 = LegacyRecentsImpl.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      */
startExitToHomeAnimation(boolean animated, ReferenceCountedTrigger postAnimationTrigger)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 (LegacyRecentsImpl.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 (LegacyRecentsImpl.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      */
startLaunchTaskAnimation(TaskView launchingTaskView, boolean screenPinningRequested, final ReferenceCountedTrigger postAnimationTrigger)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      */
startDeleteTaskAnimation(final TaskView deleteTaskView, boolean gridLayout, final ReferenceCountedTrigger postAnimationTrigger)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      */
startDeleteAllTasksAnimation(final List<TaskView> taskViews, boolean gridLayout, final ReferenceCountedTrigger postAnimationTrigger)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      */
startScrollToFocusedTaskAnimation(Task newFocusedTask, boolean requestViewFocus)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      */
startNewStackScrollAnimation(TaskStack newStack, ReferenceCountedTrigger animationTrigger)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      */
calculateStaggeredAnimDuration(int i)626     private int calculateStaggeredAnimDuration(int i) {
627         return Math.max(100, 100 + ((i - 1) * 50));
628     }
629 
startTaskGridDeleteTaskAnimation(final TaskView deleteTaskView, final ReferenceCountedTrigger postAnimationTrigger)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 
startTaskStackDeleteTaskAnimation(final TaskView deleteTaskView, final ReferenceCountedTrigger postAnimationTrigger)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 
startTaskStackDeleteAllTasksAnimation(final List<TaskView> taskViews, final ReferenceCountedTrigger postAnimationTrigger)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