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