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