1 /* 2 * Copyright (C) 2021 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 package com.android.launcher3.taskbar; 17 18 import static com.android.launcher3.taskbar.TaskbarStashController.FLAG_IN_APP; 19 import static com.android.launcher3.taskbar.TaskbarStashController.FLAG_IN_STASHED_LAUNCHER_STATE; 20 import static com.android.launcher3.taskbar.TaskbarStashController.TASKBAR_STASH_DURATION; 21 import static com.android.launcher3.taskbar.TaskbarViewController.ALPHA_INDEX_HOME; 22 23 import android.animation.Animator; 24 import android.animation.AnimatorListenerAdapter; 25 import android.animation.AnimatorSet; 26 import android.animation.ObjectAnimator; 27 28 import androidx.annotation.NonNull; 29 30 import com.android.launcher3.BaseQuickstepLauncher; 31 import com.android.launcher3.LauncherState; 32 import com.android.launcher3.statemanager.StateManager; 33 import com.android.launcher3.util.MultiValueAlpha; 34 import com.android.quickstep.AnimatedFloat; 35 import com.android.quickstep.RecentsAnimationCallbacks; 36 import com.android.quickstep.RecentsAnimationController; 37 import com.android.quickstep.views.RecentsView; 38 import com.android.systemui.shared.recents.model.ThumbnailData; 39 40 import java.util.HashMap; 41 import java.util.function.Consumer; 42 import java.util.function.Supplier; 43 44 /** 45 * Track LauncherState, RecentsAnimation, resumed state for task bar in one place here and animate 46 * the task bar accordingly. 47 */ 48 public class TaskbarLauncherStateController { 49 50 public static final int FLAG_RESUMED = 1 << 0; 51 public static final int FLAG_RECENTS_ANIMATION_RUNNING = 1 << 1; 52 public static final int FLAG_TRANSITION_STATE_RUNNING = 1 << 2; 53 54 /** Equivalent to an int with all 1s for binary operation purposes */ 55 private static final int FLAGS_ALL = ~0; 56 57 private final AnimatedFloat mIconAlignmentForResumedState = 58 new AnimatedFloat(this::onIconAlignmentRatioChangedForAppAndHomeTransition); 59 private final AnimatedFloat mIconAlignmentForGestureState = 60 new AnimatedFloat(this::onIconAlignmentRatioChangedForAppAndHomeTransition); 61 private final AnimatedFloat mIconAlignmentForLauncherState = 62 new AnimatedFloat(this::onIconAlignmentRatioChangedForStateTransition); 63 64 private TaskbarControllers mControllers; 65 private AnimatedFloat mTaskbarBackgroundAlpha; 66 private MultiValueAlpha.AlphaProperty mIconAlphaForHome; 67 private BaseQuickstepLauncher mLauncher; 68 69 private Integer mPrevState; 70 private int mState; 71 private LauncherState mLauncherState = LauncherState.NORMAL; 72 73 private boolean mIsAnimatingToLauncherViaGesture; 74 private boolean mIsAnimatingToLauncherViaResume; 75 76 private final StateManager.StateListener<LauncherState> mStateListener = 77 new StateManager.StateListener<LauncherState>() { 78 79 @Override 80 public void onStateTransitionStart(LauncherState toState) { 81 if (toState != mLauncherState) { 82 // Treat FLAG_TRANSITION_STATE_RUNNING as a changed flag even if a previous 83 // state transition was already running, so we update the new target. 84 mPrevState &= ~FLAG_TRANSITION_STATE_RUNNING; 85 mLauncherState = toState; 86 } 87 updateStateForFlag(FLAG_TRANSITION_STATE_RUNNING, true); 88 applyState(); 89 } 90 91 @Override 92 public void onStateTransitionComplete(LauncherState finalState) { 93 mLauncherState = finalState; 94 updateStateForFlag(FLAG_TRANSITION_STATE_RUNNING, false); 95 applyState(); 96 } 97 }; 98 init(TaskbarControllers controllers, BaseQuickstepLauncher launcher)99 public void init(TaskbarControllers controllers, BaseQuickstepLauncher launcher) { 100 mControllers = controllers; 101 mLauncher = launcher; 102 103 mTaskbarBackgroundAlpha = mControllers.taskbarDragLayerController 104 .getTaskbarBackgroundAlpha(); 105 MultiValueAlpha taskbarIconAlpha = mControllers.taskbarViewController.getTaskbarIconAlpha(); 106 mIconAlphaForHome = taskbarIconAlpha.getProperty(ALPHA_INDEX_HOME); 107 mIconAlphaForHome.setConsumer( 108 (Consumer<Float>) alpha -> mLauncher.getHotseat().setIconsAlpha(alpha > 0 ? 0 : 1)); 109 110 mIconAlignmentForResumedState.finishAnimation(); 111 onIconAlignmentRatioChangedForAppAndHomeTransition(); 112 113 mLauncher.getStateManager().addStateListener(mStateListener); 114 115 // Initialize to the current launcher state 116 updateStateForFlag(FLAG_RESUMED, launcher.hasBeenResumed()); 117 mLauncherState = launcher.getStateManager().getState(); 118 applyState(0); 119 } 120 onDestroy()121 public void onDestroy() { 122 mIconAlignmentForResumedState.finishAnimation(); 123 mIconAlignmentForGestureState.finishAnimation(); 124 mIconAlignmentForLauncherState.finishAnimation(); 125 126 mIconAlphaForHome.setConsumer(null); 127 mLauncher.getHotseat().setIconsAlpha(1f); 128 mLauncher.getStateManager().removeStateListener(mStateListener); 129 } 130 createAnimToLauncher(@onNull LauncherState toState, @NonNull RecentsAnimationCallbacks callbacks, long duration)131 public Animator createAnimToLauncher(@NonNull LauncherState toState, 132 @NonNull RecentsAnimationCallbacks callbacks, long duration) { 133 // If going to overview, stash the task bar 134 // If going home, align the icons to hotseat 135 AnimatorSet animatorSet = new AnimatorSet(); 136 137 // Update stashed flags first to ensure goingToUnstashedLauncherState() returns correctly. 138 TaskbarStashController stashController = mControllers.taskbarStashController; 139 stashController.updateStateForFlag(FLAG_IN_STASHED_LAUNCHER_STATE, 140 toState.isTaskbarStashed(mLauncher)); 141 stashController.updateStateForFlag(FLAG_IN_APP, false); 142 143 updateStateForFlag(FLAG_RECENTS_ANIMATION_RUNNING, true); 144 animatorSet.play(stashController.applyStateWithoutStart(duration)); 145 animatorSet.play(applyState(duration, false)); 146 147 TaskBarRecentsAnimationListener listener = new TaskBarRecentsAnimationListener(callbacks); 148 callbacks.addListener(listener); 149 RecentsView recentsView = mLauncher.getOverviewPanel(); 150 recentsView.setTaskLaunchListener(() -> { 151 listener.endGestureStateOverride(true); 152 callbacks.removeListener(listener); 153 }); 154 return animatorSet; 155 } 156 isAnimatingToLauncher()157 public boolean isAnimatingToLauncher() { 158 return mIsAnimatingToLauncherViaResume || mIsAnimatingToLauncherViaGesture; 159 } 160 161 /** 162 * Updates the proper flag to change the state of the task bar. 163 * 164 * Note that this only updates the flag. {@link #applyState()} needs to be called separately. 165 * 166 * @param flag The flag to update. 167 * @param enabled Whether to enable the flag 168 */ updateStateForFlag(int flag, boolean enabled)169 public void updateStateForFlag(int flag, boolean enabled) { 170 if (enabled) { 171 mState |= flag; 172 } else { 173 mState &= ~flag; 174 } 175 } 176 hasAnyFlag(int flagMask)177 private boolean hasAnyFlag(int flagMask) { 178 return hasAnyFlag(mState, flagMask); 179 } 180 hasAnyFlag(int flags, int flagMask)181 private boolean hasAnyFlag(int flags, int flagMask) { 182 return (flags & flagMask) != 0; 183 } 184 applyState()185 public void applyState() { 186 applyState(TASKBAR_STASH_DURATION); 187 } 188 applyState(long duration)189 public void applyState(long duration) { 190 applyState(duration, true); 191 } 192 applyState(boolean start)193 public Animator applyState(boolean start) { 194 return applyState(TASKBAR_STASH_DURATION, start); 195 } 196 applyState(long duration, boolean start)197 public Animator applyState(long duration, boolean start) { 198 Animator animator = null; 199 if (mPrevState == null || mPrevState != mState) { 200 // If this is our initial state, treat all flags as changed. 201 int changedFlags = mPrevState == null ? FLAGS_ALL : mPrevState ^ mState; 202 mPrevState = mState; 203 animator = onStateChangeApplied(changedFlags, duration, start); 204 } 205 return animator; 206 } 207 onStateChangeApplied(int changedFlags, long duration, boolean start)208 private Animator onStateChangeApplied(int changedFlags, long duration, boolean start) { 209 AnimatorSet animatorSet = new AnimatorSet(); 210 if (hasAnyFlag(changedFlags, FLAG_RESUMED)) { 211 boolean isResumed = isResumed(); 212 ObjectAnimator anim = mIconAlignmentForResumedState 213 .animateToValue(isResumed && goingToUnstashedLauncherState() 214 ? 1 : 0) 215 .setDuration(duration); 216 217 anim.addListener(new AnimatorListenerAdapter() { 218 @Override 219 public void onAnimationEnd(Animator animation) { 220 mIsAnimatingToLauncherViaResume = false; 221 } 222 223 @Override 224 public void onAnimationStart(Animator animation) { 225 mIsAnimatingToLauncherViaResume = isResumed; 226 227 TaskbarStashController stashController = mControllers.taskbarStashController; 228 stashController.updateStateForFlag(FLAG_IN_APP, !isResumed); 229 stashController.applyState(duration); 230 } 231 }); 232 animatorSet.play(anim); 233 } 234 235 if (hasAnyFlag(changedFlags, FLAG_RECENTS_ANIMATION_RUNNING)) { 236 boolean isRecentsAnimationRunning = isRecentsAnimationRunning(); 237 Animator animator = mIconAlignmentForGestureState 238 .animateToValue(isRecentsAnimationRunning && goingToUnstashedLauncherState() 239 ? 1 : 0); 240 if (isRecentsAnimationRunning) { 241 animator.setDuration(duration); 242 } 243 animator.addListener(new AnimatorListenerAdapter() { 244 @Override 245 public void onAnimationEnd(Animator animation) { 246 mIsAnimatingToLauncherViaGesture = false; 247 } 248 249 @Override 250 public void onAnimationStart(Animator animation) { 251 mIsAnimatingToLauncherViaGesture = isRecentsAnimationRunning(); 252 } 253 }); 254 animatorSet.play(animator); 255 } 256 257 if (hasAnyFlag(changedFlags, FLAG_RESUMED | FLAG_RECENTS_ANIMATION_RUNNING)) { 258 boolean goingToLauncher = hasAnyFlag(FLAG_RESUMED | FLAG_RECENTS_ANIMATION_RUNNING); 259 animatorSet.play(mTaskbarBackgroundAlpha.animateToValue(goingToLauncher ? 0 : 1) 260 .setDuration(duration)); 261 } 262 263 if (hasAnyFlag(changedFlags, FLAG_TRANSITION_STATE_RUNNING)) { 264 boolean committed = !hasAnyFlag(FLAG_TRANSITION_STATE_RUNNING); 265 playStateTransitionAnim(animatorSet, duration, committed); 266 267 if (committed && mLauncherState == LauncherState.QUICK_SWITCH) { 268 // We're about to be paused, set immediately to ensure seamless handoff. 269 updateStateForFlag(FLAG_RESUMED, false); 270 applyState(0 /* duration */); 271 } 272 } 273 274 if (start) { 275 animatorSet.start(); 276 } 277 return animatorSet; 278 } 279 280 /** Returns whether we're going to a state where taskbar icons should align with launcher. */ goingToUnstashedLauncherState()281 private boolean goingToUnstashedLauncherState() { 282 return !mControllers.taskbarStashController.isInStashedLauncherState(); 283 } 284 playStateTransitionAnim(AnimatorSet animatorSet, long duration, boolean committed)285 private void playStateTransitionAnim(AnimatorSet animatorSet, long duration, 286 boolean committed) { 287 boolean isInStashedState = mLauncherState.isTaskbarStashed(mLauncher); 288 float toAlignment = mLauncherState.isTaskbarAlignedWithHotseat(mLauncher) ? 1 : 0; 289 290 TaskbarStashController controller = mControllers.taskbarStashController; 291 controller.updateStateForFlag(FLAG_IN_STASHED_LAUNCHER_STATE, isInStashedState); 292 Animator stashAnimator = controller.applyStateWithoutStart(duration); 293 if (stashAnimator != null) { 294 stashAnimator.addListener(new AnimatorListenerAdapter() { 295 @Override 296 public void onAnimationEnd(Animator animation) { 297 if (isInStashedState && committed) { 298 // Reset hotseat alpha to default 299 mLauncher.getHotseat().setIconsAlpha(1); 300 } 301 } 302 303 @Override 304 public void onAnimationStart(Animator animation) { 305 if (mLauncher.getHotseat().getIconsAlpha() > 0) { 306 mIconAlphaForHome.setValue(mLauncher.getHotseat().getIconsAlpha()); 307 } 308 } 309 }); 310 animatorSet.play(stashAnimator); 311 } 312 313 animatorSet.play(mIconAlignmentForLauncherState.animateToValue(toAlignment) 314 .setDuration(duration)); 315 } 316 isResumed()317 private boolean isResumed() { 318 return (mState & FLAG_RESUMED) != 0; 319 } 320 isRecentsAnimationRunning()321 private boolean isRecentsAnimationRunning() { 322 return (mState & FLAG_RECENTS_ANIMATION_RUNNING) != 0; 323 } 324 onIconAlignmentRatioChangedForStateTransition()325 private void onIconAlignmentRatioChangedForStateTransition() { 326 if (!isResumed()) { 327 return; 328 } 329 onIconAlignmentRatioChanged(this::getCurrentIconAlignmentRatioForLauncherState); 330 } 331 onIconAlignmentRatioChangedForAppAndHomeTransition()332 private void onIconAlignmentRatioChangedForAppAndHomeTransition() { 333 onIconAlignmentRatioChanged(this::getCurrentIconAlignmentRatioBetweenAppAndHome); 334 } 335 onIconAlignmentRatioChanged(Supplier<Float> alignmentSupplier)336 private void onIconAlignmentRatioChanged(Supplier<Float> alignmentSupplier) { 337 if (mControllers == null) { 338 return; 339 } 340 float alignment = alignmentSupplier.get(); 341 mControllers.taskbarViewController.setLauncherIconAlignment( 342 alignment, mLauncher.getDeviceProfile()); 343 344 // Switch taskbar and hotseat in last frame 345 setTaskbarViewVisible(alignment < 1); 346 } 347 348 private float getCurrentIconAlignmentRatioBetweenAppAndHome() { 349 return Math.max(mIconAlignmentForResumedState.value, mIconAlignmentForGestureState.value); 350 } 351 352 private float getCurrentIconAlignmentRatioForLauncherState() { 353 return mIconAlignmentForLauncherState.value; 354 } 355 356 private void setTaskbarViewVisible(boolean isVisible) { 357 mIconAlphaForHome.setValue(isVisible ? 1 : 0); 358 } 359 360 private final class TaskBarRecentsAnimationListener implements 361 RecentsAnimationCallbacks.RecentsAnimationListener { 362 private final RecentsAnimationCallbacks mCallbacks; 363 364 TaskBarRecentsAnimationListener(RecentsAnimationCallbacks callbacks) { 365 mCallbacks = callbacks; 366 } 367 368 @Override 369 public void onRecentsAnimationCanceled(HashMap<Integer, ThumbnailData> thumbnailDatas) { 370 endGestureStateOverride(true); 371 } 372 373 @Override 374 public void onRecentsAnimationFinished(RecentsAnimationController controller) { 375 endGestureStateOverride(!controller.getFinishTargetIsLauncher()); 376 } 377 378 private void endGestureStateOverride(boolean finishedToApp) { 379 mCallbacks.removeListener(this); 380 381 // Update the resumed state immediately to ensure a seamless handoff 382 boolean launcherResumed = !finishedToApp; 383 updateStateForFlag(FLAG_RECENTS_ANIMATION_RUNNING, false); 384 updateStateForFlag(FLAG_RESUMED, launcherResumed); 385 applyState(); 386 // Set this last because applyState() might also animate it. 387 mIconAlignmentForResumedState.cancelAnimation(); 388 mIconAlignmentForResumedState.updateValue(launcherResumed ? 1 : 0); 389 390 TaskbarStashController controller = mControllers.taskbarStashController; 391 controller.updateStateForFlag(FLAG_IN_APP, finishedToApp); 392 controller.applyState(); 393 } 394 } 395 } 396