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 com.android.app.animation.Interpolators.EMPHASIZED_ACCELERATE; 19 import static com.android.launcher3.LauncherAnimUtils.VIEW_ALPHA; 20 import static com.android.launcher3.LauncherAnimUtils.VIEW_TRANSLATE_X; 21 22 import android.animation.Animator; 23 import android.animation.AnimatorListenerAdapter; 24 import android.animation.AnimatorSet; 25 import android.animation.ObjectAnimator; 26 import android.animation.ValueAnimator; 27 import android.annotation.NonNull; 28 import android.annotation.Nullable; 29 import android.annotation.SuppressLint; 30 import android.content.Context; 31 import android.graphics.PointF; 32 import android.graphics.Rect; 33 import android.util.AttributeSet; 34 import android.util.FloatProperty; 35 import android.util.LayoutDirection; 36 import android.util.Log; 37 import android.view.Gravity; 38 import android.view.MotionEvent; 39 import android.view.View; 40 import android.view.ViewGroup; 41 import android.widget.FrameLayout; 42 43 import androidx.dynamicanimation.animation.SpringForce; 44 45 import com.android.launcher3.R; 46 import com.android.launcher3.anim.SpringAnimationBuilder; 47 import com.android.launcher3.util.DisplayController; 48 import com.android.wm.shell.Flags; 49 import com.android.wm.shell.common.bubbles.BubbleBarLocation; 50 51 import java.util.List; 52 import java.util.function.Consumer; 53 54 /** 55 * The view that holds all the bubble views. Modifying this view should happen through 56 * {@link BubbleBarViewController}. Updates to the bubbles themselves (adds, removes, updates, 57 * selection) should happen through {@link BubbleBarController} which is the source of truth 58 * for state information about the bubbles. 59 * <p> 60 * The bubble bar has a couple of visual states: 61 * - stashed as a handle 62 * - unstashed but collapsed, in this state the bar is showing but the bubbles are stacked within it 63 * - unstashed and expanded, in this state the bar is showing and the bubbles are shown in a row 64 * with one of the bubbles being selected. Additionally, WMShell will display the expanded bubble 65 * view above the bar. 66 * <p> 67 * The bubble bar has some behavior related to taskbar: 68 * - When taskbar is unstashed, bubble bar will also become unstashed (but in its "collapsed" 69 * state) 70 * - When taskbar is stashed, bubble bar will also become stashed (unless bubble bar is in its 71 * "expanded" state) 72 * - When bubble bar is in its "expanded" state, taskbar becomes stashed 73 * <p> 74 * If there are no bubbles, the bubble bar and bubble stashed handle are not shown. Additionally 75 * the bubble bar and stashed handle are not shown on lockscreen. 76 * <p> 77 * When taskbar is in persistent or 3 button nav mode, the bubble bar is not available, and instead 78 * the bubbles are shown fully by WMShell in their floating mode. 79 */ 80 public class BubbleBarView extends FrameLayout { 81 82 private static final String TAG = "BubbleBarView"; 83 84 // TODO: (b/273594744) calculate the amount of space we have and base the max on that 85 // if it's smaller than 5. 86 private static final int MAX_BUBBLES = 5; 87 private static final int MAX_VISIBLE_BUBBLES_COLLAPSED = 2; 88 private static final int ARROW_POSITION_ANIMATION_DURATION_MS = 200; 89 private static final int WIDTH_ANIMATION_DURATION_MS = 200; 90 private static final int SCALE_ANIMATION_DURATION_MS = 200; 91 92 private static final long FADE_OUT_ANIM_ALPHA_DURATION_MS = 50L; 93 private static final long FADE_OUT_ANIM_ALPHA_DELAY_MS = 50L; 94 private static final long FADE_OUT_ANIM_POSITION_DURATION_MS = 100L; 95 // During fade out animation we shift the bubble bar 1/80th of the screen width 96 private static final float FADE_OUT_ANIM_POSITION_SHIFT = 1 / 80f; 97 98 private static final long FADE_IN_ANIM_ALPHA_DURATION_MS = 100L; 99 // Use STIFFNESS_MEDIUMLOW which is not defined in the API constants 100 private static final float FADE_IN_ANIM_POSITION_SPRING_STIFFNESS = 400f; 101 // During fade in animation we shift the bubble bar 1/60th of the screen width 102 private static final float FADE_IN_ANIM_POSITION_SHIFT = 1 / 60f; 103 104 private static final int SCALE_IN_ANIMATION_DURATION_MS = 250; 105 106 /** 107 * Custom property to set alpha value for the bar view while a bubble is being dragged. 108 * Skips applying alpha to the dragged bubble. 109 */ 110 private static final FloatProperty<BubbleBarView> BUBBLE_DRAG_ALPHA = 111 new FloatProperty<>("bubbleDragAlpha") { 112 @Override 113 public void setValue(BubbleBarView bubbleBarView, float alpha) { 114 bubbleBarView.setAlphaDuringBubbleDrag(alpha); 115 } 116 117 @Override 118 public Float get(BubbleBarView bubbleBarView) { 119 return bubbleBarView.mAlphaDuringDrag; 120 } 121 }; 122 123 private final BubbleBarBackground mBubbleBarBackground; 124 125 private boolean mIsAnimatingNewBubble = false; 126 127 /** 128 * The current bounds of all the bubble bar. Note that these bounds may not account for 129 * translation. The bounds should be retrieved using {@link #getBubbleBarBounds()} which 130 * updates the bounds and accounts for translation. 131 */ 132 private final Rect mBubbleBarBounds = new Rect(); 133 // The amount the bubbles overlap when they are stacked in the bubble bar 134 private final float mIconOverlapAmount; 135 // The spacing between the bubbles when bubble bar is expanded 136 private final float mExpandedBarIconsSpacing; 137 // The spacing between the bubbles and the borders of the bubble bar 138 private float mBubbleBarPadding; 139 // The size of a bubble in the bar 140 private float mIconSize; 141 // The scale of bubble icons 142 private float mIconScale = 1f; 143 // The elevation of the bubbles within the bar 144 private final float mBubbleElevation; 145 private final float mDragElevation; 146 private final int mPointerSize; 147 // Whether the bar is expanded (i.e. the bubble activity is being displayed). 148 private boolean mIsBarExpanded = false; 149 // The currently selected bubble view. 150 @Nullable 151 private BubbleView mSelectedBubbleView; 152 private BubbleBarLocation mBubbleBarLocation = BubbleBarLocation.DEFAULT; 153 // The click listener when the bubble bar is collapsed. 154 private View.OnClickListener mOnClickListener; 155 156 private final Rect mTempRect = new Rect(); 157 private float mRelativePivotX = 1f; 158 private float mRelativePivotY = 1f; 159 160 // An animator that represents the expansion state of the bubble bar, where 0 corresponds to the 161 // collapsed state and 1 to the fully expanded state. 162 private final ValueAnimator mWidthAnimator = ValueAnimator.ofFloat(0, 1); 163 164 /** An animator used for scaling in a new bubble to the bubble bar while expanded. */ 165 @Nullable 166 private ValueAnimator mNewBubbleScaleInAnimator = null; 167 @Nullable 168 private ValueAnimator mScalePaddingAnimator; 169 @Nullable 170 private Animator mBubbleBarLocationAnimator = null; 171 172 // We don't reorder the bubbles when they are expanded as it could be jarring for the user 173 // this runnable will be populated with any reordering of the bubbles that should be applied 174 // once they are collapsed. 175 @Nullable 176 private Runnable mReorderRunnable; 177 178 @Nullable 179 private Consumer<String> mUpdateSelectedBubbleAfterCollapse; 180 181 private boolean mDragging; 182 183 @Nullable 184 private BubbleView mDraggedBubbleView; 185 private float mAlphaDuringDrag = 1f; 186 187 private Controller mController; 188 189 private int mPreviousLayoutDirection = LayoutDirection.UNDEFINED; 190 BubbleBarView(Context context)191 public BubbleBarView(Context context) { 192 this(context, null); 193 } 194 BubbleBarView(Context context, AttributeSet attrs)195 public BubbleBarView(Context context, AttributeSet attrs) { 196 this(context, attrs, 0); 197 } 198 BubbleBarView(Context context, AttributeSet attrs, int defStyleAttr)199 public BubbleBarView(Context context, AttributeSet attrs, int defStyleAttr) { 200 this(context, attrs, defStyleAttr, 0); 201 } 202 BubbleBarView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes)203 public BubbleBarView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { 204 super(context, attrs, defStyleAttr, defStyleRes); 205 setAlpha(0); 206 setVisibility(INVISIBLE); 207 mIconOverlapAmount = getResources().getDimensionPixelSize(R.dimen.bubblebar_icon_overlap); 208 mBubbleBarPadding = getResources().getDimensionPixelSize(R.dimen.bubblebar_icon_spacing); 209 mIconSize = getResources().getDimensionPixelSize(R.dimen.bubblebar_icon_size); 210 mExpandedBarIconsSpacing = getResources().getDimensionPixelSize( 211 R.dimen.bubblebar_expanded_icon_spacing); 212 mBubbleElevation = getResources().getDimensionPixelSize(R.dimen.bubblebar_icon_elevation); 213 mDragElevation = getResources().getDimensionPixelSize(R.dimen.bubblebar_drag_elevation); 214 mPointerSize = getResources() 215 .getDimensionPixelSize(R.dimen.bubblebar_pointer_visible_size); 216 217 setClipToPadding(false); 218 219 mBubbleBarBackground = new BubbleBarBackground(context, getBubbleBarExpandedHeight()); 220 setBackgroundDrawable(mBubbleBarBackground); 221 222 mWidthAnimator.setDuration(WIDTH_ANIMATION_DURATION_MS); 223 224 addAnimationCallBacks(mWidthAnimator, 225 /* onStart= */ () -> mBubbleBarBackground.showArrow(true), 226 /* onEnd= */ () -> { 227 mBubbleBarBackground.showArrow(mIsBarExpanded); 228 if (!mIsBarExpanded && mReorderRunnable != null) { 229 mReorderRunnable.run(); 230 mReorderRunnable = null; 231 } 232 // If the bar was just collapsed and the overflow was the last bubble that was 233 // selected, set the first bubble as selected. 234 if (!mIsBarExpanded && mUpdateSelectedBubbleAfterCollapse != null 235 && mSelectedBubbleView != null 236 && mSelectedBubbleView.getBubble() instanceof BubbleBarOverflow) { 237 BubbleView firstBubble = (BubbleView) getChildAt(0); 238 mUpdateSelectedBubbleAfterCollapse.accept(firstBubble.getBubble().getKey()); 239 } 240 updateWidth(); 241 }, 242 /* onUpdate= */ animator -> { 243 updateBubblesLayoutProperties(mBubbleBarLocation); 244 invalidate(); 245 }); 246 } 247 248 249 /** 250 * Animates icon sizes and spacing between icons and bubble bar borders. 251 * 252 * @param newIconSize new icon size 253 * @param newBubbleBarPadding spacing between icons and bubble bar borders. 254 */ animateBubbleBarIconSize(float newIconSize, float newBubbleBarPadding)255 public void animateBubbleBarIconSize(float newIconSize, float newBubbleBarPadding) { 256 if (!isIconSizeOrPaddingUpdated(newIconSize, newBubbleBarPadding)) { 257 return; 258 } 259 if (!Flags.animateBubbleSizeChange()) { 260 setIconSizeAndPadding(newIconSize, newBubbleBarPadding); 261 } 262 if (mScalePaddingAnimator != null && mScalePaddingAnimator.isRunning()) { 263 mScalePaddingAnimator.cancel(); 264 } 265 ValueAnimator scalePaddingAnimator = ValueAnimator.ofFloat(0f, 1f); 266 scalePaddingAnimator.setDuration(SCALE_ANIMATION_DURATION_MS); 267 boolean isPaddingUpdated = isPaddingUpdated(newBubbleBarPadding); 268 boolean isIconSizeUpdated = isIconSizeUpdated(newIconSize); 269 float initialScale = mIconScale; 270 float initialPadding = mBubbleBarPadding; 271 float targetScale = newIconSize / getScaledIconSize(); 272 273 addAnimationCallBacks(scalePaddingAnimator, 274 /* onStart= */ null, 275 /* onEnd= */ () -> setIconSizeAndPadding(newIconSize, newBubbleBarPadding), 276 /* onUpdate= */ animator -> { 277 float transitionProgress = (float) animator.getAnimatedValue(); 278 if (isIconSizeUpdated) { 279 mIconScale = 280 initialScale + (targetScale - initialScale) * transitionProgress; 281 } 282 if (isPaddingUpdated) { 283 mBubbleBarPadding = initialPadding 284 + (newBubbleBarPadding - initialPadding) * transitionProgress; 285 } 286 updateBubblesLayoutProperties(mBubbleBarLocation); 287 invalidate(); 288 }); 289 scalePaddingAnimator.start(); 290 mScalePaddingAnimator = scalePaddingAnimator; 291 } 292 293 @Override setTranslationX(float translationX)294 public void setTranslationX(float translationX) { 295 super.setTranslationX(translationX); 296 if (mDraggedBubbleView != null) { 297 // Apply reverse of the translation as an offset to the dragged view. This ensures 298 // that the dragged bubble stays at the current location on the screen and its 299 // position is not affected by the parent translation. 300 mDraggedBubbleView.setOffsetX(-translationX); 301 } 302 } 303 304 /** 305 * Sets new icon sizes and newBubbleBarPadding between icons and bubble bar borders. 306 * 307 * @param newIconSize new icon size 308 * @param newBubbleBarPadding newBubbleBarPadding between icons and bubble bar borders. 309 */ setIconSizeAndPadding(float newIconSize, float newBubbleBarPadding)310 public void setIconSizeAndPadding(float newIconSize, float newBubbleBarPadding) { 311 // TODO(b/335457839): handle new bubble animation during the size change 312 if (!isIconSizeOrPaddingUpdated(newIconSize, newBubbleBarPadding)) { 313 return; 314 } 315 mIconScale = 1f; 316 mBubbleBarPadding = newBubbleBarPadding; 317 mIconSize = newIconSize; 318 int childCount = getChildCount(); 319 for (int i = 0; i < childCount; i++) { 320 View childView = getChildAt(i); 321 childView.setScaleY(mIconScale); 322 childView.setScaleY(mIconScale); 323 FrameLayout.LayoutParams params = (LayoutParams) childView.getLayoutParams(); 324 params.height = (int) mIconSize; 325 params.width = (int) mIconSize; 326 childView.setLayoutParams(params); 327 } 328 mBubbleBarBackground.setBackgroundHeight(getBubbleBarHeight()); 329 updateLayoutParams(); 330 } 331 getScaledIconSize()332 private float getScaledIconSize() { 333 return mIconSize * mIconScale; 334 } 335 336 @Override onLayout(boolean changed, int left, int top, int right, int bottom)337 protected void onLayout(boolean changed, int left, int top, int right, int bottom) { 338 super.onLayout(changed, left, top, right, bottom); 339 mBubbleBarBounds.left = left; 340 mBubbleBarBounds.top = top + mPointerSize; 341 mBubbleBarBounds.right = right; 342 mBubbleBarBounds.bottom = bottom; 343 344 // The bubble bar handle is aligned according to the relative pivot, 345 // by default it's aligned to the bottom edge of the screen so scale towards that 346 setPivotX(mRelativePivotX * getWidth()); 347 setPivotY(mRelativePivotY * getHeight()); 348 349 if (!mDragging) { 350 // Position the views when not dragging 351 updateBubblesLayoutProperties(mBubbleBarLocation); 352 } 353 } 354 355 @Override onRtlPropertiesChanged(int layoutDirection)356 public void onRtlPropertiesChanged(int layoutDirection) { 357 if (mBubbleBarLocation == BubbleBarLocation.DEFAULT 358 && mPreviousLayoutDirection != layoutDirection) { 359 Log.d(TAG, "BubbleBar RTL properties changed, new layoutDirection=" + layoutDirection 360 + " previous layoutDirection=" + mPreviousLayoutDirection); 361 mPreviousLayoutDirection = layoutDirection; 362 onBubbleBarLocationChanged(); 363 } 364 } 365 366 @SuppressLint("RtlHardcoded") onBubbleBarLocationChanged()367 private void onBubbleBarLocationChanged() { 368 final boolean onLeft = mBubbleBarLocation.isOnLeft(isLayoutRtl()); 369 mBubbleBarBackground.setAnchorLeft(onLeft); 370 mRelativePivotX = onLeft ? 0f : 1f; 371 LayoutParams lp = (LayoutParams) getLayoutParams(); 372 lp.gravity = Gravity.BOTTOM | (onLeft ? Gravity.LEFT : Gravity.RIGHT); 373 setLayoutParams(lp); // triggers a relayout 374 } 375 376 /** 377 * @return current {@link BubbleBarLocation} 378 */ getBubbleBarLocation()379 public BubbleBarLocation getBubbleBarLocation() { 380 return mBubbleBarLocation; 381 } 382 383 /** 384 * Update {@link BubbleBarLocation} 385 */ setBubbleBarLocation(BubbleBarLocation bubbleBarLocation)386 public void setBubbleBarLocation(BubbleBarLocation bubbleBarLocation) { 387 resetDragAnimation(); 388 if (bubbleBarLocation != mBubbleBarLocation) { 389 mBubbleBarLocation = bubbleBarLocation; 390 onBubbleBarLocationChanged(); 391 } 392 } 393 394 /** 395 * Set whether this view is currently being dragged 396 */ setIsDragging(boolean dragging)397 public void setIsDragging(boolean dragging) { 398 if (mDragging == dragging) { 399 return; 400 } 401 mDragging = dragging; 402 setElevation(dragging ? mDragElevation : mBubbleElevation); 403 if (!mDragging) { 404 // Relayout after dragging to ensure that the dragged bubble is positioned correctly 405 requestLayout(); 406 } 407 } 408 409 /** 410 * Get translation for bubble bar when drag is released and it needs to animate back to the 411 * resting position. 412 * Resting position is based on the supplied location. If the supplied location is different 413 * from the internal location that was used during bubble bar layout, translation values are 414 * calculated to position the bar at the desired location. 415 * 416 * @param initialTranslation initial bubble bar translation at the start of drag 417 * @param location desired location of the bubble bar when drag is released 418 * @return point with x and y values representing translation on x and y-axis 419 */ getBubbleBarDragReleaseTranslation(PointF initialTranslation, BubbleBarLocation location)420 public PointF getBubbleBarDragReleaseTranslation(PointF initialTranslation, 421 BubbleBarLocation location) { 422 float dragEndTranslationX = initialTranslation.x; 423 if (getBubbleBarLocation().isOnLeft(isLayoutRtl()) != location.isOnLeft(isLayoutRtl())) { 424 // Bubble bar is laid out on left or right side of the screen. And the desired new 425 // location is on the other side. Calculate x translation value required to shift 426 // bubble bar from one side to the other. 427 final float shift = getDistanceFromOtherSide(); 428 if (location.isOnLeft(isLayoutRtl())) { 429 // New location is on the left, shift left 430 // before -> |......ooo.| after -> |.ooo......| 431 dragEndTranslationX = -shift; 432 } else { 433 // New location is on the right, shift right 434 // before -> |.ooo......| after -> |......ooo.| 435 dragEndTranslationX = shift; 436 } 437 } 438 return new PointF(dragEndTranslationX, mController.getBubbleBarTranslationY()); 439 } 440 441 /** 442 * Get translation for a bubble when drag is released and it needs to animate back to the 443 * resting position. 444 * Resting position is based on the supplied location. If the supplied location is different 445 * from the internal location that was used during bubble bar layout, translation values are 446 * calculated to position the bar at the desired location. 447 * 448 * @param initialTranslation initial bubble translation inside the bar at the start of drag 449 * @param location desired location of the bubble bar when drag is released 450 * @return point with x and y values representing translation on x and y-axis 451 */ getDraggedBubbleReleaseTranslation(PointF initialTranslation, BubbleBarLocation location)452 public PointF getDraggedBubbleReleaseTranslation(PointF initialTranslation, 453 BubbleBarLocation location) { 454 float dragEndTranslationX = initialTranslation.x; 455 boolean newLocationOnLeft = location.isOnLeft(isLayoutRtl()); 456 if (getBubbleBarLocation().isOnLeft(isLayoutRtl()) != newLocationOnLeft) { 457 // Calculate translationX based on bar and bubble translations 458 float bubbleBarTx = getBubbleBarDragReleaseTranslation(initialTranslation, location).x; 459 float bubbleTx = 460 getExpandedBubbleTranslationX( 461 indexOfChild(mDraggedBubbleView), getChildCount(), newLocationOnLeft); 462 dragEndTranslationX = bubbleBarTx + bubbleTx; 463 } 464 // translationY does not change during drag and can be reused 465 return new PointF(dragEndTranslationX, initialTranslation.y); 466 } 467 getDistanceFromOtherSide()468 private float getDistanceFromOtherSide() { 469 // Calculate the shift needed to position the bubble bar on the other side 470 int displayWidth = getResources().getDisplayMetrics().widthPixels; 471 int margin = 0; 472 if (getLayoutParams() instanceof MarginLayoutParams lp) { 473 margin += lp.leftMargin; 474 margin += lp.rightMargin; 475 } 476 return (float) (displayWidth - getWidth() - margin); 477 } 478 479 /** 480 * Animate bubble bar to the given location transiently. Does not modify the layout or the value 481 * returned by {@link #getBubbleBarLocation()}. 482 */ animateToBubbleBarLocation(BubbleBarLocation bubbleBarLocation)483 public void animateToBubbleBarLocation(BubbleBarLocation bubbleBarLocation) { 484 if (mBubbleBarLocationAnimator != null && mBubbleBarLocationAnimator.isRunning()) { 485 mBubbleBarLocationAnimator.removeAllListeners(); 486 mBubbleBarLocationAnimator.cancel(); 487 } 488 489 // Location animation uses two separate animators. 490 // First animator hides the bar. 491 // After it completes, bubble positions in the bar and arrow position is updated. 492 // Second animator is started to show the bar. 493 mBubbleBarLocationAnimator = getLocationUpdateFadeOutAnimator(bubbleBarLocation); 494 mBubbleBarLocationAnimator.addListener(new AnimatorListenerAdapter() { 495 @Override 496 public void onAnimationEnd(Animator animation) { 497 updateBubblesLayoutProperties(bubbleBarLocation); 498 mBubbleBarBackground.setAnchorLeft(bubbleBarLocation.isOnLeft(isLayoutRtl())); 499 500 // Animate it in 501 mBubbleBarLocationAnimator = getLocationUpdateFadeInAnimator(bubbleBarLocation); 502 mBubbleBarLocationAnimator.start(); 503 } 504 }); 505 mBubbleBarLocationAnimator.start(); 506 } 507 getLocationUpdateFadeOutAnimator(BubbleBarLocation newLocation)508 private Animator getLocationUpdateFadeOutAnimator(BubbleBarLocation newLocation) { 509 final float shift = 510 getResources().getDisplayMetrics().widthPixels * FADE_OUT_ANIM_POSITION_SHIFT; 511 final boolean onLeft = newLocation.isOnLeft(isLayoutRtl()); 512 final float tx = getTranslationX() + (onLeft ? -shift : shift); 513 514 ObjectAnimator positionAnim = ObjectAnimator.ofFloat(this, VIEW_TRANSLATE_X, tx) 515 .setDuration(FADE_OUT_ANIM_POSITION_DURATION_MS); 516 positionAnim.setInterpolator(EMPHASIZED_ACCELERATE); 517 518 ObjectAnimator alphaAnim = ObjectAnimator.ofFloat(this, getLocationAnimAlphaProperty(), 0f) 519 .setDuration(FADE_OUT_ANIM_ALPHA_DURATION_MS); 520 alphaAnim.setStartDelay(FADE_OUT_ANIM_ALPHA_DELAY_MS); 521 522 AnimatorSet animatorSet = new AnimatorSet(); 523 animatorSet.playTogether(positionAnim, alphaAnim); 524 return animatorSet; 525 } 526 getLocationUpdateFadeInAnimator(BubbleBarLocation newLocation)527 private Animator getLocationUpdateFadeInAnimator(BubbleBarLocation newLocation) { 528 final float shift = 529 getResources().getDisplayMetrics().widthPixels * FADE_IN_ANIM_POSITION_SHIFT; 530 531 final boolean onLeft = newLocation.isOnLeft(isLayoutRtl()); 532 final float startTx; 533 final float finalTx; 534 if (newLocation == mBubbleBarLocation) { 535 // Animated location matches layout location. 536 finalTx = 0; 537 } else { 538 // We are animating in to a transient location, need to move the bar accordingly. 539 finalTx = getDistanceFromOtherSide() * (onLeft ? -1 : 1); 540 } 541 if (onLeft) { 542 // Bar will be shown on the left side. Start point is shifted right. 543 startTx = finalTx + shift; 544 } else { 545 // Bar will be shown on the right side. Start point is shifted left. 546 startTx = finalTx - shift; 547 } 548 549 ValueAnimator positionAnim = new SpringAnimationBuilder(getContext()) 550 .setStartValue(startTx) 551 .setEndValue(finalTx) 552 .setDampingRatio(SpringForce.DAMPING_RATIO_LOW_BOUNCY) 553 .setStiffness(FADE_IN_ANIM_POSITION_SPRING_STIFFNESS) 554 .build(this, VIEW_TRANSLATE_X); 555 556 ObjectAnimator alphaAnim = ObjectAnimator.ofFloat(this, getLocationAnimAlphaProperty(), 1f) 557 .setDuration(FADE_IN_ANIM_ALPHA_DURATION_MS); 558 559 AnimatorSet animatorSet = new AnimatorSet(); 560 animatorSet.playTogether(positionAnim, alphaAnim); 561 return animatorSet; 562 } 563 564 /** 565 * Get property that can be used to animate the alpha value for the bar. 566 * When a bubble is being dragged, uses {@link #BUBBLE_DRAG_ALPHA}. 567 * Falls back to {@link com.android.launcher3.LauncherAnimUtils#VIEW_ALPHA} otherwise. 568 */ getLocationAnimAlphaProperty()569 private FloatProperty<? super BubbleBarView> getLocationAnimAlphaProperty() { 570 return mDraggedBubbleView == null ? VIEW_ALPHA : BUBBLE_DRAG_ALPHA; 571 } 572 573 /** 574 * Set alpha value for the bar while a bubble is being dragged. 575 * We can not update the alpha on the bar directly because the dragged bubble would be affected 576 * as well. As it is a child view. 577 * Instead, while a bubble is being dragged, set alpha on each child view, that is not the 578 * dragged view. And set an alpha on the background. 579 * This allows for the dragged bubble to remain visible while the bar is hidden during 580 * animation. 581 */ setAlphaDuringBubbleDrag(float alpha)582 private void setAlphaDuringBubbleDrag(float alpha) { 583 mAlphaDuringDrag = alpha; 584 final int childCount = getChildCount(); 585 for (int i = 0; i < childCount; i++) { 586 View view = getChildAt(i); 587 if (view != mDraggedBubbleView) { 588 view.setAlpha(alpha); 589 } 590 } 591 if (mBubbleBarBackground != null) { 592 mBubbleBarBackground.setAlpha((int) (255 * alpha)); 593 } 594 } 595 resetDragAnimation()596 private void resetDragAnimation() { 597 if (mBubbleBarLocationAnimator != null) { 598 mBubbleBarLocationAnimator.removeAllListeners(); 599 mBubbleBarLocationAnimator.cancel(); 600 mBubbleBarLocationAnimator = null; 601 } 602 setAlphaDuringBubbleDrag(1f); 603 setTranslationX(0f); 604 setAlpha(1f); 605 } 606 607 /** 608 * Get bubble bar top coordinate on screen when bar is resting 609 */ getRestingTopPositionOnScreen()610 public int getRestingTopPositionOnScreen() { 611 int displayHeight = DisplayController.INSTANCE.get(getContext()).getInfo().currentSize.y; 612 int bubbleBarHeight = getBubbleBarBounds().height(); 613 return displayHeight - bubbleBarHeight + (int) mController.getBubbleBarTranslationY(); 614 } 615 616 /** 617 * Updates the bounds with translation that may have been applied and returns the result. 618 */ getBubbleBarBounds()619 public Rect getBubbleBarBounds() { 620 mBubbleBarBounds.top = getTop() + (int) getTranslationY() + mPointerSize; 621 mBubbleBarBounds.bottom = getBottom() + (int) getTranslationY(); 622 return mBubbleBarBounds; 623 } 624 625 /** 626 * Set bubble bar relative pivot value for X and Y, applied as a fraction of view width/height 627 * respectively. If the value is not in range of 0 to 1 it will be normalized. 628 * @param x relative X pivot value in range 0..1 629 * @param y relative Y pivot value in range 0..1 630 */ setRelativePivot(float x, float y)631 public void setRelativePivot(float x, float y) { 632 mRelativePivotX = Float.max(Float.min(x, 1), 0); 633 mRelativePivotY = Float.max(Float.min(y, 1), 0); 634 requestLayout(); 635 } 636 637 /** Like {@link #setRelativePivot(float, float)} but only updates pivot y. */ setRelativePivotY(float y)638 public void setRelativePivotY(float y) { 639 setRelativePivot(mRelativePivotX, y); 640 } 641 642 /** 643 * Get current relative pivot for X axis 644 */ getRelativePivotX()645 public float getRelativePivotX() { 646 return mRelativePivotX; 647 } 648 649 /** 650 * Get current relative pivot for Y axis 651 */ getRelativePivotY()652 public float getRelativePivotY() { 653 return mRelativePivotY; 654 } 655 656 /** Notifies the bubble bar that a new bubble animation is starting. */ onAnimatingBubbleStarted()657 public void onAnimatingBubbleStarted() { 658 mIsAnimatingNewBubble = true; 659 } 660 661 /** Notifies the bubble bar that a new bubble animation is complete. */ onAnimatingBubbleCompleted()662 public void onAnimatingBubbleCompleted() { 663 mIsAnimatingNewBubble = false; 664 } 665 666 /** Add a new bubble to the bubble bar. */ addBubble(View bubble, FrameLayout.LayoutParams lp)667 public void addBubble(View bubble, FrameLayout.LayoutParams lp) { 668 if (isExpanded()) { 669 // if we're expanded scale the new bubble in 670 bubble.setScaleX(0f); 671 bubble.setScaleY(0f); 672 addView(bubble, 0, lp); 673 createNewBubbleScaleInAnimator(bubble); 674 mNewBubbleScaleInAnimator.start(); 675 } else { 676 addView(bubble, 0, lp); 677 } 678 } 679 createNewBubbleScaleInAnimator(View bubble)680 private void createNewBubbleScaleInAnimator(View bubble) { 681 mNewBubbleScaleInAnimator = ValueAnimator.ofFloat(0, 1); 682 mNewBubbleScaleInAnimator.setDuration(SCALE_IN_ANIMATION_DURATION_MS); 683 mNewBubbleScaleInAnimator.addUpdateListener(animation -> { 684 float animatedFraction = animation.getAnimatedFraction(); 685 bubble.setScaleX(animatedFraction); 686 bubble.setScaleY(animatedFraction); 687 updateBubblesLayoutProperties(mBubbleBarLocation); 688 invalidate(); 689 }); 690 mNewBubbleScaleInAnimator.addListener(new AnimatorListenerAdapter() { 691 @Override 692 public void onAnimationCancel(Animator animation) { 693 bubble.setScaleX(1); 694 bubble.setScaleY(1); 695 } 696 697 @Override 698 public void onAnimationEnd(Animator animation) { 699 updateWidth(); 700 mNewBubbleScaleInAnimator = null; 701 } 702 }); 703 } 704 705 // TODO: (b/280605790) animate it 706 @Override addView(View child, int index, ViewGroup.LayoutParams params)707 public void addView(View child, int index, ViewGroup.LayoutParams params) { 708 if (getChildCount() + 1 > MAX_BUBBLES) { 709 // the last child view is the overflow bubble and we shouldn't remove that. remove the 710 // second to last child view. 711 removeViewInLayout(getChildAt(getChildCount() - 2)); 712 } 713 super.addView(child, index, params); 714 updateWidth(); 715 updateBubbleAccessibilityStates(); 716 updateContentDescription(); 717 } 718 719 // TODO: (b/283309949) animate it 720 @Override removeView(View view)721 public void removeView(View view) { 722 super.removeView(view); 723 if (view == mSelectedBubbleView) { 724 mSelectedBubbleView = null; 725 mBubbleBarBackground.showArrow(false); 726 } 727 updateWidth(); 728 updateBubbleAccessibilityStates(); 729 updateContentDescription(); 730 } 731 updateWidth()732 private void updateWidth() { 733 LayoutParams lp = (FrameLayout.LayoutParams) getLayoutParams(); 734 lp.width = (int) (mIsBarExpanded ? expandedWidth() : collapsedWidth()); 735 setLayoutParams(lp); 736 } 737 updateLayoutParams()738 private void updateLayoutParams() { 739 LayoutParams lp = (FrameLayout.LayoutParams) getLayoutParams(); 740 lp.height = (int) getBubbleBarExpandedHeight(); 741 lp.width = (int) (mIsBarExpanded ? expandedWidth() : collapsedWidth()); 742 setLayoutParams(lp); 743 } 744 getBubbleBarHeight()745 private float getBubbleBarHeight() { 746 return mIsBarExpanded ? getBubbleBarExpandedHeight() 747 : getBubbleBarCollapsedHeight(); 748 } 749 750 /** @return the horizontal margin between the bubble bar and the edge of the screen. */ getHorizontalMargin()751 int getHorizontalMargin() { 752 LayoutParams lp = (FrameLayout.LayoutParams) getLayoutParams(); 753 return lp.getMarginEnd(); 754 } 755 756 /** 757 * Updates the z order, positions, and badge visibility of the bubble views in the bar based 758 * on the expanded state. 759 */ updateBubblesLayoutProperties(BubbleBarLocation bubbleBarLocation)760 private void updateBubblesLayoutProperties(BubbleBarLocation bubbleBarLocation) { 761 final float widthState = (float) mWidthAnimator.getAnimatedValue(); 762 final float currentWidth = getWidth(); 763 final float expandedWidth = expandedWidth(); 764 final float collapsedWidth = collapsedWidth(); 765 int bubbleCount = getChildCount(); 766 float viewBottom = mBubbleBarBounds.height() + (isExpanded() ? mPointerSize : 0); 767 float bubbleBarAnimatedTop = viewBottom - getBubbleBarHeight(); 768 // When translating X & Y the scale is ignored, so need to deduct it from the translations 769 final float ty = bubbleBarAnimatedTop + mBubbleBarPadding - getScaleIconShift(); 770 final boolean animate = getVisibility() == VISIBLE; 771 final boolean onLeft = bubbleBarLocation.isOnLeft(isLayoutRtl()); 772 // elevation state is opposite to widthState - when expanded all icons are flat 773 float elevationState = (1 - widthState); 774 for (int i = 0; i < bubbleCount; i++) { 775 BubbleView bv = (BubbleView) getChildAt(i); 776 if (bv == mDraggedBubbleView) { 777 // Skip the dragged bubble. Its translation is managed by the drag controller. 778 continue; 779 } 780 // Clear out drag translation and offset 781 bv.setDragTranslationX(0f); 782 bv.setOffsetX(0f); 783 784 bv.setScaleX(mIconScale); 785 bv.setScaleY(mIconScale); 786 bv.setTranslationY(ty); 787 // the position of the bubble when the bar is fully expanded 788 final float expandedX = getExpandedBubbleTranslationX(i, bubbleCount, onLeft); 789 // the position of the bubble when the bar is fully collapsed 790 final float collapsedX = getCollapsedBubbleTranslationX(i, bubbleCount, onLeft); 791 792 // slowly animate elevation while keeping correct Z ordering 793 float fullElevationForChild = (MAX_BUBBLES * mBubbleElevation) - i; 794 bv.setZ(fullElevationForChild * elevationState); 795 796 if (mIsBarExpanded) { 797 // If bar is on the right, account for bubble bar expanding and shifting left 798 final float expandedBarShift = onLeft ? 0 : currentWidth - expandedWidth; 799 // where the bubble will end up when the animation ends 800 final float targetX = expandedX + expandedBarShift; 801 bv.setTranslationX(widthState * (targetX - collapsedX) + collapsedX); 802 // When we're expanded, we're not stacked so we're not behind the stack 803 bv.setBehindStack(false, animate); 804 bv.setAlpha(1); 805 } else { 806 // If bar is on the right, account for bubble bar expanding and shifting left 807 final float collapsedBarShift = onLeft ? 0 : currentWidth - collapsedWidth; 808 final float targetX = collapsedX + collapsedBarShift; 809 bv.setTranslationX(widthState * (expandedX - targetX) + targetX); 810 // If we're not the first bubble we're behind the stack 811 bv.setBehindStack(i > 0, animate); 812 // If we're fully collapsed, hide all bubbles except for the first 2. If there are 813 // only 2 bubbles, hide the second bubble as well because it's the overflow. 814 if (widthState == 0) { 815 if (i > MAX_VISIBLE_BUBBLES_COLLAPSED - 1) { 816 bv.setAlpha(0); 817 } else if (i == MAX_VISIBLE_BUBBLES_COLLAPSED - 1 818 && bubbleCount == MAX_VISIBLE_BUBBLES_COLLAPSED) { 819 bv.setAlpha(0); 820 } else { 821 bv.setAlpha(1); 822 } 823 } 824 } 825 } 826 827 // update the arrow position 828 final float collapsedArrowPosition = arrowPositionForSelectedWhenCollapsed( 829 bubbleBarLocation); 830 final float expandedArrowPosition = arrowPositionForSelectedWhenExpanded(bubbleBarLocation); 831 final float interpolatedWidth = 832 widthState * (expandedWidth - collapsedWidth) + collapsedWidth; 833 final float arrowPosition; 834 835 float interpolatedShift = (expandedArrowPosition - collapsedArrowPosition) * widthState; 836 if (onLeft) { 837 arrowPosition = collapsedArrowPosition + interpolatedShift; 838 } else { 839 if (mIsBarExpanded) { 840 arrowPosition = currentWidth - interpolatedWidth + collapsedArrowPosition 841 + interpolatedShift; 842 } else { 843 final float targetPosition = currentWidth - collapsedWidth + collapsedArrowPosition; 844 arrowPosition = 845 targetPosition + widthState * (expandedArrowPosition - targetPosition); 846 } 847 } 848 mBubbleBarBackground.setArrowPosition(arrowPosition); 849 mBubbleBarBackground.setArrowHeightFraction(widthState); 850 mBubbleBarBackground.setWidth(interpolatedWidth); 851 mBubbleBarBackground.setBackgroundHeight(getBubbleBarExpandedHeight()); 852 } 853 getScaleIconShift()854 private float getScaleIconShift() { 855 return (mIconSize - getScaledIconSize()) / 2; 856 } 857 getExpandedBubbleTranslationX(int bubbleIndex, int bubbleCount, boolean onLeft)858 private float getExpandedBubbleTranslationX(int bubbleIndex, int bubbleCount, boolean onLeft) { 859 if (bubbleIndex < 0 || bubbleIndex >= bubbleCount) { 860 return 0; 861 } 862 final float iconAndSpacing = getScaledIconSize() + mExpandedBarIconsSpacing; 863 float translationX; 864 if (mNewBubbleScaleInAnimator != null && mNewBubbleScaleInAnimator.isRunning()) { 865 translationX = getExpandedBubbleTranslationXDuringScaleAnimation( 866 bubbleIndex, bubbleCount, onLeft); 867 } else if (onLeft) { 868 translationX = mBubbleBarPadding + (bubbleCount - bubbleIndex - 1) * iconAndSpacing; 869 } else { 870 translationX = mBubbleBarPadding + bubbleIndex * iconAndSpacing; 871 } 872 return translationX - getScaleIconShift(); 873 } 874 875 /** 876 * Returns the translation X for the bubble at index {@code bubbleIndex} when the bubble bar is 877 * expanded <b>and</b> a new bubble is animating in. 878 * 879 * <p>This method assumes that the animation is running so callers are expected to verify that 880 * before calling it. 881 */ getExpandedBubbleTranslationXDuringScaleAnimation( int bubbleIndex, int bubbleCount, boolean onLeft)882 private float getExpandedBubbleTranslationXDuringScaleAnimation( 883 int bubbleIndex, int bubbleCount, boolean onLeft) { 884 // when the new bubble scale animation is running, a new bubble is animating in while the 885 // bubble bar is expanded, so we have at least 2 bubbles in the bubble bar - the expanded 886 // one, and the new one animating in. 887 888 if (mNewBubbleScaleInAnimator == null) { 889 // callers of this method are expected to verify that the animation is running, but the 890 // compiler doesn't know that. 891 return 0; 892 } 893 final float iconAndSpacing = getScaledIconSize() + mExpandedBarIconsSpacing; 894 final float newBubbleScale = mNewBubbleScaleInAnimator.getAnimatedFraction(); 895 // the new bubble is scaling in from the center, so we need to adjust its translation so 896 // that the distance to the adjacent bubble scales at the same rate. 897 final float pivotAdjustment = -(1 - newBubbleScale) * getScaledIconSize() / 2f; 898 899 if (onLeft) { 900 if (bubbleIndex == 0) { 901 // this is the animating bubble. use scaled spacing between it and the bubble to 902 // its left 903 return (bubbleCount - 1) * getScaledIconSize() 904 + (bubbleCount - 2) * mExpandedBarIconsSpacing 905 + newBubbleScale * mExpandedBarIconsSpacing 906 + pivotAdjustment; 907 } 908 // when the bubble bar is on the left, only the translation of the right-most bubble 909 // is affected by the scale animation. 910 return (bubbleCount - bubbleIndex - 1) * iconAndSpacing; 911 } else if (bubbleIndex == 0) { 912 // the bubble bar is on the right, and this is the animating bubble. it only needs 913 // to be adjusted for the scaling pivot. 914 return pivotAdjustment; 915 } else { 916 return iconAndSpacing * (bubbleIndex - 1 + newBubbleScale); 917 } 918 } 919 getCollapsedBubbleTranslationX(int bubbleIndex, int bubbleCount, boolean onLeft)920 private float getCollapsedBubbleTranslationX(int bubbleIndex, int bubbleCount, 921 boolean onLeft) { 922 if (bubbleIndex < 0 || bubbleIndex >= bubbleCount) { 923 return 0; 924 } 925 float translationX; 926 if (onLeft) { 927 // Shift the first bubble only if there are more bubbles in addition to overflow 928 translationX = mBubbleBarPadding + ( 929 bubbleIndex == 0 && bubbleCount > MAX_VISIBLE_BUBBLES_COLLAPSED 930 ? mIconOverlapAmount : 0); 931 } else { 932 translationX = mBubbleBarPadding + (bubbleIndex == 0 ? 0 : mIconOverlapAmount); 933 } 934 return translationX - getScaleIconShift(); 935 } 936 937 /** 938 * Reorders the views to match the provided list. 939 */ reorder(List<BubbleView> viewOrder)940 public void reorder(List<BubbleView> viewOrder) { 941 if (isExpanded() || mWidthAnimator.isRunning()) { 942 mReorderRunnable = () -> doReorder(viewOrder); 943 } else { 944 doReorder(viewOrder); 945 } 946 } 947 948 // TODO: (b/273592694) animate it doReorder(List<BubbleView> viewOrder)949 private void doReorder(List<BubbleView> viewOrder) { 950 if (!isExpanded()) { 951 for (int i = 0; i < viewOrder.size(); i++) { 952 View child = viewOrder.get(i); 953 // this child view may have already been removed so verify that it still exists 954 // before reordering it, otherwise it will be re-added. 955 int indexOfChild = indexOfChild(child); 956 if (child != null && indexOfChild >= 0) { 957 removeViewInLayout(child); 958 addViewInLayout(child, i, child.getLayoutParams()); 959 } 960 } 961 updateBubblesLayoutProperties(mBubbleBarLocation); 962 updateContentDescription(); 963 } 964 } 965 setUpdateSelectedBubbleAfterCollapse( Consumer<String> updateSelectedBubbleAfterCollapse)966 public void setUpdateSelectedBubbleAfterCollapse( 967 Consumer<String> updateSelectedBubbleAfterCollapse) { 968 mUpdateSelectedBubbleAfterCollapse = updateSelectedBubbleAfterCollapse; 969 } 970 setController(Controller controller)971 void setController(Controller controller) { 972 mController = controller; 973 } 974 975 /** 976 * Sets which bubble view should be shown as selected. 977 */ setSelectedBubble(BubbleView view)978 public void setSelectedBubble(BubbleView view) { 979 BubbleView previouslySelectedBubble = mSelectedBubbleView; 980 mSelectedBubbleView = view; 981 mBubbleBarBackground.showArrow(view != null); 982 // TODO: (b/283309949) remove animation should be implemented first, so than arrow 983 // animation is adjusted, skip animation for now 984 updateArrowForSelected(previouslySelectedBubble != null); 985 } 986 987 /** 988 * Sets the dragged bubble view to correctly apply Z order. Dragged view should appear on top 989 */ setDraggedBubble(@ullable BubbleView view)990 public void setDraggedBubble(@Nullable BubbleView view) { 991 if (mDraggedBubbleView != null) { 992 mDraggedBubbleView.setZ(0); 993 } 994 mDraggedBubbleView = view; 995 if (view != null) { 996 view.setZ(mDragElevation); 997 } 998 setIsDragging(view != null); 999 } 1000 1001 /** 1002 * Update the arrow position to match the selected bubble. 1003 * 1004 * @param shouldAnimate whether or not to animate the arrow. If the bar was just expanded, this 1005 * should be set to {@code false}. Otherwise set this to {@code true}. 1006 */ updateArrowForSelected(boolean shouldAnimate)1007 private void updateArrowForSelected(boolean shouldAnimate) { 1008 if (mSelectedBubbleView == null) { 1009 Log.w(TAG, "trying to update selection arrow without a selected view!"); 1010 return; 1011 } 1012 // Find the center of the bubble when it's expanded, set the arrow position to it. 1013 final float tx = arrowPositionForSelectedWhenExpanded(mBubbleBarLocation); 1014 final float currentArrowPosition = mBubbleBarBackground.getArrowPositionX(); 1015 if (tx == currentArrowPosition) { 1016 // arrow position remains unchanged 1017 return; 1018 } 1019 if (shouldAnimate && currentArrowPosition > expandedWidth()) { 1020 Log.d(TAG, "arrow out of bounds of expanded view, skip animation"); 1021 shouldAnimate = false; 1022 } 1023 if (shouldAnimate) { 1024 ValueAnimator animator = ValueAnimator.ofFloat(currentArrowPosition, tx); 1025 animator.setDuration(ARROW_POSITION_ANIMATION_DURATION_MS); 1026 animator.addUpdateListener(animation -> { 1027 float x = (float) animation.getAnimatedValue(); 1028 mBubbleBarBackground.setArrowPosition(x); 1029 invalidate(); 1030 }); 1031 animator.start(); 1032 } else { 1033 mBubbleBarBackground.setArrowPosition(tx); 1034 invalidate(); 1035 } 1036 } 1037 arrowPositionForSelectedWhenExpanded(BubbleBarLocation bubbleBarLocation)1038 private float arrowPositionForSelectedWhenExpanded(BubbleBarLocation bubbleBarLocation) { 1039 final int index = indexOfChild(mSelectedBubbleView); 1040 final float selectedBubbleTranslationX = getExpandedBubbleTranslationX( 1041 index, getChildCount(), bubbleBarLocation.isOnLeft(isLayoutRtl())); 1042 return selectedBubbleTranslationX + mIconSize / 2f; 1043 } 1044 arrowPositionForSelectedWhenCollapsed(BubbleBarLocation bubbleBarLocation)1045 private float arrowPositionForSelectedWhenCollapsed(BubbleBarLocation bubbleBarLocation) { 1046 final int index = indexOfChild(mSelectedBubbleView); 1047 final int bubblePosition; 1048 if (bubbleBarLocation.isOnLeft(isLayoutRtl())) { 1049 // Bubble positions are reversed. First bubble may be shifted, if there are more 1050 // bubbles than the current bubble and overflow. 1051 bubblePosition = index == 0 && getChildCount() > MAX_VISIBLE_BUBBLES_COLLAPSED ? 1 : 0; 1052 } else { 1053 bubblePosition = index >= MAX_VISIBLE_BUBBLES_COLLAPSED 1054 ? MAX_VISIBLE_BUBBLES_COLLAPSED - 1 : index; 1055 } 1056 return mBubbleBarPadding + bubblePosition * (mIconOverlapAmount) + getScaledIconSize() / 2f; 1057 } 1058 1059 @Override setOnClickListener(View.OnClickListener listener)1060 public void setOnClickListener(View.OnClickListener listener) { 1061 mOnClickListener = listener; 1062 setOrUnsetClickListener(); 1063 } 1064 1065 /** 1066 * The click listener used for the bubble view gets added / removed depending on whether 1067 * the bar is expanded or collapsed, this updates whether the listener is set based on state. 1068 */ setOrUnsetClickListener()1069 private void setOrUnsetClickListener() { 1070 super.setOnClickListener(mIsBarExpanded ? null : mOnClickListener); 1071 } 1072 1073 /** 1074 * Sets whether the bubble bar is expanded or collapsed. 1075 */ setExpanded(boolean isBarExpanded)1076 public void setExpanded(boolean isBarExpanded) { 1077 if (mIsBarExpanded != isBarExpanded) { 1078 mIsBarExpanded = isBarExpanded; 1079 updateArrowForSelected(/* shouldAnimate= */ false); 1080 setOrUnsetClickListener(); 1081 if (isBarExpanded) { 1082 mWidthAnimator.start(); 1083 } else { 1084 mWidthAnimator.reverse(); 1085 } 1086 updateBubbleAccessibilityStates(); 1087 } 1088 } 1089 1090 /** 1091 * Returns whether the bubble bar is expanded. 1092 */ isExpanded()1093 public boolean isExpanded() { 1094 return mIsBarExpanded; 1095 } 1096 1097 /** 1098 * Get width of the bubble bar as if it would be expanded. 1099 * 1100 * @return width of the bubble bar in its expanded state, regardless of current width 1101 */ expandedWidth()1102 public float expandedWidth() { 1103 final int childCount = getChildCount(); 1104 // spaces amount is less than child count by 1, or 0 if no child views 1105 final float totalSpace; 1106 final float totalIconSize; 1107 if (mNewBubbleScaleInAnimator != null && mNewBubbleScaleInAnimator.isRunning()) { 1108 // when this animation is running, a new bubble is animating in while the bubble bar is 1109 // expanded, so we have at least 2 bubbles in the bubble bar. 1110 final float newBubbleScale = mNewBubbleScaleInAnimator.getAnimatedFraction(); 1111 totalSpace = (childCount - 2 + newBubbleScale) * mExpandedBarIconsSpacing; 1112 totalIconSize = (childCount - 1 + newBubbleScale) * getScaledIconSize(); 1113 } else { 1114 totalSpace = Math.max(childCount - 1, 0) * mExpandedBarIconsSpacing; 1115 totalIconSize = childCount * getScaledIconSize(); 1116 } 1117 return totalIconSize + totalSpace + 2 * mBubbleBarPadding; 1118 } 1119 collapsedWidth()1120 private float collapsedWidth() { 1121 final int childCount = getChildCount(); 1122 final float horizontalPadding = 2 * mBubbleBarPadding; 1123 // If there are more than 2 bubbles, the first 2 should be visible when collapsed. 1124 // Otherwise just the first bubble should be visible because we don't show the overflow. 1125 return childCount > MAX_VISIBLE_BUBBLES_COLLAPSED 1126 ? getScaledIconSize() + mIconOverlapAmount + horizontalPadding 1127 : getScaledIconSize() + horizontalPadding; 1128 } 1129 getBubbleBarExpandedHeight()1130 private float getBubbleBarExpandedHeight() { 1131 return getBubbleBarCollapsedHeight() + mPointerSize; 1132 } 1133 getBubbleBarCollapsedHeight()1134 float getBubbleBarCollapsedHeight() { 1135 // the pointer is invisible when collapsed 1136 return getScaledIconSize() + mBubbleBarPadding * 2; 1137 } 1138 1139 /** 1140 * Returns whether the given MotionEvent, *in screen coordinates*, is within bubble bar 1141 * touch bounds. 1142 */ isEventOverAnyItem(MotionEvent ev)1143 public boolean isEventOverAnyItem(MotionEvent ev) { 1144 if (getVisibility() == View.VISIBLE) { 1145 getBoundsOnScreen(mTempRect); 1146 return mTempRect.contains((int) ev.getX(), (int) ev.getY()); 1147 } 1148 return false; 1149 } 1150 1151 @Override onInterceptTouchEvent(MotionEvent ev)1152 public boolean onInterceptTouchEvent(MotionEvent ev) { 1153 if (mIsAnimatingNewBubble) { 1154 mController.onBubbleBarTouchedWhileAnimating(); 1155 } 1156 if (!mIsBarExpanded) { 1157 // When the bar is collapsed, all taps on it should expand it. 1158 return true; 1159 } 1160 return super.onInterceptTouchEvent(ev); 1161 } 1162 1163 /** Whether a new bubble is currently animating. */ isAnimatingNewBubble()1164 public boolean isAnimatingNewBubble() { 1165 return mIsAnimatingNewBubble; 1166 } 1167 1168 hasOverview()1169 private boolean hasOverview() { 1170 // Overview is always the last bubble 1171 View lastChild = getChildAt(getChildCount() - 1); 1172 if (lastChild instanceof BubbleView bubbleView) { 1173 return bubbleView.getBubble() instanceof BubbleBarOverflow; 1174 } 1175 return false; 1176 } 1177 updateBubbleAccessibilityStates()1178 private void updateBubbleAccessibilityStates() { 1179 final int childA11y; 1180 if (mIsBarExpanded) { 1181 // Bar is expanded, focus on the bubbles 1182 setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_NO); 1183 childA11y = View.IMPORTANT_FOR_ACCESSIBILITY_YES; 1184 } else { 1185 // Bar is collapsed, only focus on the bar 1186 setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_YES); 1187 childA11y = View.IMPORTANT_FOR_ACCESSIBILITY_NO; 1188 } 1189 for (int i = 0; i < getChildCount(); i++) { 1190 getChildAt(i).setImportantForAccessibility(childA11y); 1191 // Only allowing focusing on bubbles when bar is expanded. Otherwise, in talkback mode, 1192 // bubbles can be navigates to in collapsed mode. 1193 getChildAt(i).setFocusable(mIsBarExpanded); 1194 } 1195 } 1196 updateContentDescription()1197 private void updateContentDescription() { 1198 View firstChild = getChildAt(0); 1199 CharSequence contentDesc = firstChild != null ? firstChild.getContentDescription() : ""; 1200 1201 // Don't count overflow if it exists 1202 int bubbleCount = getChildCount() - (hasOverview() ? 1 : 0); 1203 if (bubbleCount > 1) { 1204 contentDesc = getResources().getString(R.string.bubble_bar_description_multiple_bubbles, 1205 contentDesc, bubbleCount - 1); 1206 } 1207 setContentDescription(contentDesc); 1208 } 1209 isIconSizeOrPaddingUpdated(float newIconSize, float newBubbleBarPadding)1210 private boolean isIconSizeOrPaddingUpdated(float newIconSize, float newBubbleBarPadding) { 1211 return isIconSizeUpdated(newIconSize) || isPaddingUpdated(newBubbleBarPadding); 1212 } 1213 isIconSizeUpdated(float newIconSize)1214 private boolean isIconSizeUpdated(float newIconSize) { 1215 return Float.compare(mIconSize, newIconSize) != 0; 1216 } 1217 isPaddingUpdated(float newBubbleBarPadding)1218 private boolean isPaddingUpdated(float newBubbleBarPadding) { 1219 return Float.compare(mBubbleBarPadding, newBubbleBarPadding) != 0; 1220 } 1221 addAnimationCallBacks(@onNull ValueAnimator animator, @Nullable Runnable onStart, @Nullable Runnable onEnd, @Nullable ValueAnimator.AnimatorUpdateListener onUpdate)1222 private void addAnimationCallBacks(@NonNull ValueAnimator animator, 1223 @Nullable Runnable onStart, 1224 @Nullable Runnable onEnd, 1225 @Nullable ValueAnimator.AnimatorUpdateListener onUpdate) { 1226 if (onUpdate != null) animator.addUpdateListener(onUpdate); 1227 animator.addListener(new Animator.AnimatorListener() { 1228 @Override 1229 public void onAnimationCancel(Animator animator) { 1230 1231 } 1232 1233 @Override 1234 public void onAnimationStart(Animator animator) { 1235 if (onStart != null) onStart.run(); 1236 } 1237 1238 @Override 1239 public void onAnimationEnd(Animator animator) { 1240 if (onEnd != null) onEnd.run(); 1241 } 1242 1243 @Override 1244 public void onAnimationRepeat(Animator animator) { 1245 1246 } 1247 }); 1248 } 1249 1250 /** Interface for BubbleBarView to communicate with its controller. */ 1251 interface Controller { 1252 1253 /** Returns the translation Y that the bubble bar should have. */ getBubbleBarTranslationY()1254 float getBubbleBarTranslationY(); 1255 1256 /** Notifies the controller that the bubble bar was touched while it was animating. */ onBubbleBarTouchedWhileAnimating()1257 void onBubbleBarTouchedWhileAnimating(); 1258 } 1259 } 1260