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