blob: ccbb329bb151b3bd6fb925aae1341e6f6bf81846 [file] [log] [blame]
/*
* Copyright (C) 2015 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.systemui.recents.views;
import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.content.Context;
import android.content.res.Resources;
import android.graphics.RectF;
import android.view.View;
import android.view.animation.AnimationUtils;
import android.view.animation.Interpolator;
import com.android.systemui.R;
import com.android.systemui.recents.Recents;
import com.android.systemui.recents.RecentsActivityLaunchState;
import com.android.systemui.recents.RecentsConfiguration;
import com.android.systemui.recents.misc.ReferenceCountedTrigger;
import com.android.systemui.recents.model.Task;
import com.android.systemui.recents.model.TaskStack;
import com.android.systemui.statusbar.phone.PhoneStatusBar;
import java.util.List;
/**
* A helper class to create task view animations for {@link TaskView}s in a {@link TaskStackView},
* but not the contents of the {@link TaskView}s.
*/
public class TaskStackAnimationHelper {
/**
* Callbacks from the helper to coordinate view-content animations with view animations.
*/
public interface Callbacks {
/**
* Callback to prepare for the start animation for the launch target {@link TaskView}.
*/
void onPrepareLaunchTargetForEnterAnimation();
/**
* Callback to start the animation for the launch target {@link TaskView}.
*/
void onStartLaunchTargetEnterAnimation(int duration, boolean screenPinningEnabled,
ReferenceCountedTrigger postAnimationTrigger);
/**
* Callback to start the animation for the launch target {@link TaskView} when it is
* launched from Recents.
*/
void onStartLaunchTargetLaunchAnimation(int duration, boolean screenPinningRequested,
ReferenceCountedTrigger postAnimationTrigger);
}
private TaskStackView mStackView;
private Interpolator mFastOutSlowInInterpolator;
private Interpolator mFastOutLinearInInterpolator;
private Interpolator mQuintOutInterpolator;
private TaskViewTransform mTmpTransform = new TaskViewTransform();
public TaskStackAnimationHelper(Context context, TaskStackView stackView) {
mStackView = stackView;
mFastOutSlowInInterpolator = AnimationUtils.loadInterpolator(context,
com.android.internal.R.interpolator.fast_out_slow_in);
mFastOutLinearInInterpolator = AnimationUtils.loadInterpolator(context,
com.android.internal.R.interpolator.fast_out_linear_in);
mQuintOutInterpolator = AnimationUtils.loadInterpolator(context,
com.android.internal.R.interpolator.decelerate_quint);
}
/**
* Prepares the stack views and puts them in their initial animation state while visible, before
* the in-app enter animations start (after the window-transition completes).
*/
public void prepareForEnterAnimation() {
RecentsConfiguration config = Recents.getConfiguration();
RecentsActivityLaunchState launchState = config.getLaunchState();
Resources res = mStackView.getResources();
TaskStackLayoutAlgorithm stackLayout = mStackView.getStackAlgorithm();
TaskStackViewScroller stackScroller = mStackView.getScroller();
TaskStack stack = mStackView.getStack();
Task launchTargetTask = stack.getLaunchTarget();
// Break early if there are no tasks
if (stack.getTaskCount() == 0) {
return;
}
int offscreenY = stackLayout.mStackRect.bottom;
int taskViewAffiliateGroupEnterOffset = res.getDimensionPixelSize(
R.dimen.recents_task_view_affiliate_group_enter_offset);
// Prepare each of the task views for their enter animation from front to back
List<TaskView> taskViews = mStackView.getTaskViews();
for (int i = taskViews.size() - 1; i >= 0; i--) {
TaskView tv = taskViews.get(i);
Task task = tv.getTask();
boolean currentTaskOccludesLaunchTarget = (launchTargetTask != null &&
launchTargetTask.group.isTaskAboveTask(task, launchTargetTask));
boolean hideTask = (launchTargetTask != null &&
launchTargetTask.isFreeformTask() && task.isFreeformTask());
// Get the current transform for the task, which will be used to position it offscreen
stackLayout.getStackTransform(task, stackScroller.getStackScroll(), mTmpTransform,
null);
if (hideTask) {
tv.setVisibility(View.INVISIBLE);
} else if (launchState.launchedHasConfigurationChanged) {
// Just load the views as-is
} else if (launchState.launchedFromAppWithThumbnail) {
if (task.isLaunchTarget) {
tv.onPrepareLaunchTargetForEnterAnimation();
} else if (currentTaskOccludesLaunchTarget) {
// Move the task view slightly lower so we can animate it in
RectF bounds = new RectF(mTmpTransform.rect);
bounds.offset(0, taskViewAffiliateGroupEnterOffset);
tv.setClipViewInStack(false);
tv.setAlpha(0f);
tv.setLeftTopRightBottom((int) bounds.left, (int) bounds.top,
(int) bounds.right, (int) bounds.bottom);
}
} else if (launchState.launchedFromHome) {
// Move the task view off screen (below) so we can animate it in
RectF bounds = new RectF(mTmpTransform.rect);
bounds.offset(0, offscreenY);
tv.setLeftTopRightBottom((int) bounds.left, (int) bounds.top, (int) bounds.right,
(int) bounds.bottom);
}
}
}
/**
* Starts the in-app enter animation, which animates the {@link TaskView}s to their final places
* depending on how Recents was triggered.
*/
public void startEnterAnimation(final ReferenceCountedTrigger postAnimationTrigger) {
RecentsConfiguration config = Recents.getConfiguration();
RecentsActivityLaunchState launchState = config.getLaunchState();
Resources res = mStackView.getResources();
TaskStackLayoutAlgorithm stackLayout = mStackView.getStackAlgorithm();
TaskStackViewScroller stackScroller = mStackView.getScroller();
TaskStack stack = mStackView.getStack();
Task launchTargetTask = stack.getLaunchTarget();
// Break early if there are no tasks
if (stack.getTaskCount() == 0) {
return;
}
int taskViewEnterFromAppDuration = res.getInteger(
R.integer.recents_task_enter_from_app_duration);
int taskViewEnterFromAffiliatedAppDuration = res.getInteger(
R.integer.recents_task_enter_from_affiliated_app_duration);
int taskViewEnterFromHomeDuration = res.getInteger(
R.integer.recents_task_enter_from_home_duration);
int taskViewEnterFromHomeStaggerDelay = res.getInteger(
R.integer.recents_task_enter_from_home_stagger_delay);
// Create enter animations for each of the views from front to back
List<TaskView> taskViews = mStackView.getTaskViews();
int taskViewCount = taskViews.size();
for (int i = taskViewCount - 1; i >= 0; i--) {
final TaskView tv = taskViews.get(i);
Task task = tv.getTask();
boolean currentTaskOccludesLaunchTarget = false;
if (launchTargetTask != null) {
currentTaskOccludesLaunchTarget = launchTargetTask.group.isTaskAboveTask(task,
launchTargetTask);
}
// Get the current transform for the task, which will be updated to the final transform
// to animate to depending on how recents was invoked
stackLayout.getStackTransform(task, stackScroller.getStackScroll(), mTmpTransform,
null);
if (launchState.launchedFromAppWithThumbnail) {
if (task.isLaunchTarget) {
tv.onStartLaunchTargetEnterAnimation(taskViewEnterFromAppDuration,
mStackView.mScreenPinningEnabled, postAnimationTrigger);
} else {
// Animate the task up if it was occluding the launch target
if (currentTaskOccludesLaunchTarget) {
TaskViewAnimation taskAnimation = new TaskViewAnimation(
taskViewEnterFromAffiliatedAppDuration, PhoneStatusBar.ALPHA_IN,
new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
postAnimationTrigger.decrement();
tv.setClipViewInStack(false);
}
});
postAnimationTrigger.increment();
mStackView.updateTaskViewToTransform(tv, mTmpTransform, taskAnimation);
}
}
} else if (launchState.launchedFromHome) {
// Animate the tasks up
int frontIndex = (taskViewCount - i - 1);
int delay = frontIndex * taskViewEnterFromHomeStaggerDelay;
int duration = taskViewEnterFromHomeDuration +
frontIndex * taskViewEnterFromHomeStaggerDelay;
TaskViewAnimation taskAnimation = new TaskViewAnimation(delay,
duration, mQuintOutInterpolator,
postAnimationTrigger.decrementOnAnimationEnd());
postAnimationTrigger.increment();
mStackView.updateTaskViewToTransform(tv, mTmpTransform, taskAnimation);
}
}
}
/**
* Starts an in-app animation to hide all the task views so that we can transition back home.
*/
public void startExitToHomeAnimation(boolean animated,
ReferenceCountedTrigger postAnimationTrigger) {
Resources res = mStackView.getResources();
TaskStackLayoutAlgorithm stackLayout = mStackView.getStackAlgorithm();
TaskStackViewScroller stackScroller = mStackView.getScroller();
TaskStack stack = mStackView.getStack();
// Break early if there are no tasks
if (stack.getTaskCount() == 0) {
return;
}
int offscreenY = stackLayout.mStackRect.bottom;
int taskViewExitToHomeDuration = res.getInteger(
R.integer.recents_task_exit_to_home_duration);
// Create the animations for each of the tasks
List<TaskView> taskViews = mStackView.getTaskViews();
int taskViewCount = taskViews.size();
for (int i = 0; i < taskViewCount; i++) {
TaskView tv = taskViews.get(i);
Task task = tv.getTask();
TaskViewAnimation taskAnimation = new TaskViewAnimation(
animated ? taskViewExitToHomeDuration : 0, mFastOutLinearInInterpolator,
postAnimationTrigger.decrementOnAnimationEnd());
postAnimationTrigger.increment();
stackLayout.getStackTransform(task, stackScroller.getStackScroll(), mTmpTransform,
null);
mTmpTransform.rect.offset(0, offscreenY);
mStackView.updateTaskViewToTransform(tv, mTmpTransform, taskAnimation);
}
}
/**
* Starts the animation for the launching task view, hiding any tasks that might occlude the
* window transition for the launching task.
*/
public void startLaunchTaskAnimation(TaskView launchingTaskView, boolean screenPinningRequested,
final ReferenceCountedTrigger postAnimationTrigger) {
Resources res = mStackView.getResources();
TaskStackLayoutAlgorithm stackLayout = mStackView.getStackAlgorithm();
TaskStackViewScroller stackScroller = mStackView.getScroller();
int taskViewExitToAppDuration = res.getInteger(
R.integer.recents_task_exit_to_app_duration);
int taskViewAffiliateGroupEnterOffset = res.getDimensionPixelSize(
R.dimen.recents_task_view_affiliate_group_enter_offset);
Task launchingTask = launchingTaskView.getTask();
List<TaskView> taskViews = mStackView.getTaskViews();
int taskViewCount = taskViews.size();
for (int i = 0; i < taskViewCount; i++) {
TaskView tv = taskViews.get(i);
Task task = tv.getTask();
boolean currentTaskOccludesLaunchTarget = (launchingTask != null &&
launchingTask.group.isTaskAboveTask(task, launchingTask));
if (tv == launchingTaskView) {
tv.setClipViewInStack(false);
tv.onStartLaunchTargetLaunchAnimation(taskViewExitToAppDuration,
screenPinningRequested, postAnimationTrigger);
} else if (currentTaskOccludesLaunchTarget) {
// Animate this task out of view
TaskViewAnimation taskAnimation = new TaskViewAnimation(
taskViewExitToAppDuration, PhoneStatusBar.ALPHA_OUT,
postAnimationTrigger.decrementOnAnimationEnd());
postAnimationTrigger.increment();
stackLayout.getStackTransform(task, stackScroller.getStackScroll(), mTmpTransform,
null);
mTmpTransform.alpha = 0f;
mTmpTransform.rect.offset(0, taskViewAffiliateGroupEnterOffset);
mStackView.updateTaskViewToTransform(tv, mTmpTransform, taskAnimation);
}
}
}
/**
* Starts the delete animation for the specified {@link TaskView}.
*/
public void startDeleteTaskAnimation(Task deleteTask, final TaskView deleteTaskView,
final ReferenceCountedTrigger postAnimationTrigger) {
Resources res = mStackView.getResources();
TaskStackLayoutAlgorithm stackLayout = mStackView.getStackAlgorithm();
TaskStackViewScroller stackScroller = mStackView.getScroller();
int taskViewRemoveAnimDuration = res.getInteger(
R.integer.recents_animate_task_view_remove_duration);
int taskViewRemoveAnimTranslationXPx = res.getDimensionPixelSize(
R.dimen.recents_task_view_remove_anim_translation_x);
// Disabling clipping with the stack while the view is animating away
deleteTaskView.setClipViewInStack(false);
// Compose the new animation and transform and star the animation
TaskViewAnimation taskAnimation = new TaskViewAnimation(taskViewRemoveAnimDuration,
PhoneStatusBar.ALPHA_OUT, new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
postAnimationTrigger.decrement();
// Re-enable clipping with the stack (we will reuse this view)
deleteTaskView.setClipViewInStack(true);
}
});
postAnimationTrigger.increment();
stackLayout.getStackTransform(deleteTask, stackScroller.getStackScroll(), mTmpTransform,
null);
mTmpTransform.alpha = 0f;
mTmpTransform.rect.offset(taskViewRemoveAnimTranslationXPx, 0);
mStackView.updateTaskViewToTransform(deleteTaskView, mTmpTransform, taskAnimation);
}
/**
* Starts the animation to hide the {@link TaskView}s when the history is shown. The history
* view's animation will be deferred until all the {@link TaskView}s are finished animating.
*/
public void startShowHistoryAnimation(ReferenceCountedTrigger postAnimationTrigger) {
Resources res = mStackView.getResources();
TaskStackLayoutAlgorithm stackLayout = mStackView.getStackAlgorithm();
TaskStackViewScroller stackScroller = mStackView.getScroller();
int historyTransitionDuration = res.getInteger(
R.integer.recents_history_transition_duration);
List<TaskView> taskViews = mStackView.getTaskViews();
int taskViewCount = taskViews.size();
for (int i = taskViewCount - 1; i >= 0; i--) {
TaskView tv = taskViews.get(i);
Task task = tv.getTask();
TaskViewAnimation taskAnimation = new TaskViewAnimation(
historyTransitionDuration, PhoneStatusBar.ALPHA_OUT,
postAnimationTrigger.decrementOnAnimationEnd());
postAnimationTrigger.increment();
stackLayout.getStackTransform(task, stackScroller.getStackScroll(), mTmpTransform,
null);
mTmpTransform.alpha = 0f;
mStackView.updateTaskViewToTransform(tv, mTmpTransform, taskAnimation);
}
}
/**
* Starts the animation to show the {@link TaskView}s when the history is hidden. The
* {@link TaskView} animations will be deferred until the history view has been animated away.
*/
public void startHideHistoryAnimation(final ReferenceCountedTrigger postAnimationTrigger) {
final Resources res = mStackView.getResources();
final TaskStackLayoutAlgorithm stackLayout = mStackView.getStackAlgorithm();
final TaskStackViewScroller stackScroller = mStackView.getScroller();
final int historyTransitionDuration = res.getInteger(
R.integer.recents_history_transition_duration);
List<TaskView> taskViews = mStackView.getTaskViews();
int taskViewCount = taskViews.size();
for (int i = taskViewCount - 1; i >= 0; i--) {
final TaskView tv = taskViews.get(i);
postAnimationTrigger.addLastDecrementRunnable(new Runnable() {
@Override
public void run() {
TaskViewAnimation taskAnimation = new TaskViewAnimation(
historyTransitionDuration, PhoneStatusBar.ALPHA_IN);
stackLayout.getStackTransform(tv.getTask(), stackScroller.getStackScroll(),
mTmpTransform, null);
mTmpTransform.alpha = 1f;
mStackView.updateTaskViewToTransform(tv, mTmpTransform, taskAnimation);
}
});
}
}
}