1 /* 2 * Copyright (C) 2023 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.bubbles; 17 18 import static java.lang.Math.abs; 19 20 import android.animation.Animator; 21 import android.animation.AnimatorListenerAdapter; 22 import android.animation.AnimatorSet; 23 import android.annotation.Nullable; 24 import android.view.InsetsController; 25 import android.view.MotionEvent; 26 import android.view.View; 27 28 import com.android.launcher3.R; 29 import com.android.launcher3.anim.AnimatedFloat; 30 import com.android.launcher3.taskbar.StashedHandleViewController; 31 import com.android.launcher3.taskbar.TaskbarActivityContext; 32 import com.android.launcher3.taskbar.TaskbarControllers; 33 import com.android.launcher3.taskbar.TaskbarInsetsController; 34 import com.android.launcher3.taskbar.TaskbarStashController; 35 import com.android.launcher3.util.MultiPropertyFactory; 36 import com.android.wm.shell.common.bubbles.BubbleBarLocation; 37 import com.android.wm.shell.shared.animation.PhysicsAnimator; 38 39 /** 40 * Coordinates between controllers such as BubbleBarView and BubbleHandleViewController to 41 * create a cohesive animation between stashed/unstashed states. 42 */ 43 public class BubbleStashController { 44 45 private static final String TAG = "BubbleStashController"; 46 47 /** 48 * How long to stash/unstash. 49 */ 50 public static final long BAR_STASH_DURATION = InsetsController.ANIMATION_DURATION_RESIZE; 51 52 /** 53 * The scale bubble bar animates to when being stashed. 54 */ 55 private static final float STASHED_BAR_SCALE = 0.5f; 56 57 protected final TaskbarActivityContext mActivity; 58 59 // Initialized in init. 60 private TaskbarControllers mControllers; 61 private TaskbarInsetsController mTaskbarInsetsController; 62 private BubbleBarViewController mBarViewController; 63 private BubbleStashedHandleViewController mHandleViewController; 64 private TaskbarStashController mTaskbarStashController; 65 66 private MultiPropertyFactory.MultiProperty mIconAlphaForStash; 67 private AnimatedFloat mIconScaleForStash; 68 private AnimatedFloat mIconTranslationYForStash; 69 private MultiPropertyFactory.MultiProperty mBubbleStashedHandleAlpha; 70 71 private boolean mRequestedStashState; 72 private boolean mRequestedExpandedState; 73 74 private boolean mIsStashed; 75 private int mStashedHeight; 76 private int mUnstashedHeight; 77 private boolean mBubblesShowingOnHome; 78 private boolean mBubblesShowingOnOverview; 79 private boolean mIsSysuiLocked; 80 81 private final float mHandleCenterFromScreenBottom; 82 83 @Nullable 84 private AnimatorSet mAnimator; 85 BubbleStashController(TaskbarActivityContext activity)86 public BubbleStashController(TaskbarActivityContext activity) { 87 mActivity = activity; 88 // the handle is centered within the stashed taskbar area 89 mHandleCenterFromScreenBottom = 90 mActivity.getResources().getDimensionPixelSize(R.dimen.bubblebar_stashed_size) / 2f; 91 } 92 init(TaskbarControllers controllers, BubbleControllers bubbleControllers)93 public void init(TaskbarControllers controllers, BubbleControllers bubbleControllers) { 94 mControllers = controllers; 95 mTaskbarInsetsController = controllers.taskbarInsetsController; 96 mBarViewController = bubbleControllers.bubbleBarViewController; 97 mHandleViewController = bubbleControllers.bubbleStashedHandleViewController; 98 mTaskbarStashController = controllers.taskbarStashController; 99 100 mIconAlphaForStash = mBarViewController.getBubbleBarAlpha().get(0); 101 mIconScaleForStash = mBarViewController.getBubbleBarScale(); 102 mIconTranslationYForStash = mBarViewController.getBubbleBarTranslationY(); 103 104 mBubbleStashedHandleAlpha = mHandleViewController.getStashedHandleAlpha().get( 105 StashedHandleViewController.ALPHA_INDEX_STASHED); 106 107 mStashedHeight = mHandleViewController.getStashedHeight(); 108 mUnstashedHeight = mHandleViewController.getUnstashedHeight(); 109 } 110 111 /** 112 * Returns the touchable height of the bubble bar based on it's stashed state. 113 */ getTouchableHeight()114 public int getTouchableHeight() { 115 return mIsStashed ? mStashedHeight : mUnstashedHeight; 116 } 117 118 /** 119 * Returns whether the bubble bar is currently stashed. 120 */ isStashed()121 public boolean isStashed() { 122 return mIsStashed; 123 } 124 125 /** 126 * Animates the handle (or bubble bar depending on state) to be visible after the device is 127 * unlocked. 128 * 129 * <p>Normally either the bubble bar or the handle is visible, 130 * and {@link #showBubbleBar(boolean)} and {@link #stashBubbleBar()} are used to transition 131 * between these two states. But the transition from the state where both the bar and handle 132 * are invisible is slightly different. 133 */ animateAfterUnlock()134 private void animateAfterUnlock() { 135 AnimatorSet animatorSet = new AnimatorSet(); 136 if (mBubblesShowingOnHome || mBubblesShowingOnOverview) { 137 mIsStashed = false; 138 animatorSet.playTogether(mIconScaleForStash.animateToValue(1), 139 mIconTranslationYForStash.animateToValue(getBubbleBarTranslationY()), 140 mIconAlphaForStash.animateToValue(1)); 141 } else { 142 mIsStashed = true; 143 animatorSet.playTogether(mBubbleStashedHandleAlpha.animateToValue(1)); 144 } 145 146 animatorSet.addListener(new AnimatorListenerAdapter() { 147 @Override 148 public void onAnimationEnd(Animator animation) { 149 onIsStashedChanged(); 150 } 151 }); 152 animatorSet.setDuration(BAR_STASH_DURATION).start(); 153 } 154 155 /** 156 * Called when launcher enters or exits the home page. Bubbles are unstashed on home. 157 */ setBubblesShowingOnHome(boolean onHome)158 public void setBubblesShowingOnHome(boolean onHome) { 159 if (mBubblesShowingOnHome != onHome) { 160 mBubblesShowingOnHome = onHome; 161 162 if (!mBarViewController.hasBubbles()) { 163 // if there are no bubbles, there's nothing to show, so just return. 164 return; 165 } 166 167 if (mBubblesShowingOnHome) { 168 showBubbleBar(/* expanded= */ false); 169 // When transitioning from app to home the stash animator may already have been 170 // created, so we need to animate the bubble bar here to align with hotseat. 171 if (!mIsStashed) { 172 mIconTranslationYForStash.animateToValue(getBubbleBarTranslationYForHotseat()) 173 .start(); 174 } 175 // If the bubble bar is already unstashed, the taskbar touchable region won't be 176 // updated correctly, so force an update here. 177 mControllers.runAfterInit(() -> 178 mTaskbarInsetsController.onTaskbarOrBubblebarWindowHeightOrInsetsChanged()); 179 } else if (!mBarViewController.isExpanded()) { 180 stashBubbleBar(); 181 } 182 } 183 } 184 185 /** Whether bubbles are showing on the launcher home page. */ isBubblesShowingOnHome()186 public boolean isBubblesShowingOnHome() { 187 return mBubblesShowingOnHome; 188 } 189 190 // TODO: when tapping on an app in overview, this is a bit delayed compared to taskbar stashing 191 /** Called when launcher enters or exits overview. Bubbles are unstashed on overview. */ setBubblesShowingOnOverview(boolean onOverview)192 public void setBubblesShowingOnOverview(boolean onOverview) { 193 if (mBubblesShowingOnOverview != onOverview) { 194 mBubblesShowingOnOverview = onOverview; 195 if (!mBubblesShowingOnOverview && !mBarViewController.isExpanded()) { 196 stashBubbleBar(); 197 } else { 198 // When transitioning to overview the stash animator may already have been 199 // created, so we need to animate the bubble bar here to align with taskbar. 200 mIconTranslationYForStash.animateToValue(getBubbleBarTranslationYForTaskbar()) 201 .start(); 202 } 203 } 204 } 205 206 /** Whether bubbles are showing on Overview. */ isBubblesShowingOnOverview()207 public boolean isBubblesShowingOnOverview() { 208 return mBubblesShowingOnOverview; 209 } 210 211 /** Called when sysui locked state changes, when locked, bubble bar is stashed. */ onSysuiLockedStateChange(boolean isSysuiLocked)212 public void onSysuiLockedStateChange(boolean isSysuiLocked) { 213 if (isSysuiLocked != mIsSysuiLocked) { 214 mIsSysuiLocked = isSysuiLocked; 215 if (!mIsSysuiLocked && mBarViewController.hasBubbles()) { 216 animateAfterUnlock(); 217 } 218 } 219 } 220 221 /** 222 * Stashes the bubble bar if allowed based on other state (e.g. on home and overview the 223 * bar does not stash). 224 */ stashBubbleBar()225 public void stashBubbleBar() { 226 mRequestedStashState = true; 227 mRequestedExpandedState = false; 228 updateStashedAndExpandedState(); 229 } 230 231 /** 232 * Shows the bubble bar, and expands bubbles depending on {@param expandBubbles}. 233 */ showBubbleBar(boolean expandBubbles)234 public void showBubbleBar(boolean expandBubbles) { 235 mRequestedStashState = false; 236 mRequestedExpandedState = expandBubbles; 237 updateStashedAndExpandedState(); 238 } 239 updateStashedAndExpandedState()240 private void updateStashedAndExpandedState() { 241 if (mBarViewController.isHiddenForNoBubbles()) { 242 // If there are no bubbles the bar and handle are invisible, nothing to do here. 243 return; 244 } 245 boolean isStashed = mRequestedStashState 246 && !mBubblesShowingOnHome 247 && !mBubblesShowingOnOverview; 248 if (mIsStashed != isStashed) { 249 // notify the view controller that the stash state is about to change so that it can 250 // cancel an ongoing animation if there is one. 251 // note that this has to be called before updating mIsStashed with the new value, 252 // otherwise interrupting an ongoing animation may update it again with the wrong state 253 mBarViewController.onStashStateChanging(); 254 mIsStashed = isStashed; 255 if (mAnimator != null) { 256 mAnimator.cancel(); 257 } 258 mAnimator = createStashAnimator(mIsStashed, BAR_STASH_DURATION); 259 mAnimator.start(); 260 onIsStashedChanged(); 261 } 262 if (mBarViewController.isExpanded() != mRequestedExpandedState) { 263 mBarViewController.setExpanded(mRequestedExpandedState); 264 } 265 } 266 267 /** 268 * Create a stash animation. 269 * 270 * @param isStashed whether it's a stash animation or an unstash animation 271 * @param duration duration of the animation 272 * @return the animation 273 */ createStashAnimator(boolean isStashed, long duration)274 private AnimatorSet createStashAnimator(boolean isStashed, long duration) { 275 AnimatorSet animatorSet = new AnimatorSet(); 276 277 AnimatorSet fullLengthAnimatorSet = new AnimatorSet(); 278 // Not exactly half and may overlap. See [first|second]HalfDurationScale below. 279 AnimatorSet firstHalfAnimatorSet = new AnimatorSet(); 280 AnimatorSet secondHalfAnimatorSet = new AnimatorSet(); 281 282 final float firstHalfDurationScale; 283 final float secondHalfDurationScale; 284 285 if (isStashed) { 286 firstHalfDurationScale = 0.75f; 287 secondHalfDurationScale = 0.5f; 288 289 fullLengthAnimatorSet.play( 290 mIconTranslationYForStash.animateToValue(getStashTranslation())); 291 292 firstHalfAnimatorSet.playTogether( 293 mIconAlphaForStash.animateToValue(0), 294 mIconScaleForStash.animateToValue(STASHED_BAR_SCALE)); 295 secondHalfAnimatorSet.playTogether( 296 mBubbleStashedHandleAlpha.animateToValue(1)); 297 } else { 298 firstHalfDurationScale = 0.5f; 299 secondHalfDurationScale = 0.75f; 300 301 final float translationY = getBubbleBarTranslationY(); 302 303 fullLengthAnimatorSet.playTogether( 304 mIconScaleForStash.animateToValue(1), 305 mIconTranslationYForStash.animateToValue(translationY)); 306 307 firstHalfAnimatorSet.playTogether( 308 mBubbleStashedHandleAlpha.animateToValue(0) 309 ); 310 secondHalfAnimatorSet.playTogether( 311 mIconAlphaForStash.animateToValue(1) 312 ); 313 } 314 315 fullLengthAnimatorSet.play(mHandleViewController.createRevealAnimToIsStashed(isStashed)); 316 317 fullLengthAnimatorSet.setDuration(duration); 318 firstHalfAnimatorSet.setDuration((long) (duration * firstHalfDurationScale)); 319 secondHalfAnimatorSet.setDuration((long) (duration * secondHalfDurationScale)); 320 secondHalfAnimatorSet.setStartDelay((long) (duration * (1 - secondHalfDurationScale))); 321 322 animatorSet.playTogether(fullLengthAnimatorSet, firstHalfAnimatorSet, 323 secondHalfAnimatorSet); 324 animatorSet.addListener(new AnimatorListenerAdapter() { 325 @Override 326 public void onAnimationEnd(Animator animation) { 327 mAnimator = null; 328 mControllers.runAfterInit(() -> { 329 if (isStashed) { 330 mBarViewController.setExpanded(false); 331 } 332 mTaskbarInsetsController.onTaskbarOrBubblebarWindowHeightOrInsetsChanged(); 333 }); 334 } 335 }); 336 return animatorSet; 337 } 338 getStashTranslation()339 private float getStashTranslation() { 340 return (mUnstashedHeight - mStashedHeight) / 2f; 341 } 342 onIsStashedChanged()343 private void onIsStashedChanged() { 344 mControllers.runAfterInit(() -> { 345 mHandleViewController.onIsStashedChanged(); 346 mTaskbarInsetsController.onTaskbarOrBubblebarWindowHeightOrInsetsChanged(); 347 }); 348 } 349 getBubbleBarTranslationYForTaskbar()350 public float getBubbleBarTranslationYForTaskbar() { 351 return -mActivity.getDeviceProfile().taskbarBottomMargin; 352 } 353 getBubbleBarTranslationYForHotseat()354 private float getBubbleBarTranslationYForHotseat() { 355 final float hotseatBottomSpace = mActivity.getDeviceProfile().hotseatBarBottomSpacePx; 356 final float hotseatCellHeight = mActivity.getDeviceProfile().hotseatCellHeightPx; 357 return -hotseatBottomSpace - hotseatCellHeight + mUnstashedHeight - abs( 358 hotseatCellHeight - mUnstashedHeight) / 2; 359 } 360 getBubbleBarTranslationY()361 public float getBubbleBarTranslationY() { 362 // If we're on home, adjust the translation so the bubble bar aligns with hotseat. 363 // Otherwise we're either showing in an app or in overview. In either case adjust it so 364 // the bubble bar aligns with the taskbar. 365 return mBubblesShowingOnHome ? getBubbleBarTranslationYForHotseat() 366 : getBubbleBarTranslationYForTaskbar(); 367 } 368 369 /** 370 * The difference on the Y axis between the center of the handle and the center of the bubble 371 * bar. 372 */ getDiffBetweenHandleAndBarCenters()373 public float getDiffBetweenHandleAndBarCenters() { 374 // the difference between the centers of the handle and the bubble bar is the difference 375 // between their distance from the bottom of the screen. 376 377 float barCenter = mBarViewController.getBubbleBarCollapsedHeight() / 2f; 378 return mHandleCenterFromScreenBottom - barCenter; 379 } 380 381 /** The distance the handle moves as part of the new bubble animation. */ getStashedHandleTranslationForNewBubbleAnimation()382 public float getStashedHandleTranslationForNewBubbleAnimation() { 383 // the should move up to the top of the stashed taskbar area. it is centered within it so 384 // it should move the same distance as it is away from the bottom. 385 return -mHandleCenterFromScreenBottom; 386 } 387 388 /** Checks whether the motion event is over the stash handle. */ isEventOverStashHandle(MotionEvent ev)389 public boolean isEventOverStashHandle(MotionEvent ev) { 390 return mHandleViewController.isEventOverHandle(ev); 391 } 392 393 /** Set a bubble bar location */ setBubbleBarLocation(BubbleBarLocation bubbleBarLocation)394 public void setBubbleBarLocation(BubbleBarLocation bubbleBarLocation) { 395 mHandleViewController.setBubbleBarLocation(bubbleBarLocation); 396 } 397 398 /** Returns the [PhysicsAnimator] for the stashed handle view. */ getStashedHandlePhysicsAnimator()399 public PhysicsAnimator<View> getStashedHandlePhysicsAnimator() { 400 return mHandleViewController.getPhysicsAnimator(); 401 } 402 403 /** Notifies taskbar that it should update its touchable region. */ updateTaskbarTouchRegion()404 public void updateTaskbarTouchRegion() { 405 mTaskbarInsetsController.onTaskbarOrBubblebarWindowHeightOrInsetsChanged(); 406 } 407 408 /** Shows the bubble bar immediately without animation. */ showBubbleBarImmediate()409 public void showBubbleBarImmediate() { 410 mHandleViewController.setTranslationYForSwipe(0); 411 mIconTranslationYForStash.updateValue(getBubbleBarTranslationY()); 412 mIconAlphaForStash.setValue(1); 413 mIconScaleForStash.updateValue(1); 414 mIsStashed = false; 415 onIsStashedChanged(); 416 } 417 418 /** Stashes the bubble bar immediately without animation. */ stashBubbleBarImmediate()419 public void stashBubbleBarImmediate() { 420 mHandleViewController.setTranslationYForSwipe(0); 421 mBubbleStashedHandleAlpha.setValue(1); 422 mIconAlphaForStash.setValue(0); 423 mIconTranslationYForStash.updateValue(getStashTranslation()); 424 mIconScaleForStash.updateValue(STASHED_BAR_SCALE); 425 mIsStashed = true; 426 onIsStashedChanged(); 427 } 428 429 /** 430 * Updates the values of the internal animators after the new bubble animation was interrupted 431 * 432 * @param isStashed whether the current state should be stashed 433 * @param bubbleBarTranslationY the current bubble bar translation. this is only used if the 434 * bubble bar is showing to ensure that the stash animator runs 435 * smoothly. 436 */ onNewBubbleAnimationInterrupted(boolean isStashed, float bubbleBarTranslationY)437 public void onNewBubbleAnimationInterrupted(boolean isStashed, float bubbleBarTranslationY) { 438 if (isStashed) { 439 mBubbleStashedHandleAlpha.setValue(1); 440 mIconAlphaForStash.setValue(0); 441 mIconScaleForStash.updateValue(STASHED_BAR_SCALE); 442 mIconTranslationYForStash.updateValue(getStashTranslation()); 443 } else { 444 mBubbleStashedHandleAlpha.setValue(0); 445 mHandleViewController.setTranslationYForSwipe(0); 446 mIconAlphaForStash.setValue(1); 447 mIconScaleForStash.updateValue(1); 448 mIconTranslationYForStash.updateValue(bubbleBarTranslationY); 449 } 450 mIsStashed = isStashed; 451 onIsStashedChanged(); 452 } 453 454 /** Set the translation Y for the stashed handle. */ setHandleTranslationY(float ty)455 public void setHandleTranslationY(float ty) { 456 mHandleViewController.setTranslationYForSwipe(ty); 457 } 458 } 459