blob: 55749348b004425b452d0631680db59993543678 [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.animation.ValueAnimator;
import android.content.Context;
import android.content.res.Configuration;
import android.content.res.Resources;
import android.util.Log;
import android.view.animation.Interpolator;
import android.view.animation.PathInterpolator;
import com.android.systemui.Interpolators;
import com.android.systemui.R;
import com.android.systemui.recents.LegacyRecentsImpl;
import com.android.systemui.recents.RecentsActivityLaunchState;
import com.android.systemui.recents.RecentsConfiguration;
import com.android.systemui.recents.RecentsDebugFlags;
import com.android.systemui.recents.events.EventBus;
import com.android.systemui.recents.events.component.SetWaitingForTransitionStartEvent;
import com.android.systemui.recents.misc.ReferenceCountedTrigger;
import com.android.systemui.shared.recents.model.Task;
import com.android.systemui.recents.model.TaskStack;
import com.android.systemui.recents.views.lowram.TaskStackLowRamLayoutAlgorithm;
import com.android.systemui.recents.utilities.AnimationProps;
import java.util.ArrayList;
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(TaskViewTransform transform, 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);
/**
* Callback to start the animation for the front {@link TaskView} if there is no launch
* target.
*/
void onStartFrontTaskEnterAnimation(boolean screenPinningEnabled);
}
private static final int DOUBLE_FRAME_OFFSET_MS = 33;
private static final int FRAME_OFFSET_MS = 16;
private static final int ENTER_EXIT_NUM_ANIMATING_TASKS = 5;
private static final int ENTER_FROM_HOME_ALPHA_DURATION = 100;
public static final int ENTER_FROM_HOME_TRANSLATION_DURATION = 300;
private static final Interpolator ENTER_FROM_HOME_ALPHA_INTERPOLATOR = Interpolators.LINEAR;
public static final int EXIT_TO_HOME_TRANSLATION_DURATION = 200;
private static final Interpolator EXIT_TO_HOME_TRANSLATION_INTERPOLATOR =
new PathInterpolator(0.4f, 0, 0.6f, 1f);
private static final int DISMISS_TASK_DURATION = 175;
private static final int DISMISS_ALL_TASKS_DURATION = 200;
private static final Interpolator DISMISS_ALL_TRANSLATION_INTERPOLATOR =
new PathInterpolator(0.4f, 0, 1f, 1f);
private static final Interpolator FOCUS_NEXT_TASK_INTERPOLATOR =
new PathInterpolator(0.4f, 0, 0, 1f);
private static final Interpolator FOCUS_IN_FRONT_NEXT_TASK_INTERPOLATOR =
new PathInterpolator(0, 0, 0, 1f);
private static final Interpolator FOCUS_BEHIND_NEXT_TASK_INTERPOLATOR =
Interpolators.LINEAR_OUT_SLOW_IN;
private static final Interpolator ENTER_WHILE_DOCKING_INTERPOLATOR =
Interpolators.LINEAR_OUT_SLOW_IN;
private final int mEnterAndExitFromHomeTranslationOffset;
private TaskStackView mStackView;
private TaskViewTransform mTmpTransform = new TaskViewTransform();
private ArrayList<TaskViewTransform> mTmpCurrentTaskTransforms = new ArrayList<>();
private ArrayList<TaskViewTransform> mTmpFinalTaskTransforms = new ArrayList<>();
public TaskStackAnimationHelper(Context context, TaskStackView stackView) {
mStackView = stackView;
mEnterAndExitFromHomeTranslationOffset = LegacyRecentsImpl.getConfiguration().isGridEnabled
? 0 : DOUBLE_FRAME_OFFSET_MS;
}
/**
* 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 = LegacyRecentsImpl.getConfiguration();
RecentsActivityLaunchState launchState = config.getLaunchState();
Resources res = mStackView.getResources();
Resources appResources = mStackView.getContext().getApplicationContext().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 offscreenYOffset = stackLayout.mStackRect.height();
int taskViewAffiliateGroupEnterOffset = res.getDimensionPixelSize(
R.dimen.recents_task_stack_animation_affiliate_enter_offset);
int launchedWhileDockingOffset = res.getDimensionPixelSize(
R.dimen.recents_task_stack_animation_launched_while_docking_offset);
boolean isLandscape = appResources.getConfiguration().orientation
== Configuration.ORIENTATION_LANDSCAPE;
float top = 0;
final boolean isLowRamDevice = LegacyRecentsImpl.getConfiguration().isLowRamDevice;
if (isLowRamDevice && launchState.launchedFromApp && !launchState.launchedViaDockGesture) {
stackLayout.getStackTransform(launchTargetTask, stackScroller.getStackScroll(),
mTmpTransform, null /* frontTransform */);
top = mTmpTransform.rect.top;
}
// 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();
// Get the current transform for the task, which will be used to position it offscreen
stackLayout.getStackTransform(task, stackScroller.getStackScroll(), mTmpTransform,
null);
if (launchState.launchedFromApp && !launchState.launchedViaDockGesture) {
if (task.isLaunchTarget) {
tv.onPrepareLaunchTargetForEnterAnimation();
} else if (isLowRamDevice && i >= taskViews.size() -
(TaskStackLowRamLayoutAlgorithm.MAX_LAYOUT_TASK_COUNT + 1)
&& !RecentsDebugFlags.Static.DisableRecentsLowRamEnterExitAnimation) {
// Move the last 2nd and 3rd last tasks in-app animation to match the motion of
// the last task's app transition
stackLayout.getStackTransform(task, stackScroller.getStackScroll(),
mTmpTransform, null);
mTmpTransform.rect.offset(0, -top);
mTmpTransform.alpha = 0f;
mStackView.updateTaskViewToTransform(tv, mTmpTransform,
AnimationProps.IMMEDIATE);
stackLayout.getStackTransform(task, stackScroller.getStackScroll(),
mTmpTransform, null);
mTmpTransform.alpha = 1f;
// Duration see {@link
// com.android.server.wm.AppTransition#DEFAULT_APP_TRANSITION_DURATION}
mStackView.updateTaskViewToTransform(tv, mTmpTransform,
new AnimationProps(336, Interpolators.FAST_OUT_SLOW_IN));
}
} else if (launchState.launchedFromHome) {
if (isLowRamDevice) {
mTmpTransform.rect.offset(0, stackLayout.getTaskRect().height() / 4);
} else {
// Move the task view off screen (below) so we can animate it in
mTmpTransform.rect.offset(0, offscreenYOffset);
}
mTmpTransform.alpha = 0f;
mStackView.updateTaskViewToTransform(tv, mTmpTransform, AnimationProps.IMMEDIATE);
} else if (launchState.launchedViaDockGesture) {
int offset = isLandscape
? launchedWhileDockingOffset
: (int) (offscreenYOffset * 0.9f);
mTmpTransform.rect.offset(0, offset);
mTmpTransform.alpha = 0f;
mStackView.updateTaskViewToTransform(tv, mTmpTransform, AnimationProps.IMMEDIATE);
}
}
}
/**
* 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 = LegacyRecentsImpl.getConfiguration();
RecentsActivityLaunchState launchState = config.getLaunchState();
Resources res = mStackView.getResources();
Resources appRes = mStackView.getContext().getApplicationContext().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;
}
final boolean isLowRamDevice = LegacyRecentsImpl.getConfiguration().isLowRamDevice;
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 dockGestureAnimDuration = appRes.getInteger(
R.integer.long_press_dock_anim_duration);
// Since low ram devices have an animation when entering app -> recents, do not allow
// toggle until the animation is complete
if (launchState.launchedFromApp && !launchState.launchedViaDockGesture && isLowRamDevice) {
postAnimationTrigger.addLastDecrementRunnable(() -> EventBus.getDefault()
.send(new SetWaitingForTransitionStartEvent(false)));
}
// 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--) {
int taskIndexFromFront = taskViewCount - i - 1;
int taskIndexFromBack = i;
final TaskView tv = taskViews.get(i);
Task task = tv.getTask();
// 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.launchedFromApp && !launchState.launchedViaDockGesture) {
if (task.isLaunchTarget) {
tv.onStartLaunchTargetEnterAnimation(mTmpTransform,
taskViewEnterFromAppDuration, mStackView.mScreenPinningEnabled,
postAnimationTrigger);
}
} else if (launchState.launchedFromHome) {
// Animate the tasks up, but offset the animations to be relative to the front-most
// task animation
final float startOffsetFraction = (float) (Math.min(ENTER_EXIT_NUM_ANIMATING_TASKS,
taskIndexFromFront) * mEnterAndExitFromHomeTranslationOffset) /
ENTER_FROM_HOME_TRANSLATION_DURATION;
AnimationProps taskAnimation = new AnimationProps()
.setInterpolator(AnimationProps.ALPHA, ENTER_FROM_HOME_ALPHA_INTERPOLATOR)
.setListener(postAnimationTrigger.decrementOnAnimationEnd());
if (isLowRamDevice) {
taskAnimation.setInterpolator(AnimationProps.BOUNDS,
Interpolators.FAST_OUT_SLOW_IN)
.setDuration(AnimationProps.BOUNDS, 150)
.setDuration(AnimationProps.ALPHA, 150);
} else {
taskAnimation.setStartDelay(AnimationProps.ALPHA,
Math.min(ENTER_EXIT_NUM_ANIMATING_TASKS, taskIndexFromFront) *
FRAME_OFFSET_MS)
.setInterpolator(AnimationProps.BOUNDS,
new RecentsEntrancePathInterpolator(0f, 0f, 0.2f, 1f,
startOffsetFraction))
.setDuration(AnimationProps.BOUNDS, ENTER_FROM_HOME_TRANSLATION_DURATION)
.setDuration(AnimationProps.ALPHA, ENTER_FROM_HOME_ALPHA_DURATION);
}
postAnimationTrigger.increment();
mStackView.updateTaskViewToTransform(tv, mTmpTransform, taskAnimation);
if (i == taskViewCount - 1) {
tv.onStartFrontTaskEnterAnimation(mStackView.mScreenPinningEnabled);
}
} else if (launchState.launchedViaDockGesture) {
// Animate the tasks up - add some delay to match the divider animation
AnimationProps taskAnimation = new AnimationProps()
.setDuration(AnimationProps.BOUNDS, dockGestureAnimDuration +
(taskIndexFromBack * DOUBLE_FRAME_OFFSET_MS))
.setInterpolator(AnimationProps.BOUNDS,
ENTER_WHILE_DOCKING_INTERPOLATOR)
.setStartDelay(AnimationProps.BOUNDS, 48)
.setListener(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) {
TaskStackLayoutAlgorithm stackLayout = mStackView.getStackAlgorithm();
TaskStack stack = mStackView.getStack();
// Break early if there are no tasks
if (stack.getTaskCount() == 0) {
return;
}
int offscreenYOffset = stackLayout.mStackRect.height();
// Create the animations for each of the tasks
List<TaskView> taskViews = mStackView.getTaskViews();
int taskViewCount = taskViews.size();
for (int i = 0; i < taskViewCount; i++) {
int taskIndexFromFront = taskViewCount - i - 1;
TaskView tv = taskViews.get(i);
Task task = tv.getTask();
if (mStackView.isIgnoredTask(task)) {
continue;
}
// Animate the tasks down
AnimationProps taskAnimation;
if (animated) {
int delay = Math.min(ENTER_EXIT_NUM_ANIMATING_TASKS , taskIndexFromFront) *
mEnterAndExitFromHomeTranslationOffset;
taskAnimation = new AnimationProps()
.setDuration(AnimationProps.BOUNDS, EXIT_TO_HOME_TRANSLATION_DURATION)
.setListener(postAnimationTrigger.decrementOnAnimationEnd());
if (LegacyRecentsImpl.getConfiguration().isLowRamDevice) {
taskAnimation.setInterpolator(AnimationProps.BOUNDS,
Interpolators.FAST_OUT_SLOW_IN);
} else {
taskAnimation.setStartDelay(AnimationProps.BOUNDS, delay)
.setInterpolator(AnimationProps.BOUNDS,
EXIT_TO_HOME_TRANSLATION_INTERPOLATOR);
}
postAnimationTrigger.increment();
} else {
taskAnimation = AnimationProps.IMMEDIATE;
}
mTmpTransform.fillIn(tv);
if (LegacyRecentsImpl.getConfiguration().isLowRamDevice) {
taskAnimation.setInterpolator(AnimationProps.ALPHA,
EXIT_TO_HOME_TRANSLATION_INTERPOLATOR)
.setDuration(AnimationProps.ALPHA, EXIT_TO_HOME_TRANSLATION_DURATION);
mTmpTransform.rect.offset(0, stackLayout.mTaskStackLowRamLayoutAlgorithm
.getTaskRect().height() / 4);
mTmpTransform.alpha = 0f;
} else {
mTmpTransform.rect.offset(0, offscreenYOffset);
}
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();
int taskViewExitToAppDuration = res.getInteger(
R.integer.recents_task_exit_to_app_duration);
int taskViewAffiliateGroupEnterOffset = res.getDimensionPixelSize(
R.dimen.recents_task_stack_animation_affiliate_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();
if (tv == launchingTaskView) {
tv.setClipViewInStack(false);
postAnimationTrigger.addLastDecrementRunnable(new Runnable() {
@Override
public void run() {
tv.setClipViewInStack(true);
}
});
tv.onStartLaunchTargetLaunchAnimation(taskViewExitToAppDuration,
screenPinningRequested, postAnimationTrigger);
}
}
}
/**
* Starts the delete animation for the specified {@link TaskView}.
*/
public void startDeleteTaskAnimation(final TaskView deleteTaskView, boolean gridLayout,
final ReferenceCountedTrigger postAnimationTrigger) {
if (gridLayout) {
startTaskGridDeleteTaskAnimation(deleteTaskView, postAnimationTrigger);
} else {
startTaskStackDeleteTaskAnimation(deleteTaskView, postAnimationTrigger);
}
}
/**
* Starts the delete animation for all the {@link TaskView}s.
*/
public void startDeleteAllTasksAnimation(final List<TaskView> taskViews, boolean gridLayout,
final ReferenceCountedTrigger postAnimationTrigger) {
if (gridLayout) {
for (int i = 0; i < taskViews.size(); i++) {
startTaskGridDeleteTaskAnimation(taskViews.get(i), postAnimationTrigger);
}
} else {
startTaskStackDeleteAllTasksAnimation(taskViews, postAnimationTrigger);
}
}
/**
* Starts the animation to focus the next {@link TaskView} when paging through recents.
*
* @return whether or not this will trigger a scroll in the stack
*/
public boolean startScrollToFocusedTaskAnimation(Task newFocusedTask,
boolean requestViewFocus) {
TaskStackLayoutAlgorithm stackLayout = mStackView.getStackAlgorithm();
TaskStackViewScroller stackScroller = mStackView.getScroller();
TaskStack stack = mStackView.getStack();
final float curScroll = stackScroller.getStackScroll();
final float newScroll = stackScroller.getBoundedStackScroll(
stackLayout.getStackScrollForTask(newFocusedTask));
boolean willScrollToFront = newScroll > curScroll;
boolean willScroll = Float.compare(newScroll, curScroll) != 0;
// Get the current set of task transforms
int taskViewCount = mStackView.getTaskViews().size();
ArrayList<Task> stackTasks = stack.getTasks();
mStackView.getCurrentTaskTransforms(stackTasks, mTmpCurrentTaskTransforms);
// Pick up the newly visible views after the scroll
mStackView.bindVisibleTaskViews(newScroll);
// Update the internal state
stackLayout.setFocusState(TaskStackLayoutAlgorithm.STATE_FOCUSED);
stackScroller.setStackScroll(newScroll, null /* animation */);
mStackView.cancelDeferredTaskViewLayoutAnimation();
// Get the final set of task transforms
mStackView.getLayoutTaskTransforms(newScroll, stackLayout.getFocusState(), stackTasks,
true /* ignoreTaskOverrides */, mTmpFinalTaskTransforms);
// Focus the task view
TaskView newFocusedTaskView = mStackView.getChildViewForTask(newFocusedTask);
if (newFocusedTaskView == null) {
// Log the error if we have no task view, and skip the animation
Log.e("TaskStackAnimationHelper", "b/27389156 null-task-view prebind:" + taskViewCount +
" postbind:" + mStackView.getTaskViews().size() + " prescroll:" + curScroll +
" postscroll: " + newScroll);
return false;
}
newFocusedTaskView.setFocusedState(true, requestViewFocus);
// Setup the end listener to return all the hidden views to the view pool after the
// focus animation
ReferenceCountedTrigger postAnimTrigger = new ReferenceCountedTrigger();
postAnimTrigger.addLastDecrementRunnable(new Runnable() {
@Override
public void run() {
mStackView.bindVisibleTaskViews(newScroll);
}
});
List<TaskView> taskViews = mStackView.getTaskViews();
taskViewCount = taskViews.size();
int newFocusTaskViewIndex = taskViews.indexOf(newFocusedTaskView);
for (int i = 0; i < taskViewCount; i++) {
TaskView tv = taskViews.get(i);
Task task = tv.getTask();
if (mStackView.isIgnoredTask(task)) {
continue;
}
int taskIndex = stackTasks.indexOf(task);
TaskViewTransform fromTransform = mTmpCurrentTaskTransforms.get(taskIndex);
TaskViewTransform toTransform = mTmpFinalTaskTransforms.get(taskIndex);
// Update the task to the initial state (for the newly picked up tasks)
mStackView.updateTaskViewToTransform(tv, fromTransform, AnimationProps.IMMEDIATE);
int duration;
Interpolator interpolator;
if (willScrollToFront) {
duration = calculateStaggeredAnimDuration(i);
interpolator = FOCUS_BEHIND_NEXT_TASK_INTERPOLATOR;
} else {
if (i < newFocusTaskViewIndex) {
duration = 150 + ((newFocusTaskViewIndex - i - 1) * 50);
interpolator = FOCUS_BEHIND_NEXT_TASK_INTERPOLATOR;
} else if (i > newFocusTaskViewIndex) {
duration = Math.max(100, 150 - ((i - newFocusTaskViewIndex - 1) * 50));
interpolator = FOCUS_IN_FRONT_NEXT_TASK_INTERPOLATOR;
} else {
duration = 200;
interpolator = FOCUS_NEXT_TASK_INTERPOLATOR;
}
}
AnimationProps anim = new AnimationProps()
.setDuration(AnimationProps.BOUNDS, duration)
.setInterpolator(AnimationProps.BOUNDS, interpolator)
.setListener(postAnimTrigger.decrementOnAnimationEnd());
postAnimTrigger.increment();
mStackView.updateTaskViewToTransform(tv, toTransform, anim);
}
return willScroll;
}
/**
* Starts the animation to go to the initial stack layout with a task focused. In addition, the
* previous task will be animated in after the scroll completes.
*/
public void startNewStackScrollAnimation(TaskStack newStack,
ReferenceCountedTrigger animationTrigger) {
TaskStackLayoutAlgorithm stackLayout = mStackView.getStackAlgorithm();
TaskStackViewScroller stackScroller = mStackView.getScroller();
// Get the current set of task transforms
ArrayList<Task> stackTasks = newStack.getTasks();
mStackView.getCurrentTaskTransforms(stackTasks, mTmpCurrentTaskTransforms);
// Update the stack
mStackView.setTasks(newStack, false /* allowNotifyStackChanges */);
mStackView.updateLayoutAlgorithm(false /* boundScroll */);
// Pick up the newly visible views after the scroll
final float newScroll = stackLayout.mInitialScrollP;
mStackView.bindVisibleTaskViews(newScroll);
// Update the internal state
stackLayout.setFocusState(TaskStackLayoutAlgorithm.STATE_UNFOCUSED);
stackLayout.setTaskOverridesForInitialState(newStack, true /* ignoreScrollToFront */);
stackScroller.setStackScroll(newScroll);
mStackView.cancelDeferredTaskViewLayoutAnimation();
// Get the final set of task transforms
mStackView.getLayoutTaskTransforms(newScroll, stackLayout.getFocusState(), stackTasks,
false /* ignoreTaskOverrides */, mTmpFinalTaskTransforms);
// Hide the front most task view until the scroll is complete
Task frontMostTask = newStack.getFrontMostTask();
final TaskView frontMostTaskView = mStackView.getChildViewForTask(frontMostTask);
final TaskViewTransform frontMostTransform = mTmpFinalTaskTransforms.get(
stackTasks.indexOf(frontMostTask));
if (frontMostTaskView != null) {
mStackView.updateTaskViewToTransform(frontMostTaskView,
stackLayout.getFrontOfStackTransform(), AnimationProps.IMMEDIATE);
}
// Setup the end listener to return all the hidden views to the view pool after the
// focus animation
animationTrigger.addLastDecrementRunnable(new Runnable() {
@Override
public void run() {
mStackView.bindVisibleTaskViews(newScroll);
// Now, animate in the front-most task
if (frontMostTaskView != null) {
mStackView.updateTaskViewToTransform(frontMostTaskView, frontMostTransform,
new AnimationProps(75, 250, FOCUS_BEHIND_NEXT_TASK_INTERPOLATOR));
}
}
});
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();
if (mStackView.isIgnoredTask(task)) {
continue;
}
if (task == frontMostTask && frontMostTaskView != null) {
continue;
}
int taskIndex = stackTasks.indexOf(task);
TaskViewTransform fromTransform = mTmpCurrentTaskTransforms.get(taskIndex);
TaskViewTransform toTransform = mTmpFinalTaskTransforms.get(taskIndex);
// Update the task to the initial state (for the newly picked up tasks)
mStackView.updateTaskViewToTransform(tv, fromTransform, AnimationProps.IMMEDIATE);
int duration = calculateStaggeredAnimDuration(i);
Interpolator interpolator = FOCUS_BEHIND_NEXT_TASK_INTERPOLATOR;
AnimationProps anim = new AnimationProps()
.setDuration(AnimationProps.BOUNDS, duration)
.setInterpolator(AnimationProps.BOUNDS, interpolator)
.setListener(animationTrigger.decrementOnAnimationEnd());
animationTrigger.increment();
mStackView.updateTaskViewToTransform(tv, toTransform, anim);
}
}
/**
* Caclulates a staggered duration for {@link #startScrollToFocusedTaskAnimation} and
* {@link #startNewStackScrollAnimation}.
*/
private int calculateStaggeredAnimDuration(int i) {
return Math.max(100, 100 + ((i - 1) * 50));
}
private void startTaskGridDeleteTaskAnimation(final TaskView deleteTaskView,
final ReferenceCountedTrigger postAnimationTrigger) {
postAnimationTrigger.increment();
postAnimationTrigger.addLastDecrementRunnable(() -> {
mStackView.getTouchHandler().onChildDismissed(deleteTaskView);
});
deleteTaskView.animate().setDuration(300).scaleX(0.9f).scaleY(0.9f).alpha(0).setListener(
new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
postAnimationTrigger.decrement();
}}).start();
}
private void startTaskStackDeleteTaskAnimation(final TaskView deleteTaskView,
final ReferenceCountedTrigger postAnimationTrigger) {
TaskStackViewTouchHandler touchHandler = mStackView.getTouchHandler();
touchHandler.onBeginManualDrag(deleteTaskView);
postAnimationTrigger.increment();
postAnimationTrigger.addLastDecrementRunnable(() -> {
touchHandler.onChildDismissed(deleteTaskView);
});
final float dismissSize = touchHandler.getScaledDismissSize();
ValueAnimator animator = ValueAnimator.ofFloat(0f, 1f);
animator.setDuration(400);
animator.addUpdateListener((animation) -> {
float progress = (Float) animation.getAnimatedValue();
deleteTaskView.setTranslationX(progress * dismissSize);
touchHandler.updateSwipeProgress(deleteTaskView, true, progress);
});
animator.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
postAnimationTrigger.decrement();
}
});
animator.start();
}
private void startTaskStackDeleteAllTasksAnimation(final List<TaskView> taskViews,
final ReferenceCountedTrigger postAnimationTrigger) {
TaskStackLayoutAlgorithm stackLayout = mStackView.getStackAlgorithm();
int offscreenXOffset = mStackView.getMeasuredWidth() - stackLayout.getTaskRect().left;
int taskViewCount = taskViews.size();
for (int i = taskViewCount - 1; i >= 0; i--) {
TaskView tv = taskViews.get(i);
int taskIndexFromFront = taskViewCount - i - 1;
int startDelay = taskIndexFromFront * DOUBLE_FRAME_OFFSET_MS;
// Disabling clipping with the stack while the view is animating away
tv.setClipViewInStack(false);
// Compose the new animation and transform and star the animation
AnimationProps taskAnimation = new AnimationProps(startDelay,
DISMISS_ALL_TASKS_DURATION, DISMISS_ALL_TRANSLATION_INTERPOLATOR,
new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
postAnimationTrigger.decrement();
// Re-enable clipping with the stack (we will reuse this view)
tv.setClipViewInStack(true);
}
});
postAnimationTrigger.increment();
mTmpTransform.fillIn(tv);
mTmpTransform.rect.offset(offscreenXOffset, 0);
mStackView.updateTaskViewToTransform(tv, mTmpTransform, taskAnimation);
}
}
}