TaskStackAnimationHelper.java revision c0d7058b14c24cd07912f5629c26b39b7b4673d5
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.view.View;
25
26import com.android.systemui.Interpolators;
27import com.android.systemui.R;
28import com.android.systemui.recents.Recents;
29import com.android.systemui.recents.RecentsActivityLaunchState;
30import com.android.systemui.recents.RecentsConfiguration;
31import com.android.systemui.recents.misc.ReferenceCountedTrigger;
32import com.android.systemui.recents.model.Task;
33import com.android.systemui.recents.model.TaskStack;
34
35import java.util.List;
36
37/**
38 * A helper class to create task view animations for {@link TaskView}s in a {@link TaskStackView},
39 * but not the contents of the {@link TaskView}s.
40 */
41public class TaskStackAnimationHelper {
42
43    /**
44     * Callbacks from the helper to coordinate view-content animations with view animations.
45     */
46    public interface Callbacks {
47        /**
48         * Callback to prepare for the start animation for the launch target {@link TaskView}.
49         */
50        void onPrepareLaunchTargetForEnterAnimation();
51
52        /**
53         * Callback to start the animation for the launch target {@link TaskView}.
54         */
55        void onStartLaunchTargetEnterAnimation(int duration, boolean screenPinningEnabled,
56                ReferenceCountedTrigger postAnimationTrigger);
57
58        /**
59         * Callback to start the animation for the launch target {@link TaskView} when it is
60         * launched from Recents.
61         */
62        void onStartLaunchTargetLaunchAnimation(int duration, boolean screenPinningRequested,
63                ReferenceCountedTrigger postAnimationTrigger);
64    }
65
66    private TaskStackView mStackView;
67
68    private TaskViewTransform mTmpTransform = new TaskViewTransform();
69
70    public TaskStackAnimationHelper(Context context, TaskStackView stackView) {
71        mStackView = stackView;
72    }
73
74    /**
75     * Prepares the stack views and puts them in their initial animation state while visible, before
76     * the in-app enter animations start (after the window-transition completes).
77     */
78    public void prepareForEnterAnimation() {
79        RecentsConfiguration config = Recents.getConfiguration();
80        RecentsActivityLaunchState launchState = config.getLaunchState();
81        Resources res = mStackView.getResources();
82
83        TaskStackLayoutAlgorithm stackLayout = mStackView.getStackAlgorithm();
84        TaskStackViewScroller stackScroller = mStackView.getScroller();
85        TaskStack stack = mStackView.getStack();
86        Task launchTargetTask = stack.getLaunchTarget();
87
88        // Break early if there are no tasks
89        if (stack.getTaskCount() == 0) {
90            return;
91        }
92
93        int offscreenY = stackLayout.mStackRect.bottom;
94        int taskViewAffiliateGroupEnterOffset = res.getDimensionPixelSize(
95                R.dimen.recents_task_view_affiliate_group_enter_offset);
96
97        // Prepare each of the task views for their enter animation from front to back
98        List<TaskView> taskViews = mStackView.getTaskViews();
99        for (int i = taskViews.size() - 1; i >= 0; i--) {
100            TaskView tv = taskViews.get(i);
101            Task task = tv.getTask();
102            boolean currentTaskOccludesLaunchTarget = (launchTargetTask != null &&
103                    launchTargetTask.group.isTaskAboveTask(task, launchTargetTask));
104            boolean hideTask = (launchTargetTask != null &&
105                    launchTargetTask.isFreeformTask() && task.isFreeformTask());
106
107            // Get the current transform for the task, which will be used to position it offscreen
108            stackLayout.getStackTransform(task, stackScroller.getStackScroll(), mTmpTransform,
109                    null);
110
111            if (hideTask) {
112                tv.setVisibility(View.INVISIBLE);
113            } else if (launchState.launchedHasConfigurationChanged) {
114                // Just load the views as-is
115            } else if (launchState.launchedFromAppWithThumbnail) {
116                if (task.isLaunchTarget) {
117                    tv.onPrepareLaunchTargetForEnterAnimation();
118                } else if (currentTaskOccludesLaunchTarget) {
119                    // Move the task view slightly lower so we can animate it in
120                    RectF bounds = new RectF(mTmpTransform.rect);
121                    bounds.offset(0, taskViewAffiliateGroupEnterOffset);
122                    tv.setClipViewInStack(false);
123                    tv.setAlpha(0f);
124                    tv.setLeftTopRightBottom((int) bounds.left, (int) bounds.top,
125                            (int) bounds.right, (int) bounds.bottom);
126                }
127            } else if (launchState.launchedFromHome) {
128                // Move the task view off screen (below) so we can animate it in
129                RectF bounds = new RectF(mTmpTransform.rect);
130                bounds.offsetTo(bounds.left, offscreenY);
131                tv.setLeftTopRightBottom((int) bounds.left, (int) bounds.top, (int) bounds.right,
132                        (int) bounds.bottom);
133            }
134        }
135    }
136
137    /**
138     * Starts the in-app enter animation, which animates the {@link TaskView}s to their final places
139     * depending on how Recents was triggered.
140     */
141    public void startEnterAnimation(final ReferenceCountedTrigger postAnimationTrigger) {
142        RecentsConfiguration config = Recents.getConfiguration();
143        RecentsActivityLaunchState launchState = config.getLaunchState();
144        Resources res = mStackView.getResources();
145
146        TaskStackLayoutAlgorithm stackLayout = mStackView.getStackAlgorithm();
147        TaskStackViewScroller stackScroller = mStackView.getScroller();
148        TaskStack stack = mStackView.getStack();
149        Task launchTargetTask = stack.getLaunchTarget();
150
151        // Break early if there are no tasks
152        if (stack.getTaskCount() == 0) {
153            return;
154        }
155
156        int taskViewEnterFromAppDuration = res.getInteger(
157                R.integer.recents_task_enter_from_app_duration);
158        int taskViewEnterFromAffiliatedAppDuration = res.getInteger(
159                R.integer.recents_task_enter_from_affiliated_app_duration);
160        int taskViewEnterFromHomeDuration = res.getInteger(
161                R.integer.recents_task_enter_from_home_duration);
162        int taskViewEnterFromHomeStaggerDelay = res.getInteger(
163                R.integer.recents_task_enter_from_home_stagger_delay);
164
165        // Create enter animations for each of the views from front to back
166        List<TaskView> taskViews = mStackView.getTaskViews();
167        int taskViewCount = taskViews.size();
168        for (int i = taskViewCount - 1; i >= 0; i--) {
169            final TaskView tv = taskViews.get(i);
170            Task task = tv.getTask();
171            boolean currentTaskOccludesLaunchTarget = false;
172            if (launchTargetTask != null) {
173                currentTaskOccludesLaunchTarget = launchTargetTask.group.isTaskAboveTask(task,
174                        launchTargetTask);
175            }
176
177            // Get the current transform for the task, which will be updated to the final transform
178            // to animate to depending on how recents was invoked
179            stackLayout.getStackTransform(task, stackScroller.getStackScroll(), mTmpTransform,
180                    null);
181
182            if (launchState.launchedFromAppWithThumbnail) {
183                if (task.isLaunchTarget) {
184                    tv.onStartLaunchTargetEnterAnimation(taskViewEnterFromAppDuration,
185                            mStackView.mScreenPinningEnabled, postAnimationTrigger);
186                } else {
187                    // Animate the task up if it was occluding the launch target
188                    if (currentTaskOccludesLaunchTarget) {
189                        TaskViewAnimation taskAnimation = new TaskViewAnimation(
190                                taskViewEnterFromAffiliatedAppDuration, Interpolators.ALPHA_IN,
191                                new AnimatorListenerAdapter() {
192                                    @Override
193                                    public void onAnimationEnd(Animator animation) {
194                                        postAnimationTrigger.decrement();
195                                        tv.setClipViewInStack(false);
196                                    }
197                                });
198                        postAnimationTrigger.increment();
199                        mStackView.updateTaskViewToTransform(tv, mTmpTransform, taskAnimation);
200                    }
201                }
202
203            } else if (launchState.launchedFromHome) {
204                // Animate the tasks up
205                int frontIndex = (taskViewCount - i - 1);
206                int delay = frontIndex * taskViewEnterFromHomeStaggerDelay;
207                int duration = taskViewEnterFromHomeDuration +
208                        frontIndex * taskViewEnterFromHomeStaggerDelay;
209
210                TaskViewAnimation taskAnimation = new TaskViewAnimation(delay,
211                        duration, Interpolators.DECELERATE_QUINT,
212                        postAnimationTrigger.decrementOnAnimationEnd());
213                postAnimationTrigger.increment();
214                mStackView.updateTaskViewToTransform(tv, mTmpTransform, taskAnimation);
215            }
216        }
217    }
218
219    /**
220     * Starts an in-app animation to hide all the task views so that we can transition back home.
221     */
222    public void startExitToHomeAnimation(boolean animated,
223            ReferenceCountedTrigger postAnimationTrigger) {
224        Resources res = mStackView.getResources();
225        TaskStackLayoutAlgorithm stackLayout = mStackView.getStackAlgorithm();
226        TaskStackViewScroller stackScroller = mStackView.getScroller();
227        TaskStack stack = mStackView.getStack();
228
229        // Break early if there are no tasks
230        if (stack.getTaskCount() == 0) {
231            return;
232        }
233
234        int offscreenY = stackLayout.mStackRect.bottom;
235        int taskViewExitToHomeDuration = res.getInteger(
236                R.integer.recents_task_exit_to_home_duration);
237
238        // Create the animations for each of the tasks
239        List<TaskView> taskViews = mStackView.getTaskViews();
240        int taskViewCount = taskViews.size();
241        for (int i = 0; i < taskViewCount; i++) {
242            TaskView tv = taskViews.get(i);
243            Task task = tv.getTask();
244            TaskViewAnimation taskAnimation = new TaskViewAnimation(
245                    animated ? taskViewExitToHomeDuration : 0, Interpolators.FAST_OUT_LINEAR_IN,
246                    postAnimationTrigger.decrementOnAnimationEnd());
247            postAnimationTrigger.increment();
248
249            stackLayout.getStackTransform(task, stackScroller.getStackScroll(), mTmpTransform,
250                    null);
251            mTmpTransform.rect.offsetTo(mTmpTransform.rect.left, offscreenY);
252            mStackView.updateTaskViewToTransform(tv, mTmpTransform, taskAnimation);
253        }
254    }
255
256    /**
257     * Starts the animation for the launching task view, hiding any tasks that might occlude the
258     * window transition for the launching task.
259     */
260    public void startLaunchTaskAnimation(TaskView launchingTaskView, boolean screenPinningRequested,
261            final ReferenceCountedTrigger postAnimationTrigger) {
262        Resources res = mStackView.getResources();
263        TaskStackLayoutAlgorithm stackLayout = mStackView.getStackAlgorithm();
264        TaskStackViewScroller stackScroller = mStackView.getScroller();
265
266        int taskViewExitToAppDuration = res.getInteger(
267                R.integer.recents_task_exit_to_app_duration);
268        int taskViewAffiliateGroupEnterOffset = res.getDimensionPixelSize(
269                R.dimen.recents_task_view_affiliate_group_enter_offset);
270
271        Task launchingTask = launchingTaskView.getTask();
272        List<TaskView> taskViews = mStackView.getTaskViews();
273        int taskViewCount = taskViews.size();
274        for (int i = 0; i < taskViewCount; i++) {
275            TaskView tv = taskViews.get(i);
276            Task task = tv.getTask();
277            boolean currentTaskOccludesLaunchTarget = (launchingTask != null &&
278                    launchingTask.group.isTaskAboveTask(task, launchingTask));
279
280            if (tv == launchingTaskView) {
281                tv.setClipViewInStack(false);
282                tv.onStartLaunchTargetLaunchAnimation(taskViewExitToAppDuration,
283                        screenPinningRequested, postAnimationTrigger);
284            } else if (currentTaskOccludesLaunchTarget) {
285                // Animate this task out of view
286                TaskViewAnimation taskAnimation = new TaskViewAnimation(
287                        taskViewExitToAppDuration, Interpolators.ALPHA_OUT,
288                        postAnimationTrigger.decrementOnAnimationEnd());
289                postAnimationTrigger.increment();
290
291                stackLayout.getStackTransform(task, stackScroller.getStackScroll(), mTmpTransform,
292                        null);
293                mTmpTransform.alpha = 0f;
294                mTmpTransform.rect.offset(0, taskViewAffiliateGroupEnterOffset);
295                mStackView.updateTaskViewToTransform(tv, mTmpTransform, taskAnimation);
296            }
297        }
298    }
299
300    /**
301     * Starts the delete animation for the specified {@link TaskView}.
302     */
303    public void startDeleteTaskAnimation(Task deleteTask, final TaskView deleteTaskView,
304            final ReferenceCountedTrigger postAnimationTrigger) {
305        Resources res = mStackView.getResources();
306        TaskStackLayoutAlgorithm stackLayout = mStackView.getStackAlgorithm();
307        TaskStackViewScroller stackScroller = mStackView.getScroller();
308
309        int taskViewRemoveAnimDuration = res.getInteger(
310                R.integer.recents_animate_task_view_remove_duration);
311        int taskViewRemoveAnimTranslationXPx = res.getDimensionPixelSize(
312                R.dimen.recents_task_view_remove_anim_translation_x);
313
314        // Disabling clipping with the stack while the view is animating away
315        deleteTaskView.setClipViewInStack(false);
316
317        // Compose the new animation and transform and star the animation
318        TaskViewAnimation taskAnimation = new TaskViewAnimation(taskViewRemoveAnimDuration,
319                Interpolators.ALPHA_OUT, new AnimatorListenerAdapter() {
320            @Override
321            public void onAnimationEnd(Animator animation) {
322                postAnimationTrigger.decrement();
323
324                // Re-enable clipping with the stack (we will reuse this view)
325                deleteTaskView.setClipViewInStack(true);
326            }
327        });
328        postAnimationTrigger.increment();
329
330        stackLayout.getStackTransform(deleteTask, stackScroller.getStackScroll(), mTmpTransform,
331                null);
332        mTmpTransform.alpha = 0f;
333        mTmpTransform.rect.offset(taskViewRemoveAnimTranslationXPx, 0);
334        mStackView.updateTaskViewToTransform(deleteTaskView, mTmpTransform, taskAnimation);
335    }
336
337    /**
338     * Starts the animation to hide the {@link TaskView}s when the history is shown.
339     */
340    public void startShowHistoryAnimation(ReferenceCountedTrigger postAnimationTrigger) {
341        Resources res = mStackView.getResources();
342        TaskStackLayoutAlgorithm stackLayout = mStackView.getStackAlgorithm();
343        TaskStackViewScroller stackScroller = mStackView.getScroller();
344
345        int offscreenY = stackLayout.mStackRect.bottom;
346        int historyTransitionDuration = res.getInteger(
347                R.integer.recents_history_transition_duration);
348        int startDelayIncr = 16;
349
350        List<TaskView> taskViews = mStackView.getTaskViews();
351        int taskViewCount = taskViews.size();
352        for (int i = taskViewCount - 1; i >= 0; i--) {
353            TaskView tv = taskViews.get(i);
354            Task task = tv.getTask();
355            TaskViewAnimation taskAnimation = new TaskViewAnimation(startDelayIncr * i,
356                    historyTransitionDuration, Interpolators.FAST_OUT_SLOW_IN,
357                    postAnimationTrigger.decrementOnAnimationEnd());
358            postAnimationTrigger.increment();
359
360            stackLayout.getStackTransform(task, stackScroller.getStackScroll(), mTmpTransform,
361                    null);
362            mTmpTransform.alpha = 0f;
363            mTmpTransform.rect.offsetTo(mTmpTransform.rect.left, offscreenY);
364            mStackView.updateTaskViewToTransform(tv, mTmpTransform, taskAnimation);
365        }
366    }
367
368    /**
369     * Starts the animation to show the {@link TaskView}s when the history is hidden.
370     */
371    public void startHideHistoryAnimation() {
372        Resources res = mStackView.getResources();
373        TaskStackLayoutAlgorithm stackLayout = mStackView.getStackAlgorithm();
374        TaskStackViewScroller stackScroller = mStackView.getScroller();
375
376        int historyTransitionDuration = res.getInteger(
377                R.integer.recents_history_transition_duration);
378        int startDelayIncr = 16;
379
380        List<TaskView> taskViews = mStackView.getTaskViews();
381        int taskViewCount = taskViews.size();
382        for (int i = taskViewCount - 1; i >= 0; i--) {
383            TaskView tv = taskViews.get(i);
384            TaskViewAnimation taskAnimation = new TaskViewAnimation(startDelayIncr * i,
385                    historyTransitionDuration, Interpolators.FAST_OUT_SLOW_IN);
386            stackLayout.getStackTransform(tv.getTask(), stackScroller.getStackScroll(),
387                    mTmpTransform, null);
388            mTmpTransform.alpha = 1f;
389            mStackView.updateTaskViewToTransform(tv, mTmpTransform, taskAnimation);
390        }
391    }
392}
393