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 26 import com.android.launcher3.anim.AnimatedFloat; 27 import com.android.launcher3.taskbar.StashedHandleViewController; 28 import com.android.launcher3.taskbar.TaskbarActivityContext; 29 import com.android.launcher3.taskbar.TaskbarControllers; 30 import com.android.launcher3.taskbar.TaskbarInsetsController; 31 import com.android.launcher3.taskbar.TaskbarStashController; 32 import com.android.launcher3.util.MultiPropertyFactory; 33 34 /** 35 * Coordinates between controllers such as BubbleBarView and BubbleHandleViewController to 36 * create a cohesive animation between stashed/unstashed states. 37 */ 38 public class BubbleStashController { 39 40 private static final String TAG = BubbleStashController.class.getSimpleName(); 41 42 /** 43 * How long to stash/unstash. 44 */ 45 public static final long BAR_STASH_DURATION = InsetsController.ANIMATION_DURATION_RESIZE; 46 47 /** 48 * The scale bubble bar animates to when being stashed. 49 */ 50 private static final float STASHED_BAR_SCALE = 0.5f; 51 52 protected final TaskbarActivityContext mActivity; 53 54 // Initialized in init. 55 private TaskbarControllers mControllers; 56 private TaskbarInsetsController mTaskbarInsetsController; 57 private BubbleBarViewController mBarViewController; 58 private BubbleStashedHandleViewController mHandleViewController; 59 private TaskbarStashController mTaskbarStashController; 60 61 private MultiPropertyFactory.MultiProperty mIconAlphaForStash; 62 private AnimatedFloat mIconScaleForStash; 63 private AnimatedFloat mIconTranslationYForStash; 64 private MultiPropertyFactory.MultiProperty mBubbleStashedHandleAlpha; 65 66 private boolean mRequestedStashState; 67 private boolean mRequestedExpandedState; 68 69 private boolean mIsStashed; 70 private int mStashedHeight; 71 private int mUnstashedHeight; 72 private boolean mBubblesShowingOnHome; 73 private boolean mBubblesShowingOnOverview; 74 private boolean mIsSysuiLocked; 75 76 @Nullable 77 private AnimatorSet mAnimator; 78 BubbleStashController(TaskbarActivityContext activity)79 public BubbleStashController(TaskbarActivityContext activity) { 80 mActivity = activity; 81 } 82 init(TaskbarControllers controllers, BubbleControllers bubbleControllers)83 public void init(TaskbarControllers controllers, BubbleControllers bubbleControllers) { 84 mControllers = controllers; 85 mTaskbarInsetsController = controllers.taskbarInsetsController; 86 mBarViewController = bubbleControllers.bubbleBarViewController; 87 mHandleViewController = bubbleControllers.bubbleStashedHandleViewController; 88 mTaskbarStashController = controllers.taskbarStashController; 89 90 mIconAlphaForStash = mBarViewController.getBubbleBarAlpha().get(0); 91 mIconScaleForStash = mBarViewController.getBubbleBarScale(); 92 mIconTranslationYForStash = mBarViewController.getBubbleBarTranslationY(); 93 94 mBubbleStashedHandleAlpha = mHandleViewController.getStashedHandleAlpha().get( 95 StashedHandleViewController.ALPHA_INDEX_STASHED); 96 97 mStashedHeight = mHandleViewController.getStashedHeight(); 98 mUnstashedHeight = mHandleViewController.getUnstashedHeight(); 99 } 100 101 /** 102 * Returns the touchable height of the bubble bar based on it's stashed state. 103 */ getTouchableHeight()104 public int getTouchableHeight() { 105 return mIsStashed ? mStashedHeight : mUnstashedHeight; 106 } 107 108 /** 109 * Returns whether the bubble bar is currently stashed. 110 */ isStashed()111 public boolean isStashed() { 112 return mIsStashed; 113 } 114 115 /** 116 * Animates the bubble bar and handle to their initial state, transitioning from the state where 117 * both views are invisible. Called when the first bubble is added or when the device is 118 * unlocked. 119 * 120 * <p>Normally either the bubble bar or the handle is visible, 121 * and {@link #showBubbleBar(boolean)} and {@link #stashBubbleBar()} are used to transition 122 * between these two states. But the transition from the state where both the bar and handle 123 * are invisible is slightly different. 124 * 125 * <p>The initial state will depend on the current state of the device, i.e. overview, home etc 126 * and whether bubbles are requested to be expanded. 127 */ animateToInitialState(boolean expanding)128 public void animateToInitialState(boolean expanding) { 129 AnimatorSet animatorSet = new AnimatorSet(); 130 if (expanding || mBubblesShowingOnHome || mBubblesShowingOnOverview) { 131 mIsStashed = false; 132 animatorSet.playTogether(mIconScaleForStash.animateToValue(1), 133 mIconTranslationYForStash.animateToValue(getBubbleBarTranslationY()), 134 mIconAlphaForStash.animateToValue(1)); 135 } else { 136 mIsStashed = true; 137 animatorSet.playTogether(mBubbleStashedHandleAlpha.animateToValue(1)); 138 } 139 140 animatorSet.addListener(new AnimatorListenerAdapter() { 141 @Override 142 public void onAnimationEnd(Animator animation) { 143 onIsStashedChanged(); 144 } 145 }); 146 animatorSet.setDuration(BAR_STASH_DURATION).start(); 147 } 148 149 /** 150 * Called when launcher enters or exits the home page. Bubbles are unstashed on home. 151 */ setBubblesShowingOnHome(boolean onHome)152 public void setBubblesShowingOnHome(boolean onHome) { 153 if (mBubblesShowingOnHome != onHome) { 154 mBubblesShowingOnHome = onHome; 155 156 if (!mBarViewController.hasBubbles()) { 157 // if there are no bubbles, there's nothing to show, so just return. 158 return; 159 } 160 161 if (mBubblesShowingOnHome) { 162 showBubbleBar(/* expanded= */ false); 163 // When transitioning from app to home the stash animator may already have been 164 // created, so we need to animate the bubble bar here to align with hotseat. 165 if (!mIsStashed) { 166 mIconTranslationYForStash.animateToValue(getBubbleBarTranslationYForHotseat()) 167 .start(); 168 } 169 // If the bubble bar is already unstashed, the taskbar touchable region won't be 170 // updated correctly, so force an update here. 171 mControllers.runAfterInit(() -> 172 mTaskbarInsetsController.onTaskbarOrBubblebarWindowHeightOrInsetsChanged()); 173 } else if (!mBarViewController.isExpanded()) { 174 stashBubbleBar(); 175 } 176 } 177 } 178 179 /** Whether bubbles are showing on the launcher home page. */ isBubblesShowingOnHome()180 public boolean isBubblesShowingOnHome() { 181 return mBubblesShowingOnHome; 182 } 183 184 // TODO: when tapping on an app in overview, this is a bit delayed compared to taskbar stashing 185 /** Called when launcher enters or exits overview. Bubbles are unstashed on overview. */ setBubblesShowingOnOverview(boolean onOverview)186 public void setBubblesShowingOnOverview(boolean onOverview) { 187 if (mBubblesShowingOnOverview != onOverview) { 188 mBubblesShowingOnOverview = onOverview; 189 if (!mBubblesShowingOnOverview && !mBarViewController.isExpanded()) { 190 stashBubbleBar(); 191 } else { 192 // When transitioning to overview the stash animator may already have been 193 // created, so we need to animate the bubble bar here to align with taskbar. 194 mIconTranslationYForStash.animateToValue(getBubbleBarTranslationYForTaskbar()) 195 .start(); 196 } 197 } 198 } 199 200 /** Whether bubbles are showing on Overview. */ isBubblesShowingOnOverview()201 public boolean isBubblesShowingOnOverview() { 202 return mBubblesShowingOnOverview; 203 } 204 205 /** Called when sysui locked state changes, when locked, bubble bar is stashed. */ onSysuiLockedStateChange(boolean isSysuiLocked)206 public void onSysuiLockedStateChange(boolean isSysuiLocked) { 207 if (isSysuiLocked != mIsSysuiLocked) { 208 mIsSysuiLocked = isSysuiLocked; 209 if (!mIsSysuiLocked && mBarViewController.hasBubbles()) { 210 animateToInitialState(false /* expanding */); 211 } 212 } 213 } 214 215 /** 216 * Stashes the bubble bar if allowed based on other state (e.g. on home and overview the 217 * bar does not stash). 218 */ stashBubbleBar()219 public void stashBubbleBar() { 220 mRequestedStashState = true; 221 mRequestedExpandedState = false; 222 updateStashedAndExpandedState(); 223 } 224 225 /** 226 * Shows the bubble bar, and expands bubbles depending on {@param expandBubbles}. 227 */ showBubbleBar(boolean expandBubbles)228 public void showBubbleBar(boolean expandBubbles) { 229 mRequestedStashState = false; 230 mRequestedExpandedState = expandBubbles; 231 updateStashedAndExpandedState(); 232 } 233 updateStashedAndExpandedState()234 private void updateStashedAndExpandedState() { 235 if (mBarViewController.isHiddenForNoBubbles()) { 236 // If there are no bubbles the bar and handle are invisible, nothing to do here. 237 return; 238 } 239 boolean isStashed = mRequestedStashState 240 && !mBubblesShowingOnHome 241 && !mBubblesShowingOnOverview; 242 if (mIsStashed != isStashed) { 243 mIsStashed = isStashed; 244 if (mAnimator != null) { 245 mAnimator.cancel(); 246 } 247 mAnimator = createStashAnimator(mIsStashed, BAR_STASH_DURATION); 248 mAnimator.start(); 249 onIsStashedChanged(); 250 } 251 if (mBarViewController.isExpanded() != mRequestedExpandedState) { 252 mBarViewController.setExpanded(mRequestedExpandedState); 253 } 254 } 255 256 /** 257 * Create a stash animation. 258 * 259 * @param isStashed whether it's a stash animation or an unstash animation 260 * @param duration duration of the animation 261 * @return the animation 262 */ createStashAnimator(boolean isStashed, long duration)263 private AnimatorSet createStashAnimator(boolean isStashed, long duration) { 264 AnimatorSet animatorSet = new AnimatorSet(); 265 final float stashTranslation = (mUnstashedHeight - mStashedHeight) / 2f; 266 267 AnimatorSet fullLengthAnimatorSet = new AnimatorSet(); 268 // Not exactly half and may overlap. See [first|second]HalfDurationScale below. 269 AnimatorSet firstHalfAnimatorSet = new AnimatorSet(); 270 AnimatorSet secondHalfAnimatorSet = new AnimatorSet(); 271 272 final float firstHalfDurationScale; 273 final float secondHalfDurationScale; 274 275 if (isStashed) { 276 firstHalfDurationScale = 0.75f; 277 secondHalfDurationScale = 0.5f; 278 279 fullLengthAnimatorSet.play(mIconTranslationYForStash.animateToValue(stashTranslation)); 280 281 firstHalfAnimatorSet.playTogether( 282 mIconAlphaForStash.animateToValue(0), 283 mIconScaleForStash.animateToValue(STASHED_BAR_SCALE)); 284 secondHalfAnimatorSet.playTogether( 285 mBubbleStashedHandleAlpha.animateToValue(1)); 286 } else { 287 firstHalfDurationScale = 0.5f; 288 secondHalfDurationScale = 0.75f; 289 290 final float translationY = getBubbleBarTranslationY(); 291 292 fullLengthAnimatorSet.playTogether( 293 mIconScaleForStash.animateToValue(1), 294 mIconTranslationYForStash.animateToValue(translationY)); 295 296 firstHalfAnimatorSet.playTogether( 297 mBubbleStashedHandleAlpha.animateToValue(0) 298 ); 299 secondHalfAnimatorSet.playTogether( 300 mIconAlphaForStash.animateToValue(1) 301 ); 302 } 303 304 fullLengthAnimatorSet.play(mHandleViewController.createRevealAnimToIsStashed(isStashed)); 305 306 fullLengthAnimatorSet.setDuration(duration); 307 firstHalfAnimatorSet.setDuration((long) (duration * firstHalfDurationScale)); 308 secondHalfAnimatorSet.setDuration((long) (duration * secondHalfDurationScale)); 309 secondHalfAnimatorSet.setStartDelay((long) (duration * (1 - secondHalfDurationScale))); 310 311 animatorSet.playTogether(fullLengthAnimatorSet, firstHalfAnimatorSet, 312 secondHalfAnimatorSet); 313 animatorSet.addListener(new AnimatorListenerAdapter() { 314 @Override 315 public void onAnimationEnd(Animator animation) { 316 mAnimator = null; 317 mControllers.runAfterInit(() -> { 318 if (isStashed) { 319 mBarViewController.setExpanded(false); 320 } 321 mTaskbarInsetsController.onTaskbarOrBubblebarWindowHeightOrInsetsChanged(); 322 }); 323 } 324 }); 325 return animatorSet; 326 } 327 onIsStashedChanged()328 private void onIsStashedChanged() { 329 mControllers.runAfterInit(() -> { 330 mHandleViewController.onIsStashedChanged(); 331 mTaskbarInsetsController.onTaskbarOrBubblebarWindowHeightOrInsetsChanged(); 332 }); 333 } 334 getBubbleBarTranslationYForTaskbar()335 private float getBubbleBarTranslationYForTaskbar() { 336 return -mActivity.getDeviceProfile().taskbarBottomMargin; 337 } 338 getBubbleBarTranslationYForHotseat()339 private float getBubbleBarTranslationYForHotseat() { 340 final float hotseatBottomSpace = mActivity.getDeviceProfile().hotseatBarBottomSpacePx; 341 final float hotseatCellHeight = mActivity.getDeviceProfile().hotseatCellHeightPx; 342 return -hotseatBottomSpace - hotseatCellHeight + mUnstashedHeight - abs( 343 hotseatCellHeight - mUnstashedHeight) / 2; 344 } 345 getBubbleBarTranslationY()346 float getBubbleBarTranslationY() { 347 // If we're on home, adjust the translation so the bubble bar aligns with hotseat. 348 // Otherwise we're either showing in an app or in overview. In either case adjust it so 349 // the bubble bar aligns with the taskbar. 350 return mBubblesShowingOnHome ? getBubbleBarTranslationYForHotseat() 351 : getBubbleBarTranslationYForTaskbar(); 352 } 353 } 354