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.TimeInterpolator;
22 import android.animation.ValueAnimator;
23 import android.content.Context;
24 import android.content.res.Configuration;
25 import android.content.res.Resources;
26 import android.util.Log;
27 import android.view.View;
28 import android.view.animation.Interpolator;
29 import android.view.animation.PathInterpolator;
30 
31 import com.android.systemui.Interpolators;
32 import com.android.systemui.R;
33 import com.android.systemui.recents.Recents;
34 import com.android.systemui.recents.RecentsActivityLaunchState;
35 import com.android.systemui.recents.RecentsConfiguration;
36 import com.android.systemui.recents.misc.ReferenceCountedTrigger;
37 import com.android.systemui.recents.model.Task;
38 import com.android.systemui.recents.model.TaskStack;
39 
40 import java.util.ArrayList;
41 import 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  */
47 public 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          */
onPrepareLaunchTargetForEnterAnimation()56         void onPrepareLaunchTargetForEnterAnimation();
57 
58         /**
59          * Callback to start the animation for the launch target {@link TaskView}.
60          */
onStartLaunchTargetEnterAnimation(TaskViewTransform transform, int duration, boolean screenPinningEnabled, ReferenceCountedTrigger postAnimationTrigger)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          */
onStartLaunchTargetLaunchAnimation(int duration, boolean screenPinningRequested, ReferenceCountedTrigger postAnimationTrigger)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          */
onStartFrontTaskEnterAnimation(boolean screenPinningEnabled)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 
TaskStackAnimationHelper(Context context, TaskStackView stackView)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      */
prepareForEnterAnimation()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      */
startEnterAnimation(final ReferenceCountedTrigger postAnimationTrigger)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      */
startExitToHomeAnimation(boolean animated, ReferenceCountedTrigger postAnimationTrigger)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      */
startLaunchTaskAnimation(TaskView launchingTaskView, boolean screenPinningRequested, final ReferenceCountedTrigger postAnimationTrigger)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      */
startDeleteTaskAnimation(final TaskView deleteTaskView, final ReferenceCountedTrigger postAnimationTrigger)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      */
startDeleteAllTasksAnimation(final List<TaskView> taskViews, final ReferenceCountedTrigger postAnimationTrigger)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      */
startScrollToFocusedTaskAnimation(Task newFocusedTask, boolean requestViewFocus)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      */
startNewStackScrollAnimation(TaskStack newStack, ReferenceCountedTrigger animationTrigger)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      */
calculateStaggeredAnimDuration(int i)651     private int calculateStaggeredAnimDuration(int i) {
652         return Math.max(100, 100 + ((i - 1) * 50));
653     }
654 }
655