TaskStackAnimationHelper.java revision 8f6ee48225ad1cdf966c8f406c85113b13833c7b
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.content.Context;
22import android.content.res.Resources;
23import android.graphics.RectF;
24import android.util.Log;
25import android.view.View;
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.misc.ReferenceCountedTrigger;
35import com.android.systemui.recents.model.Task;
36import com.android.systemui.recents.model.TaskStack;
37
38import java.util.ArrayList;
39import java.util.List;
40
41/**
42 * A helper class to create task view animations for {@link TaskView}s in a {@link TaskStackView},
43 * but not the contents of the {@link TaskView}s.
44 */
45public class TaskStackAnimationHelper {
46
47    /**
48     * Callbacks from the helper to coordinate view-content animations with view animations.
49     */
50    public interface Callbacks {
51        /**
52         * Callback to prepare for the start animation for the launch target {@link TaskView}.
53         */
54        void onPrepareLaunchTargetForEnterAnimation();
55
56        /**
57         * Callback to start the animation for the launch target {@link TaskView}.
58         */
59        void onStartLaunchTargetEnterAnimation(TaskViewTransform transform, int duration,
60                boolean screenPinningEnabled, ReferenceCountedTrigger postAnimationTrigger);
61
62        /**
63         * Callback to start the animation for the launch target {@link TaskView} when it is
64         * launched from Recents.
65         */
66        void onStartLaunchTargetLaunchAnimation(int duration, boolean screenPinningRequested,
67                ReferenceCountedTrigger postAnimationTrigger);
68
69        /**
70         * Callback to start the animation for the front {@link TaskView} if there is no launch
71         * target.
72         */
73        void onStartFrontTaskEnterAnimation(boolean screenPinningEnabled);
74    }
75
76    private static final int FRAME_OFFSET_MS = 16;
77
78    public static final int ENTER_FROM_HOME_ALPHA_DURATION = 100;
79    public static final int ENTER_FROM_HOME_TRANSLATION_DURATION = 333;
80    public static final int ENTER_WHILE_DOCKING_DURATION = 150;
81
82    private static final PathInterpolator ENTER_FROM_HOME_TRANSLATION_INTERPOLATOR =
83            new PathInterpolator(0, 0, 0, 1f);
84    private static final PathInterpolator ENTER_FROM_HOME_ALPHA_INTERPOLATOR =
85            new PathInterpolator(0, 0, 0.2f, 1f);
86
87    public static final int EXIT_TO_HOME_ALPHA_DURATION = 100;
88    public static final int EXIT_TO_HOME_TRANSLATION_DURATION = 150;
89    private static final PathInterpolator EXIT_TO_HOME_TRANSLATION_INTERPOLATOR =
90            new PathInterpolator(0.8f, 0, 0.6f, 1f);
91    private static final PathInterpolator EXIT_TO_HOME_ALPHA_INTERPOLATOR =
92            new PathInterpolator(0.4f, 0, 1f, 1f);
93
94    private static final PathInterpolator FOCUS_NEXT_TASK_INTERPOLATOR =
95            new PathInterpolator(0.4f, 0, 0, 1f);
96    private static final PathInterpolator FOCUS_IN_FRONT_NEXT_TASK_INTERPOLATOR =
97            new PathInterpolator(0, 0, 0, 1f);
98    private static final PathInterpolator FOCUS_BEHIND_NEXT_TASK_INTERPOLATOR =
99            new PathInterpolator(0.4f, 0, 0.2f, 1f);
100
101    private static final PathInterpolator ENTER_WHILE_DOCKING_INTERPOLATOR =
102            new PathInterpolator(0, 0, 0.2f, 1f);
103
104    private TaskStackView mStackView;
105
106    private TaskViewTransform mTmpTransform = new TaskViewTransform();
107    private ArrayList<TaskViewTransform> mTmpCurrentTaskTransforms = new ArrayList<>();
108    private ArrayList<TaskViewTransform> mTmpFinalTaskTransforms = new ArrayList<>();
109
110    public TaskStackAnimationHelper(Context context, TaskStackView stackView) {
111        mStackView = stackView;
112    }
113
114    /**
115     * Prepares the stack views and puts them in their initial animation state while visible, before
116     * the in-app enter animations start (after the window-transition completes).
117     */
118    public void prepareForEnterAnimation() {
119        RecentsConfiguration config = Recents.getConfiguration();
120        RecentsActivityLaunchState launchState = config.getLaunchState();
121        Resources res = mStackView.getResources();
122
123        TaskStackLayoutAlgorithm stackLayout = mStackView.getStackAlgorithm();
124        TaskStackViewScroller stackScroller = mStackView.getScroller();
125        TaskStack stack = mStackView.getStack();
126        Task launchTargetTask = stack.getLaunchTarget();
127
128        // Break early if there are no tasks
129        if (stack.getTaskCount() == 0) {
130            return;
131        }
132
133        int offscreenYOffset = stackLayout.mStackRect.height();
134        int taskViewAffiliateGroupEnterOffset = res.getDimensionPixelSize(
135                R.dimen.recents_task_stack_animation_affiliate_enter_offset);
136        int launchedWhileDockingOffset = res.getDimensionPixelSize(
137                R.dimen.recents_task_stack_animation_launched_while_docking_offset);
138
139        // Prepare each of the task views for their enter animation from front to back
140        List<TaskView> taskViews = mStackView.getTaskViews();
141        for (int i = taskViews.size() - 1; i >= 0; i--) {
142            TaskView tv = taskViews.get(i);
143            Task task = tv.getTask();
144            boolean currentTaskOccludesLaunchTarget = (launchTargetTask != null &&
145                    launchTargetTask.group.isTaskAboveTask(task, launchTargetTask));
146            boolean hideTask = (launchTargetTask != null &&
147                    launchTargetTask.isFreeformTask() && task.isFreeformTask());
148
149            // Get the current transform for the task, which will be used to position it offscreen
150            stackLayout.getStackTransform(task, stackScroller.getStackScroll(), mTmpTransform,
151                    null);
152
153            if (hideTask) {
154                tv.setVisibility(View.INVISIBLE);
155            } else if (launchState.launchedHasConfigurationChanged) {
156                // Just load the views as-is
157            } else if (launchState.launchedFromApp && !launchState.launchedWhileDocking) {
158                if (task.isLaunchTarget) {
159                    tv.onPrepareLaunchTargetForEnterAnimation();
160                } else if (currentTaskOccludesLaunchTarget) {
161                    // Move the task view slightly lower so we can animate it in
162                    RectF bounds = new RectF(mTmpTransform.rect);
163                    bounds.offset(0, taskViewAffiliateGroupEnterOffset);
164                    tv.setClipViewInStack(false);
165                    tv.setAlpha(0f);
166                    tv.setLeftTopRightBottom((int) bounds.left, (int) bounds.top,
167                            (int) bounds.right, (int) bounds.bottom);
168                }
169            } else if (launchState.launchedFromHome) {
170                // Move the task view off screen (below) so we can animate it in
171                RectF bounds = new RectF(mTmpTransform.rect);
172                bounds.offset(0, offscreenYOffset);
173                tv.setAlpha(0f);
174                tv.setLeftTopRightBottom((int) bounds.left, (int) bounds.top, (int) bounds.right,
175                        (int) bounds.bottom);
176            } else if (launchState.launchedWhileDocking) {
177                RectF bounds = new RectF(mTmpTransform.rect);
178                bounds.offset(0, launchedWhileDockingOffset);
179                tv.setLeftTopRightBottom((int) bounds.left, (int) bounds.top, (int) bounds.right,
180                        (int) bounds.bottom);
181            }
182        }
183    }
184
185    /**
186     * Starts the in-app enter animation, which animates the {@link TaskView}s to their final places
187     * depending on how Recents was triggered.
188     */
189    public void startEnterAnimation(final ReferenceCountedTrigger postAnimationTrigger) {
190        RecentsConfiguration config = Recents.getConfiguration();
191        RecentsActivityLaunchState launchState = config.getLaunchState();
192        Resources res = mStackView.getResources();
193
194        TaskStackLayoutAlgorithm stackLayout = mStackView.getStackAlgorithm();
195        TaskStackViewScroller stackScroller = mStackView.getScroller();
196        TaskStack stack = mStackView.getStack();
197        Task launchTargetTask = stack.getLaunchTarget();
198
199        // Break early if there are no tasks
200        if (stack.getTaskCount() == 0) {
201            return;
202        }
203
204        int taskViewEnterFromAppDuration = res.getInteger(
205                R.integer.recents_task_enter_from_app_duration);
206        int taskViewEnterFromAffiliatedAppDuration = res.getInteger(
207                R.integer.recents_task_enter_from_affiliated_app_duration);
208
209        // Create enter animations for each of the views from front to back
210        List<TaskView> taskViews = mStackView.getTaskViews();
211        int taskViewCount = taskViews.size();
212        for (int i = taskViewCount - 1; i >= 0; i--) {
213            int taskIndexFromFront = taskViewCount - i - 1;
214            int taskIndexFromBack = i;
215            final TaskView tv = taskViews.get(i);
216            Task task = tv.getTask();
217            boolean currentTaskOccludesLaunchTarget = false;
218            if (launchTargetTask != null) {
219                currentTaskOccludesLaunchTarget = launchTargetTask.group.isTaskAboveTask(task,
220                        launchTargetTask);
221            }
222
223            // Get the current transform for the task, which will be updated to the final transform
224            // to animate to depending on how recents was invoked
225            stackLayout.getStackTransform(task, stackScroller.getStackScroll(), mTmpTransform,
226                    null);
227
228            if (launchState.launchedFromApp && !launchState.launchedWhileDocking) {
229                if (task.isLaunchTarget) {
230                    tv.onStartLaunchTargetEnterAnimation(mTmpTransform,
231                            taskViewEnterFromAppDuration, mStackView.mScreenPinningEnabled,
232                            postAnimationTrigger);
233                } else {
234                    // Animate the task up if it was occluding the launch target
235                    if (currentTaskOccludesLaunchTarget) {
236                        AnimationProps taskAnimation = new AnimationProps(
237                                taskViewEnterFromAffiliatedAppDuration, Interpolators.ALPHA_IN,
238                                new AnimatorListenerAdapter() {
239                                    @Override
240                                    public void onAnimationEnd(Animator animation) {
241                                        postAnimationTrigger.decrement();
242                                        tv.setClipViewInStack(true);
243                                    }
244                                });
245                        postAnimationTrigger.increment();
246                        mStackView.updateTaskViewToTransform(tv, mTmpTransform, taskAnimation);
247                    }
248                }
249
250            } else if (launchState.launchedFromHome) {
251                // Animate the tasks up
252                AnimationProps taskAnimation = new AnimationProps()
253                        .setStartDelay(AnimationProps.ALPHA, taskIndexFromFront * FRAME_OFFSET_MS)
254                        .setDuration(AnimationProps.ALPHA, ENTER_FROM_HOME_ALPHA_DURATION)
255                        .setDuration(AnimationProps.BOUNDS, ENTER_FROM_HOME_TRANSLATION_DURATION -
256                                (taskIndexFromFront * FRAME_OFFSET_MS))
257                        .setInterpolator(AnimationProps.BOUNDS,
258                                ENTER_FROM_HOME_TRANSLATION_INTERPOLATOR)
259                        .setInterpolator(AnimationProps.ALPHA,
260                                ENTER_FROM_HOME_ALPHA_INTERPOLATOR)
261                        .setListener(postAnimationTrigger.decrementOnAnimationEnd());
262                postAnimationTrigger.increment();
263                mStackView.updateTaskViewToTransform(tv, mTmpTransform, taskAnimation);
264                if (i == taskViewCount - 1) {
265                    tv.onStartFrontTaskEnterAnimation(mStackView.mScreenPinningEnabled);
266                }
267            } else if (launchState.launchedWhileDocking) {
268                // Animate the tasks up
269                AnimationProps taskAnimation = new AnimationProps()
270                        .setDuration(AnimationProps.BOUNDS, (int) (ENTER_WHILE_DOCKING_DURATION +
271                                                        (taskIndexFromBack * 2f * FRAME_OFFSET_MS)))
272                        .setInterpolator(AnimationProps.BOUNDS,
273                                ENTER_WHILE_DOCKING_INTERPOLATOR)
274                        .setListener(postAnimationTrigger.decrementOnAnimationEnd());
275                postAnimationTrigger.increment();
276                mStackView.updateTaskViewToTransform(tv, mTmpTransform, taskAnimation);
277            }
278        }
279    }
280
281    /**
282     * Starts an in-app animation to hide all the task views so that we can transition back home.
283     */
284    public void startExitToHomeAnimation(boolean animated,
285            ReferenceCountedTrigger postAnimationTrigger) {
286        TaskStackLayoutAlgorithm stackLayout = mStackView.getStackAlgorithm();
287        TaskStackViewScroller stackScroller = mStackView.getScroller();
288        TaskStack stack = mStackView.getStack();
289
290        // Break early if there are no tasks
291        if (stack.getTaskCount() == 0) {
292            return;
293        }
294
295        int offscreenYOffset = stackLayout.mStackRect.height();
296
297        // Create the animations for each of the tasks
298        List<TaskView> taskViews = mStackView.getTaskViews();
299        int taskViewCount = taskViews.size();
300        for (int i = 0; i < taskViewCount; i++) {
301            int taskIndexFromFront = taskViewCount - i - 1;
302            TaskView tv = taskViews.get(i);
303            Task task = tv.getTask();
304
305            // Animate the tasks down
306            AnimationProps taskAnimation;
307            if (animated) {
308                taskAnimation = new AnimationProps()
309                        .setStartDelay(AnimationProps.ALPHA, i * FRAME_OFFSET_MS)
310                        .setDuration(AnimationProps.ALPHA, EXIT_TO_HOME_ALPHA_DURATION)
311                        .setDuration(AnimationProps.BOUNDS, EXIT_TO_HOME_TRANSLATION_DURATION +
312                                (taskIndexFromFront * FRAME_OFFSET_MS))
313                        .setInterpolator(AnimationProps.BOUNDS,
314                                EXIT_TO_HOME_TRANSLATION_INTERPOLATOR)
315                        .setInterpolator(AnimationProps.ALPHA,
316                                EXIT_TO_HOME_ALPHA_INTERPOLATOR)
317                        .setListener(postAnimationTrigger.decrementOnAnimationEnd());
318                postAnimationTrigger.increment();
319            } else {
320                taskAnimation = AnimationProps.IMMEDIATE;
321            }
322
323            stackLayout.getStackTransform(task, stackScroller.getStackScroll(), mTmpTransform,
324                    null);
325            mTmpTransform.alpha = 0f;
326            mTmpTransform.rect.offset(0, offscreenYOffset);
327            mStackView.updateTaskViewToTransform(tv, mTmpTransform, taskAnimation);
328        }
329    }
330
331    /**
332     * Starts the animation for the launching task view, hiding any tasks that might occlude the
333     * window transition for the launching task.
334     */
335    public void startLaunchTaskAnimation(TaskView launchingTaskView, boolean screenPinningRequested,
336            final ReferenceCountedTrigger postAnimationTrigger) {
337        Resources res = mStackView.getResources();
338        TaskStackLayoutAlgorithm stackLayout = mStackView.getStackAlgorithm();
339        TaskStackViewScroller stackScroller = mStackView.getScroller();
340
341        int taskViewExitToAppDuration = res.getInteger(
342                R.integer.recents_task_exit_to_app_duration);
343        int taskViewAffiliateGroupEnterOffset = res.getDimensionPixelSize(
344                R.dimen.recents_task_stack_animation_affiliate_enter_offset);
345
346        Task launchingTask = launchingTaskView.getTask();
347        List<TaskView> taskViews = mStackView.getTaskViews();
348        int taskViewCount = taskViews.size();
349        for (int i = 0; i < taskViewCount; i++) {
350            TaskView tv = taskViews.get(i);
351            Task task = tv.getTask();
352            boolean currentTaskOccludesLaunchTarget = (launchingTask != null &&
353                    launchingTask.group.isTaskAboveTask(task, launchingTask));
354
355            if (tv == launchingTaskView) {
356                tv.setClipViewInStack(false);
357                tv.onStartLaunchTargetLaunchAnimation(taskViewExitToAppDuration,
358                        screenPinningRequested, postAnimationTrigger);
359            } else if (currentTaskOccludesLaunchTarget) {
360                // Animate this task out of view
361                AnimationProps taskAnimation = new AnimationProps(
362                        taskViewExitToAppDuration, Interpolators.ALPHA_OUT,
363                        postAnimationTrigger.decrementOnAnimationEnd());
364                postAnimationTrigger.increment();
365
366                stackLayout.getStackTransform(task, stackScroller.getStackScroll(), mTmpTransform,
367                        null);
368                mTmpTransform.alpha = 0f;
369                mTmpTransform.rect.offset(0, taskViewAffiliateGroupEnterOffset);
370                mStackView.updateTaskViewToTransform(tv, mTmpTransform, taskAnimation);
371            }
372        }
373    }
374
375    /**
376     * Starts the delete animation for the specified {@link TaskView}.
377     */
378    public void startDeleteTaskAnimation(Task deleteTask, final TaskView deleteTaskView,
379            final ReferenceCountedTrigger postAnimationTrigger) {
380        Resources res = mStackView.getResources();
381        TaskStackLayoutAlgorithm stackLayout = mStackView.getStackAlgorithm();
382        TaskStackViewScroller stackScroller = mStackView.getScroller();
383
384        int taskViewRemoveAnimDuration = res.getInteger(
385                R.integer.recents_animate_task_view_remove_duration);
386        int taskViewRemoveAnimTranslationXPx = res.getDimensionPixelSize(
387                R.dimen.recents_task_view_remove_anim_translation_x);
388
389        // Disabling clipping with the stack while the view is animating away
390        deleteTaskView.setClipViewInStack(false);
391
392        // Compose the new animation and transform and star the animation
393        AnimationProps taskAnimation = new AnimationProps(taskViewRemoveAnimDuration,
394                Interpolators.ALPHA_OUT, new AnimatorListenerAdapter() {
395            @Override
396            public void onAnimationEnd(Animator animation) {
397                postAnimationTrigger.decrement();
398
399                // Re-enable clipping with the stack (we will reuse this view)
400                deleteTaskView.setClipViewInStack(true);
401            }
402        });
403        postAnimationTrigger.increment();
404
405        stackLayout.getStackTransform(deleteTask, stackScroller.getStackScroll(), mTmpTransform,
406                null);
407        mTmpTransform.alpha = 0f;
408        mTmpTransform.rect.offset(taskViewRemoveAnimTranslationXPx, 0);
409        mStackView.updateTaskViewToTransform(deleteTaskView, mTmpTransform, taskAnimation);
410    }
411
412    /**
413     * Starts the animation to focus the next {@link TaskView} when paging through recents.
414     *
415     * @return whether or not this will trigger a scroll in the stack
416     */
417    public boolean startScrollToFocusedTaskAnimation(Task newFocusedTask,
418            boolean requestViewFocus) {
419        TaskStackLayoutAlgorithm stackLayout = mStackView.getStackAlgorithm();
420        TaskStackViewScroller stackScroller = mStackView.getScroller();
421        TaskStack stack = mStackView.getStack();
422
423        final float curScroll = stackScroller.getStackScroll();
424        final float newScroll = stackLayout.getStackScrollForTask(newFocusedTask);
425        boolean willScrollToFront = newScroll > curScroll;
426        boolean willScroll = Float.compare(newScroll, curScroll) != 0;
427
428        // Get the current set of task transforms
429        int taskViewCount = mStackView.getTaskViews().size();
430        ArrayList<Task> stackTasks = stack.getStackTasks();
431        mStackView.getCurrentTaskTransforms(stackTasks, mTmpCurrentTaskTransforms);
432
433        // Pick up the newly visible views after the scroll
434        mStackView.bindVisibleTaskViews(newScroll);
435
436        // Update the internal state
437        stackLayout.setFocusState(TaskStackLayoutAlgorithm.STATE_FOCUSED);
438        stackScroller.setStackScroll(newScroll, null /* animation */);
439        mStackView.cancelDeferredTaskViewLayoutAnimation();
440
441        // Get the final set of task transforms
442        mStackView.getLayoutTaskTransforms(newScroll, stackLayout.getFocusState(), stackTasks,
443                mTmpFinalTaskTransforms);
444
445        // Focus the task view
446        TaskView newFocusedTaskView = mStackView.getChildViewForTask(newFocusedTask);
447        if (newFocusedTaskView == null) {
448            // Log the error if we have no task view, and skip the animation
449            Log.e("TaskStackAnimationHelper", "b/27389156 null-task-view prebind:" + taskViewCount +
450                    " postbind:" + mStackView.getTaskViews().size() + " prescroll:" + curScroll +
451                    " postscroll: " + newScroll);
452            return false;
453        }
454        newFocusedTaskView.setFocusedState(true, requestViewFocus);
455
456        // Setup the end listener to return all the hidden views to the view pool after the
457        // focus animation
458        ReferenceCountedTrigger postAnimTrigger = new ReferenceCountedTrigger();
459        postAnimTrigger.addLastDecrementRunnable(new Runnable() {
460            @Override
461            public void run() {
462                mStackView.bindVisibleTaskViews(newScroll);
463            }
464        });
465
466        List<TaskView> taskViews = mStackView.getTaskViews();
467        taskViewCount = taskViews.size();
468        int newFocusTaskViewIndex = taskViews.indexOf(newFocusedTaskView);
469        for (int i = 0; i < taskViewCount; i++) {
470            TaskView tv = taskViews.get(i);
471            Task task = tv.getTask();
472
473            if (mStackView.isIgnoredTask(task)) {
474                continue;
475            }
476
477            int taskIndex = stackTasks.indexOf(task);
478            TaskViewTransform fromTransform = mTmpCurrentTaskTransforms.get(taskIndex);
479            TaskViewTransform toTransform = mTmpFinalTaskTransforms.get(taskIndex);
480
481            // Update the task to the initial state (for the newly picked up tasks)
482            mStackView.updateTaskViewToTransform(tv, fromTransform, AnimationProps.IMMEDIATE);
483
484            int duration;
485            Interpolator interpolator;
486            if (willScrollToFront) {
487                duration = Math.max(100, 100 + ((i - 1) * 50));
488                interpolator = FOCUS_BEHIND_NEXT_TASK_INTERPOLATOR;
489            } else {
490                if (i < newFocusTaskViewIndex) {
491                    duration = 150 + ((newFocusTaskViewIndex - i - 1) * 50);
492                    interpolator = FOCUS_BEHIND_NEXT_TASK_INTERPOLATOR;
493                } else if (i > newFocusTaskViewIndex) {
494                    duration = Math.max(100, 150 - ((i - newFocusTaskViewIndex - 1) * 50));
495                    interpolator = FOCUS_IN_FRONT_NEXT_TASK_INTERPOLATOR;
496                } else {
497                    duration = 200;
498                    interpolator = FOCUS_NEXT_TASK_INTERPOLATOR;
499                }
500            }
501
502            AnimationProps anim = new AnimationProps()
503                    .setDuration(AnimationProps.BOUNDS, duration)
504                    .setInterpolator(AnimationProps.BOUNDS, interpolator)
505                    .setListener(postAnimTrigger.decrementOnAnimationEnd());
506            postAnimTrigger.increment();
507            mStackView.updateTaskViewToTransform(tv, toTransform, anim);
508        }
509        return willScroll;
510    }
511}
512