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.launcher3.LauncherAnimUtils.VIEW_ALPHA; 19 20 import android.animation.Animator; 21 import android.animation.AnimatorListenerAdapter; 22 import android.animation.ObjectAnimator; 23 import android.animation.ValueAnimator; 24 import android.annotation.NonNull; 25 import android.annotation.Nullable; 26 import android.annotation.SuppressLint; 27 import android.content.Context; 28 import android.graphics.PointF; 29 import android.graphics.Rect; 30 import android.os.Bundle; 31 import android.util.AttributeSet; 32 import android.util.FloatProperty; 33 import android.util.LayoutDirection; 34 import android.util.Log; 35 import android.view.Gravity; 36 import android.view.MotionEvent; 37 import android.view.View; 38 import android.view.ViewGroup; 39 import android.view.accessibility.AccessibilityNodeInfo; 40 import android.widget.FrameLayout; 41 42 import com.android.app.animation.Interpolators; 43 import com.android.launcher3.R; 44 import com.android.launcher3.anim.AnimatorListeners; 45 import com.android.launcher3.taskbar.BarsLocationAnimatorHelper; 46 import com.android.launcher3.taskbar.bubbles.animation.BubbleAnimator; 47 import com.android.launcher3.util.DisplayController; 48 import com.android.wm.shell.shared.bubbles.BubbleBarLocation; 49 50 import java.io.PrintWriter; 51 import java.util.ArrayList; 52 import java.util.List; 53 import java.util.function.Consumer; 54 55 /** 56 * The view that holds all the bubble views. Modifying this view should happen through 57 * {@link BubbleBarViewController}. Updates to the bubbles themselves (adds, removes, updates, 58 * selection) should happen through {@link BubbleBarController} which is the source of truth 59 * for state information about the bubbles. 60 * <p> 61 * The bubble bar has a couple of visual states: 62 * - stashed as a handle 63 * - unstashed but collapsed, in this state the bar is showing but the bubbles are stacked within it 64 * - unstashed and expanded, in this state the bar is showing and the bubbles are shown in a row 65 * with one of the bubbles being selected. Additionally, WMShell will display the expanded bubble 66 * view above the bar. 67 * <p> 68 * The bubble bar has some behavior related to taskbar: 69 * - When taskbar is unstashed, bubble bar will also become unstashed (but in its "collapsed" 70 * state) 71 * - When taskbar is stashed, bubble bar will also become stashed (unless bubble bar is in its 72 * "expanded" state) 73 * - When bubble bar is in its "expanded" state, taskbar becomes stashed 74 * <p> 75 * If there are no bubbles, the bubble bar and bubble stashed handle are not shown. Additionally 76 * the bubble bar and stashed handle are not shown on lockscreen. 77 * <p> 78 * When taskbar is in persistent or 3 button nav mode, the bubble bar is not available, and instead 79 * the bubbles are shown fully by WMShell in their floating mode. 80 */ 81 public class BubbleBarView extends FrameLayout { 82 83 public static final long FADE_OUT_ANIM_POSITION_DURATION_MS = 100L; 84 public static final long FADE_IN_ANIM_ALPHA_DURATION_MS = 100L; 85 public static final long FADE_OUT_BUBBLE_BAR_DURATION_MS = 150L; 86 private static final String TAG = "BubbleBarView"; 87 // TODO: (b/273594744) calculate the amount of space we have and base the max on that 88 // if it's smaller than 5. 89 private static final int MAX_BUBBLES = 5; 90 private static final int MAX_VISIBLE_BUBBLES_COLLAPSED = 2; 91 private static final int ARROW_POSITION_ANIMATION_DURATION_MS = 200; 92 private static final int WIDTH_ANIMATION_DURATION_MS = 400; 93 private static final int SCALE_ANIMATION_DURATION_MS = 200; 94 95 /** 96 * Custom property to set alpha value for the bar view while a bubble is being dragged. 97 * Skips applying alpha to the dragged bubble. 98 */ 99 private static final FloatProperty<BubbleBarView> BUBBLE_DRAG_ALPHA = 100 new FloatProperty<>("bubbleDragAlpha") { 101 @Override 102 public void setValue(BubbleBarView bubbleBarView, float alpha) { 103 bubbleBarView.setAlphaDuringBubbleDrag(alpha); 104 } 105 106 @Override 107 public Float get(BubbleBarView bubbleBarView) { 108 return bubbleBarView.mAlphaDuringDrag; 109 } 110 }; 111 112 private final BubbleBarBackground mBubbleBarBackground; 113 114 /** 115 * The current bounds of all the bubble bar. Note that these bounds may not account for 116 * translation. The bounds should be retrieved using {@link #getBubbleBarBounds()} which 117 * updates the bounds and accounts for translation. 118 */ 119 private final Rect mBubbleBarBounds = new Rect(); 120 // The amount the bubbles overlap when they are stacked in the bubble bar 121 private final float mIconOverlapAmount; 122 // The spacing between the bubbles when bubble bar is expanded 123 private final float mExpandedBarIconsSpacing; 124 // The spacing between the bubbles and the borders of the bubble bar 125 private float mBubbleBarPadding; 126 // The size of a bubble in the bar 127 private float mIconSize; 128 // The scale of bubble icons 129 private float mIconScale = 1f; 130 // The elevation of the bubbles within the bar 131 private final float mBubbleElevation; 132 private final float mDragElevation; 133 private final int mPointerSize; 134 // Whether the bar is expanded (i.e. the bubble activity is being displayed). 135 private boolean mIsBarExpanded = false; 136 // The currently selected bubble view. 137 @Nullable 138 private BubbleView mSelectedBubbleView; 139 private BubbleBarLocation mBubbleBarLocation = BubbleBarLocation.DEFAULT; 140 // The click listener when the bubble bar is collapsed. 141 private View.OnClickListener mOnClickListener; 142 143 private final Rect mTempRect = new Rect(); 144 private float mRelativePivotX = 1f; 145 private float mRelativePivotY = 1f; 146 147 // An animator that represents the expansion state of the bubble bar, where 0 corresponds to the 148 // collapsed state and 1 to the fully expanded state. 149 private ValueAnimator mWidthAnimator = createExpansionAnimator(/* expanding = */ false); 150 151 @Nullable 152 private ValueAnimator mDismissAnimator = null; 153 154 /** An animator used for animating individual bubbles in the bubble bar while expanded. */ 155 @Nullable 156 private BubbleAnimator mBubbleAnimator = null; 157 @Nullable 158 private ValueAnimator mScalePaddingAnimator; 159 160 @Nullable 161 private Animator mBubbleBarLocationAnimator = null; 162 163 // We don't reorder the bubbles when they are expanded as it could be jarring for the user 164 // this runnable will be populated with any reordering of the bubbles that should be applied 165 // once they are collapsed. 166 @Nullable 167 private Runnable mReorderRunnable; 168 169 @Nullable 170 private Consumer<String> mUpdateSelectedBubbleAfterCollapse; 171 172 private boolean mDragging; 173 174 @Nullable 175 private BubbleView mDraggedBubbleView; 176 @Nullable 177 private BubbleView mDismissedByDragBubbleView; 178 private float mAlphaDuringDrag = 1f; 179 180 /** Additional translation in the y direction that is applied to each bubble */ 181 private float mBubbleOffsetY; 182 183 private Controller mController; 184 185 private int mPreviousLayoutDirection = LayoutDirection.UNDEFINED; 186 BubbleBarView(Context context)187 public BubbleBarView(Context context) { 188 this(context, null); 189 } 190 BubbleBarView(Context context, AttributeSet attrs)191 public BubbleBarView(Context context, AttributeSet attrs) { 192 this(context, attrs, 0); 193 } 194 BubbleBarView(Context context, AttributeSet attrs, int defStyleAttr)195 public BubbleBarView(Context context, AttributeSet attrs, int defStyleAttr) { 196 this(context, attrs, defStyleAttr, 0); 197 } 198 BubbleBarView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes)199 public BubbleBarView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { 200 super(context, attrs, defStyleAttr, defStyleRes); 201 setVisibility(INVISIBLE); 202 mIconOverlapAmount = getResources().getDimensionPixelSize(R.dimen.bubblebar_icon_overlap); 203 mBubbleBarPadding = getResources().getDimensionPixelSize(R.dimen.bubblebar_icon_spacing); 204 mIconSize = getResources().getDimensionPixelSize(R.dimen.bubblebar_icon_size); 205 mExpandedBarIconsSpacing = getResources().getDimensionPixelSize( 206 R.dimen.bubblebar_expanded_icon_spacing); 207 mBubbleElevation = getResources().getDimensionPixelSize(R.dimen.bubblebar_icon_elevation); 208 mDragElevation = getResources().getDimensionPixelSize(R.dimen.dragged_bubble_elevation); 209 mPointerSize = getResources() 210 .getDimensionPixelSize(R.dimen.bubblebar_pointer_visible_size); 211 212 setClipToPadding(false); 213 214 mBubbleBarBackground = new BubbleBarBackground(context, getBubbleBarExpandedHeight()); 215 setBackgroundDrawable(mBubbleBarBackground); 216 } 217 218 219 /** 220 * Animates icon sizes and spacing between icons and bubble bar borders. 221 * 222 * @param newIconSize new icon size 223 * @param newBubbleBarPadding spacing between icons and bubble bar borders. 224 */ animateBubbleBarIconSize(float newIconSize, float newBubbleBarPadding)225 public void animateBubbleBarIconSize(float newIconSize, float newBubbleBarPadding) { 226 if (!isIconSizeOrPaddingUpdated(newIconSize, newBubbleBarPadding)) { 227 return; 228 } 229 if (mScalePaddingAnimator != null && mScalePaddingAnimator.isRunning()) { 230 mScalePaddingAnimator.cancel(); 231 } 232 ValueAnimator scalePaddingAnimator = ValueAnimator.ofFloat(0f, 1f); 233 scalePaddingAnimator.setDuration(SCALE_ANIMATION_DURATION_MS); 234 boolean isPaddingUpdated = isPaddingUpdated(newBubbleBarPadding); 235 boolean isIconSizeUpdated = isIconSizeUpdated(newIconSize); 236 float initialScale = mIconScale; 237 float initialPadding = mBubbleBarPadding; 238 float targetScale = newIconSize / getScaledIconSize(); 239 240 addAnimationCallBacks(scalePaddingAnimator, 241 /* onStart= */ null, 242 /* onEnd= */ () -> setIconSizeAndPadding(newIconSize, newBubbleBarPadding), 243 /* onUpdate= */ animator -> { 244 float transitionProgress = (float) animator.getAnimatedValue(); 245 if (isIconSizeUpdated) { 246 mIconScale = 247 initialScale + (targetScale - initialScale) * transitionProgress; 248 } 249 if (isPaddingUpdated) { 250 mBubbleBarPadding = initialPadding 251 + (newBubbleBarPadding - initialPadding) * transitionProgress; 252 } 253 updateBubblesLayoutProperties(mBubbleBarLocation); 254 invalidate(); 255 }); 256 scalePaddingAnimator.start(); 257 mScalePaddingAnimator = scalePaddingAnimator; 258 } 259 260 @Override setTranslationX(float translationX)261 public void setTranslationX(float translationX) { 262 super.setTranslationX(translationX); 263 if (mDraggedBubbleView != null) { 264 // Apply reverse of the translation as an offset to the dragged view. This ensures 265 // that the dragged bubble stays at the current location on the screen and its 266 // position is not affected by the parent translation. 267 mDraggedBubbleView.setOffsetX(-translationX); 268 } 269 } 270 271 /** 272 * Set scale for bubble bar background in x direction 273 */ setBackgroundScaleX(float scaleX)274 public void setBackgroundScaleX(float scaleX) { 275 mBubbleBarBackground.setScaleX(scaleX); 276 } 277 278 /** 279 * Set scale for bubble bar background in y direction 280 */ setBackgroundScaleY(float scaleY)281 public void setBackgroundScaleY(float scaleY) { 282 mBubbleBarBackground.setScaleY(scaleY); 283 } 284 285 /** 286 * Set alpha for bubble views 287 */ setBubbleAlpha(float alpha)288 public void setBubbleAlpha(float alpha) { 289 for (int i = 0; i < getChildCount(); i++) { 290 getChildAt(i).setAlpha(alpha); 291 } 292 } 293 294 /** 295 * Set alpha for bar background 296 */ setBackgroundAlpha(float alpha)297 public void setBackgroundAlpha(float alpha) { 298 mBubbleBarBackground.setAlpha((int) (255 * alpha)); 299 } 300 301 /** 302 * Sets offset of each bubble view in the y direction from the base position in the bar. 303 */ setBubbleOffsetY(float offsetY)304 public void setBubbleOffsetY(float offsetY) { 305 mBubbleOffsetY = offsetY; 306 for (int i = 0; i < getChildCount(); i++) { 307 getChildAt(i).setTranslationY(getBubbleTranslationY()); 308 } 309 } 310 311 /** 312 * Set the bubble icons size and spacing between the bubbles and the borders of the bubble 313 * bar. 314 */ setIconSizeAndPaddingForPinning(float newIconSize, float newBubbleBarPadding)315 public void setIconSizeAndPaddingForPinning(float newIconSize, float newBubbleBarPadding) { 316 mBubbleBarPadding = newBubbleBarPadding; 317 mIconScale = newIconSize / mIconSize; 318 updateBubblesLayoutProperties(mBubbleBarLocation); 319 invalidate(); 320 } 321 322 /** 323 * Sets new icon sizes and newBubbleBarPadding between icons and bubble bar borders. 324 * 325 * @param newIconSize new icon size 326 * @param newBubbleBarPadding newBubbleBarPadding between icons and bubble bar borders. 327 */ setIconSizeAndPadding(float newIconSize, float newBubbleBarPadding)328 public void setIconSizeAndPadding(float newIconSize, float newBubbleBarPadding) { 329 // TODO(b/335457839): handle new bubble animation during the size change 330 if (!isIconSizeOrPaddingUpdated(newIconSize, newBubbleBarPadding)) { 331 return; 332 } 333 mIconScale = 1f; 334 mBubbleBarPadding = newBubbleBarPadding; 335 mIconSize = newIconSize; 336 int childCount = getChildCount(); 337 for (int i = 0; i < childCount; i++) { 338 View childView = getChildAt(i); 339 childView.setScaleX(mIconScale); 340 childView.setScaleY(mIconScale); 341 FrameLayout.LayoutParams params = (LayoutParams) childView.getLayoutParams(); 342 params.height = (int) mIconSize; 343 params.width = (int) mIconSize; 344 childView.setLayoutParams(params); 345 } 346 mBubbleBarBackground.setBackgroundHeight(getBubbleBarHeight()); 347 updateLayoutParams(); 348 } 349 getScaledIconSize()350 private float getScaledIconSize() { 351 return mIconSize * mIconScale; 352 } 353 354 @Override onLayout(boolean changed, int left, int top, int right, int bottom)355 protected void onLayout(boolean changed, int left, int top, int right, int bottom) { 356 super.onLayout(changed, left, top, right, bottom); 357 mBubbleBarBounds.left = left; 358 mBubbleBarBounds.top = top + mPointerSize; 359 mBubbleBarBounds.right = right; 360 mBubbleBarBounds.bottom = bottom; 361 362 // The bubble bar handle is aligned according to the relative pivot, 363 // by default it's aligned to the bottom edge of the screen so scale towards that 364 setPivotX(mRelativePivotX * getWidth()); 365 setPivotY(mRelativePivotY * getHeight()); 366 367 if (!mDragging) { 368 // Position the views when not dragging 369 updateBubblesLayoutProperties(mBubbleBarLocation); 370 } 371 } 372 373 @Override onRtlPropertiesChanged(int layoutDirection)374 public void onRtlPropertiesChanged(int layoutDirection) { 375 if (mBubbleBarLocation == BubbleBarLocation.DEFAULT 376 && mPreviousLayoutDirection != layoutDirection) { 377 Log.d(TAG, "BubbleBar RTL properties changed, new layoutDirection=" + layoutDirection 378 + " previous layoutDirection=" + mPreviousLayoutDirection); 379 mPreviousLayoutDirection = layoutDirection; 380 onBubbleBarLocationChanged(); 381 } 382 } 383 384 @Override onInitializeAccessibilityNodeInfoInternal(AccessibilityNodeInfo info)385 public void onInitializeAccessibilityNodeInfoInternal(AccessibilityNodeInfo info) { 386 super.onInitializeAccessibilityNodeInfoInternal(info); 387 // Always show only expand action as the menu is only for collapsed bubble bar 388 info.addAction(AccessibilityNodeInfo.AccessibilityAction.ACTION_EXPAND); 389 info.addAction(AccessibilityNodeInfo.AccessibilityAction.ACTION_CLICK); 390 info.addAction(new AccessibilityNodeInfo.AccessibilityAction(R.id.action_dismiss_all, 391 getResources().getString(R.string.bubble_bar_action_dismiss_all))); 392 if (mBubbleBarLocation.isOnLeft(isLayoutRtl())) { 393 info.addAction(new AccessibilityNodeInfo.AccessibilityAction(R.id.action_move_right, 394 getResources().getString(R.string.bubble_bar_action_move_right))); 395 } else { 396 info.addAction(new AccessibilityNodeInfo.AccessibilityAction(R.id.action_move_left, 397 getResources().getString(R.string.bubble_bar_action_move_left))); 398 } 399 } 400 401 @Override performAccessibilityActionInternal(int action, @androidx.annotation.Nullable Bundle arguments)402 public boolean performAccessibilityActionInternal(int action, 403 @androidx.annotation.Nullable Bundle arguments) { 404 if (action == AccessibilityNodeInfo.ACTION_EXPAND 405 || action == AccessibilityNodeInfo.ACTION_CLICK) { 406 mController.expandBubbleBar(); 407 return true; 408 } 409 if (action == R.id.action_dismiss_all) { 410 mController.dismissBubbleBar(); 411 return true; 412 } 413 if (action == R.id.action_move_left) { 414 mController.updateBubbleBarLocation(BubbleBarLocation.LEFT, 415 BubbleBarLocation.UpdateSource.A11Y_ACTION_BAR); 416 return true; 417 } 418 if (action == R.id.action_move_right) { 419 mController.updateBubbleBarLocation(BubbleBarLocation.RIGHT, 420 BubbleBarLocation.UpdateSource.A11Y_ACTION_BAR); 421 return true; 422 } 423 return super.performAccessibilityActionInternal(action, arguments); 424 } 425 426 @SuppressLint("RtlHardcoded") onBubbleBarLocationChanged()427 private void onBubbleBarLocationChanged() { 428 final boolean onLeft = mBubbleBarLocation.isOnLeft(isLayoutRtl()); 429 mBubbleBarBackground.setAnchorLeft(onLeft); 430 mRelativePivotX = onLeft ? 0f : 1f; 431 LayoutParams lp = (LayoutParams) getLayoutParams(); 432 lp.gravity = Gravity.BOTTOM | (onLeft ? Gravity.LEFT : Gravity.RIGHT); 433 setLayoutParams(lp); // triggers a relayout 434 updateBubbleAccessibilityStates(); 435 } 436 437 /** 438 * @return current {@link BubbleBarLocation} 439 */ getBubbleBarLocation()440 public BubbleBarLocation getBubbleBarLocation() { 441 return mBubbleBarLocation; 442 } 443 444 /** 445 * Update {@link BubbleBarLocation} 446 */ setBubbleBarLocation(BubbleBarLocation bubbleBarLocation)447 public void setBubbleBarLocation(BubbleBarLocation bubbleBarLocation) { 448 resetDragAnimation(); 449 if (bubbleBarLocation != mBubbleBarLocation) { 450 mBubbleBarLocation = bubbleBarLocation; 451 onBubbleBarLocationChanged(); 452 } 453 } 454 455 /** 456 * Set whether this view is currently being dragged 457 */ setIsDragging(boolean dragging)458 public void setIsDragging(boolean dragging) { 459 if (mDragging == dragging) { 460 return; 461 } 462 mDragging = dragging; 463 mController.setIsDragging(dragging); 464 if (!mDragging) { 465 // Relayout after dragging to ensure that the dragged bubble is positioned correctly 466 requestLayout(); 467 } 468 } 469 470 /** 471 * Get translation for bubble bar when drag is released and it needs to animate back to the 472 * resting position. 473 * Resting position is based on the supplied location. If the supplied location is different 474 * from the internal location that was used during bubble bar layout, translation values are 475 * calculated to position the bar at the desired location. 476 * 477 * @param initialTranslation initial bubble bar translation at the start of drag 478 * @param location desired location of the bubble bar when drag is released 479 * @return point with x and y values representing translation on x and y-axis 480 */ getBubbleBarDragReleaseTranslation(PointF initialTranslation, BubbleBarLocation location)481 public PointF getBubbleBarDragReleaseTranslation(PointF initialTranslation, 482 BubbleBarLocation location) { 483 float dragEndTranslationX = initialTranslation.x; 484 if (getBubbleBarLocation().isOnLeft(isLayoutRtl()) != location.isOnLeft(isLayoutRtl())) { 485 // Bubble bar is laid out on left or right side of the screen. And the desired new 486 // location is on the other side. Calculate x translation value required to shift 487 // bubble bar from one side to the other. 488 final float shift = getDistanceFromOtherSide(); 489 if (location.isOnLeft(isLayoutRtl())) { 490 // New location is on the left, shift left 491 // before -> |......ooo.| after -> |.ooo......| 492 dragEndTranslationX = -shift; 493 } else { 494 // New location is on the right, shift right 495 // before -> |.ooo......| after -> |......ooo.| 496 dragEndTranslationX = shift; 497 } 498 } 499 return new PointF(dragEndTranslationX, mController.getBubbleBarTranslationY()); 500 } 501 502 /** 503 * Get translation for a bubble when drag is released and it needs to animate back to the 504 * resting position. 505 * Resting position is based on the supplied location. If the supplied location is different 506 * from the internal location that was used during bubble bar layout, translation values are 507 * calculated to position the bar at the desired location. 508 * 509 * @param initialTranslation initial bubble translation inside the bar at the start of drag 510 * @param location desired location of the bubble bar when drag is released 511 * @return point with x and y values representing translation on x and y-axis 512 */ getDraggedBubbleReleaseTranslation(PointF initialTranslation, BubbleBarLocation location)513 public PointF getDraggedBubbleReleaseTranslation(PointF initialTranslation, 514 BubbleBarLocation location) { 515 float dragEndTranslationX = initialTranslation.x; 516 boolean newLocationOnLeft = location.isOnLeft(isLayoutRtl()); 517 if (getBubbleBarLocation().isOnLeft(isLayoutRtl()) != newLocationOnLeft) { 518 // Calculate translationX based on bar and bubble translations 519 float bubbleBarTx = getBubbleBarDragReleaseTranslation(initialTranslation, location).x; 520 float bubbleTx = 521 getExpandedBubbleTranslationX( 522 indexOfChild(mDraggedBubbleView), getChildCount(), newLocationOnLeft); 523 dragEndTranslationX = bubbleBarTx + bubbleTx; 524 } 525 // translationY does not change during drag and can be reused 526 return new PointF(dragEndTranslationX, initialTranslation.y); 527 } 528 getDistanceFromOtherSide()529 private float getDistanceFromOtherSide() { 530 // Calculate the shift needed to position the bubble bar on the other side 531 int displayWidth = getResources().getDisplayMetrics().widthPixels; 532 int margin = 0; 533 if (getLayoutParams() instanceof MarginLayoutParams lp) { 534 margin += lp.leftMargin; 535 margin += lp.rightMargin; 536 } 537 return (float) (displayWidth - getWidth() - margin); 538 } 539 540 /** Set whether the background should show the drop target */ showDropTarget(boolean isDropTarget)541 public void showDropTarget(boolean isDropTarget) { 542 mBubbleBarBackground.showDropTarget(isDropTarget); 543 } 544 545 /** Returns whether the Bubble Bar is currently displaying a drop target. */ isShowingDropTarget()546 public boolean isShowingDropTarget() { 547 return mBubbleBarBackground.isShowingDropTarget(); 548 } 549 550 /** 551 * Animate bubble bar to the given location transiently. Does not modify the layout or the value 552 * returned by {@link #getBubbleBarLocation()}. 553 */ animateToBubbleBarLocation(BubbleBarLocation bubbleBarLocation)554 public void animateToBubbleBarLocation(BubbleBarLocation bubbleBarLocation) { 555 if (mBubbleBarLocationAnimator != null && mBubbleBarLocationAnimator.isRunning()) { 556 mBubbleBarLocationAnimator.removeAllListeners(); 557 mBubbleBarLocationAnimator.cancel(); 558 } 559 560 // Location animation uses two separate animators. 561 // First animator hides the bar. 562 // After it completes, bubble positions in the bar and arrow position is updated. 563 // Second animator is started to show the bar. 564 mBubbleBarLocationAnimator = animateToBubbleBarLocationOut(bubbleBarLocation); 565 mBubbleBarLocationAnimator.addListener(AnimatorListeners.forEndCallback(() -> { 566 // Animate it in 567 mBubbleBarLocationAnimator = animateToBubbleBarLocationIn(mBubbleBarLocation, 568 bubbleBarLocation); 569 mBubbleBarLocationAnimator.start(); 570 })); 571 mBubbleBarLocationAnimator.start(); 572 } 573 574 /** Creates animator for animating bubble bar in. */ animateToBubbleBarLocationIn(BubbleBarLocation fromLocation, BubbleBarLocation toLocation)575 public Animator animateToBubbleBarLocationIn(BubbleBarLocation fromLocation, 576 BubbleBarLocation toLocation) { 577 updateBubblesLayoutProperties(toLocation); 578 mBubbleBarBackground.setAnchorLeft(toLocation.isOnLeft(isLayoutRtl())); 579 ObjectAnimator alphaInAnim = ObjectAnimator.ofFloat(BubbleBarView.this, 580 getLocationAnimAlphaProperty(), 1f); 581 return BarsLocationAnimatorHelper.getBubbleBarLocationInAnimator(toLocation, fromLocation, 582 getDistanceFromOtherSide(), alphaInAnim, this); 583 } 584 585 /** 586 * Creates animator for animating bubble bar out. 587 * 588 * @param targetLocation the location bubble br should animate to. 589 */ animateToBubbleBarLocationOut(BubbleBarLocation targetLocation)590 public Animator animateToBubbleBarLocationOut(BubbleBarLocation targetLocation) { 591 ObjectAnimator alphaOutAnim = ObjectAnimator.ofFloat( 592 this, getLocationAnimAlphaProperty(), 0f); 593 Animator outAnimation = BarsLocationAnimatorHelper.getBubbleBarLocationOutAnimator( 594 this, 595 targetLocation, 596 alphaOutAnim); 597 outAnimation.addListener(new AnimatorListenerAdapter() { 598 @Override 599 public void onAnimationEnd(@NonNull Animator animation, boolean isReverse) { 600 // need to restore the original bar view state in case icon is dropped to the bubble 601 // bar original location 602 updateBubblesLayoutProperties(targetLocation); 603 mBubbleBarBackground.setAnchorLeft(targetLocation.isOnLeft(isLayoutRtl())); 604 setTranslationX(0f); 605 } 606 }); 607 return outAnimation; 608 } 609 610 /** 611 * Get property that can be used to animate the alpha value for the bar. 612 * When a bubble is being dragged, uses {@link #BUBBLE_DRAG_ALPHA}. 613 * Falls back to {@link com.android.launcher3.LauncherAnimUtils#VIEW_ALPHA} otherwise. 614 */ getLocationAnimAlphaProperty()615 private FloatProperty<? super BubbleBarView> getLocationAnimAlphaProperty() { 616 return mDraggedBubbleView == null ? VIEW_ALPHA : BUBBLE_DRAG_ALPHA; 617 } 618 619 /** 620 * Set alpha value for the bar while a bubble is being dragged. 621 * We can not update the alpha on the bar directly because the dragged bubble would be affected 622 * as well. As it is a child view. 623 * Instead, while a bubble is being dragged, set alpha on each child view, that is not the 624 * dragged view. And set an alpha on the background. 625 * This allows for the dragged bubble to remain visible while the bar is hidden during 626 * animation. 627 */ setAlphaDuringBubbleDrag(float alpha)628 private void setAlphaDuringBubbleDrag(float alpha) { 629 mAlphaDuringDrag = alpha; 630 final int childCount = getChildCount(); 631 for (int i = 0; i < childCount; i++) { 632 View view = getChildAt(i); 633 if (view != mDraggedBubbleView) { 634 view.setAlpha(alpha); 635 } 636 } 637 if (mBubbleBarBackground != null) { 638 mBubbleBarBackground.setAlpha((int) (255 * alpha)); 639 } 640 } 641 resetDragAnimation()642 private void resetDragAnimation() { 643 if (mBubbleBarLocationAnimator != null) { 644 mBubbleBarLocationAnimator.removeAllListeners(); 645 mBubbleBarLocationAnimator.cancel(); 646 mBubbleBarLocationAnimator = null; 647 } 648 setAlphaDuringBubbleDrag(1f); 649 setTranslationX(0f); 650 if (mIsBarExpanded && getBubbleChildCount() > 0) { 651 setAlpha(1f); 652 } 653 } 654 655 /** 656 * Get bubble bar top coordinate on screen when bar is resting 657 */ getRestingTopPositionOnScreen()658 public int getRestingTopPositionOnScreen() { 659 int displayHeight = DisplayController.INSTANCE.get(getContext()).getInfo().currentSize.y; 660 int bubbleBarHeight = getBubbleBarBounds().height(); 661 return displayHeight - bubbleBarHeight + (int) mController.getBubbleBarTranslationY(); 662 } 663 664 /** Returns the bounds with translation that may have been applied. */ getBubbleBarBounds()665 public Rect getBubbleBarBounds() { 666 Rect bounds = new Rect(mBubbleBarBounds); 667 bounds.top = getTop() + (int) getTranslationY() + mPointerSize; 668 bounds.bottom = getBottom() + (int) getTranslationY(); 669 return bounds; 670 } 671 672 /** Returns the expanded bounds with translation that may have been applied. */ getBubbleBarExpandedBounds()673 public Rect getBubbleBarExpandedBounds() { 674 Rect expandedBounds = getBubbleBarBounds(); 675 if (!isExpanded() || isExpanding()) { 676 if (mBubbleBarLocation.isOnLeft(isLayoutRtl())) { 677 expandedBounds.right = expandedBounds.left + (int) expandedWidth(); 678 } else { 679 expandedBounds.left = expandedBounds.right - (int) expandedWidth(); 680 } 681 } 682 return expandedBounds; 683 } 684 685 /** 686 * Set bubble bar relative pivot value for X and Y, applied as a fraction of view width/height 687 * respectively. If the value is not in range of 0 to 1 it will be normalized. 688 * 689 * @param x relative X pivot value in range 0..1 690 * @param y relative Y pivot value in range 0..1 691 */ setRelativePivot(float x, float y)692 public void setRelativePivot(float x, float y) { 693 mRelativePivotX = Float.max(Float.min(x, 1), 0); 694 mRelativePivotY = Float.max(Float.min(y, 1), 0); 695 requestLayout(); 696 } 697 698 /** Like {@link #setRelativePivot(float, float)} but only updates pivot y. */ setRelativePivotY(float y)699 public void setRelativePivotY(float y) { 700 setRelativePivot(mRelativePivotX, y); 701 } 702 703 /** 704 * Get current relative pivot for X axis 705 */ getRelativePivotX()706 public float getRelativePivotX() { 707 return mRelativePivotX; 708 } 709 710 /** 711 * Get current relative pivot for Y axis 712 */ getRelativePivotY()713 public float getRelativePivotY() { 714 return mRelativePivotY; 715 } 716 717 /** Add a new bubble to the bubble bar without updating the selected bubble. */ addBubble(BubbleView bubble)718 public void addBubble(BubbleView bubble) { 719 addBubble(bubble, /* bubbleToSelect = */ null); 720 } 721 722 /** 723 * Add a new bubble to the bubble bar and selects the provided bubble. 724 * 725 * @param bubble bubble to add 726 * @param bubbleToSelect if {@code null}, then selected bubble does not change 727 */ addBubble(BubbleView bubble, @Nullable BubbleView bubbleToSelect)728 public void addBubble(BubbleView bubble, @Nullable BubbleView bubbleToSelect) { 729 FrameLayout.LayoutParams lp = new FrameLayout.LayoutParams((int) mIconSize, (int) mIconSize, 730 Gravity.LEFT); 731 final int index = bubble.isOverflow() ? getChildCount() : 0; 732 733 if (isExpanded()) { 734 // if we're expanded scale the new bubble in 735 bubble.setScaleX(0f); 736 bubble.setScaleY(0f); 737 addView(bubble, index, lp); 738 bubble.showDotIfNeeded(/* animate= */ false); 739 740 mBubbleAnimator = new BubbleAnimator(mIconSize, mExpandedBarIconsSpacing, 741 getChildCount(), mBubbleBarLocation.isOnLeft(isLayoutRtl())); 742 BubbleAnimator.Listener listener = new BubbleAnimator.Listener() { 743 744 @Override 745 public void onAnimationEnd() { 746 updateLayoutParams(); 747 mBubbleAnimator = null; 748 } 749 750 @Override 751 public void onAnimationCancel() { 752 bubble.setScaleX(1); 753 bubble.setScaleY(1); 754 } 755 756 @Override 757 public void onAnimationUpdate(float animatedFraction) { 758 bubble.setScaleX(animatedFraction); 759 bubble.setScaleY(animatedFraction); 760 updateBubblesLayoutProperties(mBubbleBarLocation); 761 invalidate(); 762 } 763 }; 764 if (bubbleToSelect != null) { 765 mBubbleAnimator.animateNewBubble(indexOfChild(mSelectedBubbleView), 766 indexOfChild(bubbleToSelect), listener); 767 } else { 768 mBubbleAnimator.animateNewBubble(indexOfChild(mSelectedBubbleView), listener); 769 } 770 } else { 771 addView(bubble, index, lp); 772 } 773 } 774 775 /** Add a new bubble and remove an old bubble from the bubble bar. */ addBubbleAndRemoveBubble(BubbleView addedBubble, BubbleView removedBubble, @Nullable BubbleView bubbleToSelect, Runnable onEndRunnable)776 public void addBubbleAndRemoveBubble(BubbleView addedBubble, BubbleView removedBubble, 777 @Nullable BubbleView bubbleToSelect, Runnable onEndRunnable) { 778 FrameLayout.LayoutParams lp = new FrameLayout.LayoutParams((int) mIconSize, (int) mIconSize, 779 Gravity.LEFT); 780 int addedIndex = addedBubble.isOverflow() ? getChildCount() : 0; 781 if (!isExpanded()) { 782 removeView(removedBubble); 783 addView(addedBubble, addedIndex, lp); 784 if (onEndRunnable != null) { 785 onEndRunnable.run(); 786 } 787 return; 788 } 789 addedBubble.setScaleX(0f); 790 addedBubble.setScaleY(0f); 791 addView(addedBubble, addedIndex, lp); 792 int indexOfCurrentSelectedBubble = indexOfChild(mSelectedBubbleView); 793 int indexOfBubbleToRemove = indexOfChild(removedBubble); 794 int indexOfNewlySelectedBubble = bubbleToSelect == null 795 ? indexOfCurrentSelectedBubble : indexOfChild(bubbleToSelect); 796 // Since removed bubble is kept till the end of the animation we should check if there are 797 // more than one bubble. In such a case the bar will remain open without the selected bubble 798 if (mSelectedBubbleView == removedBubble 799 && bubbleToSelect == null 800 && getBubbleChildCount() > 1) { 801 Log.w(TAG, "Remove the currently selected bubble without selecting a new one."); 802 } 803 mBubbleAnimator = new BubbleAnimator(mIconSize, mExpandedBarIconsSpacing, 804 getChildCount(), mBubbleBarLocation.isOnLeft(isLayoutRtl())); 805 BubbleAnimator.Listener listener = new BubbleAnimator.Listener() { 806 807 @Override 808 public void onAnimationEnd() { 809 removeView(removedBubble); 810 updateLayoutParams(); 811 mBubbleAnimator = null; 812 if (onEndRunnable != null) { 813 onEndRunnable.run(); 814 } 815 } 816 817 @Override 818 public void onAnimationCancel() { 819 addedBubble.setScaleX(1); 820 addedBubble.setScaleY(1); 821 removedBubble.setScaleX(0); 822 removedBubble.setScaleY(0); 823 } 824 825 @Override 826 public void onAnimationUpdate(float animatedFraction) { 827 addedBubble.setScaleX(animatedFraction); 828 addedBubble.setScaleY(animatedFraction); 829 removedBubble.setScaleX(1 - animatedFraction); 830 removedBubble.setScaleY(1 - animatedFraction); 831 updateBubblesLayoutProperties(mBubbleBarLocation); 832 invalidate(); 833 } 834 }; 835 mBubbleAnimator.animateNewAndRemoveOld(indexOfCurrentSelectedBubble, 836 indexOfNewlySelectedBubble, indexOfBubbleToRemove, addedIndex, listener); 837 } 838 839 @Override addView(View child, int index, ViewGroup.LayoutParams params)840 public void addView(View child, int index, ViewGroup.LayoutParams params) { 841 super.addView(child, index, params); 842 updateLayoutParams(); 843 updateBubbleAccessibilityStates(); 844 updateContentDescription(); 845 updateDotsAndBadgesIfCollapsed(); 846 } 847 848 /** Removes the given bubble from the bubble bar. */ removeBubble(View bubble)849 public void removeBubble(View bubble) { 850 if (isExpanded()) { 851 final boolean dismissedByDrag = mDraggedBubbleView == bubble; 852 if (dismissedByDrag) { 853 mDismissedByDragBubbleView = mDraggedBubbleView; 854 } 855 boolean removingLastRemainingBubble = getBubbleChildCount() == 1; 856 int bubbleCount = getChildCount(); 857 mBubbleAnimator = new BubbleAnimator(mIconSize, mExpandedBarIconsSpacing, 858 bubbleCount, mBubbleBarLocation.isOnLeft(isLayoutRtl())); 859 BubbleAnimator.Listener listener = new BubbleAnimator.Listener() { 860 861 @Override 862 public void onAnimationEnd() { 863 removeView(bubble); 864 mBubbleAnimator = null; 865 } 866 867 @Override 868 public void onAnimationCancel() { 869 bubble.setScaleX(0); 870 bubble.setScaleY(0); 871 } 872 873 @Override 874 public void onAnimationUpdate(float animatedFraction) { 875 // don't update the scale if this bubble was dismissed by drag 876 if (!dismissedByDrag) { 877 bubble.setScaleX(1 - animatedFraction); 878 bubble.setScaleY(1 - animatedFraction); 879 } 880 updateBubblesLayoutProperties(mBubbleBarLocation); 881 invalidate(); 882 } 883 }; 884 int bubbleIndex = indexOfChild(bubble); 885 BubbleView lastBubble = (BubbleView) getChildAt(bubbleCount - 1); 886 String lastBubbleKey = lastBubble.getBubble().getKey(); 887 boolean removingLastBubble = 888 BubbleBarOverflow.KEY.equals(lastBubbleKey) 889 ? bubbleIndex == bubbleCount - 2 890 : bubbleIndex == bubbleCount - 1; 891 mBubbleAnimator.animateRemovedBubble( 892 indexOfChild(bubble), indexOfChild(mSelectedBubbleView), removingLastBubble, 893 removingLastRemainingBubble, listener); 894 if (removingLastRemainingBubble && mDismissAnimator == null) { 895 createDismissAnimator().start(); 896 } 897 } else { 898 removeView(bubble); 899 } 900 } 901 902 // TODO: (b/283309949) animate it 903 @Override removeView(View view)904 public void removeView(View view) { 905 super.removeView(view); 906 if (view == mSelectedBubbleView) { 907 mSelectedBubbleView = null; 908 mBubbleBarBackground.showArrow(false); 909 } 910 updateLayoutParams(); 911 updateBubbleAccessibilityStates(); 912 updateContentDescription(); 913 mDismissedByDragBubbleView = null; 914 updateDotsAndBadgesIfCollapsed(); 915 } 916 createDismissAnimator()917 private ValueAnimator createDismissAnimator() { 918 ValueAnimator animator = 919 ValueAnimator.ofFloat(0, 1).setDuration(FADE_OUT_BUBBLE_BAR_DURATION_MS); 920 animator.setInterpolator(Interpolators.EMPHASIZED); 921 Runnable onEnd = () -> { 922 mDismissAnimator = null; 923 setAlpha(0); 924 }; 925 addAnimationCallBacks(animator, /* onStart= */ null, onEnd, 926 /* onUpdate= */ anim -> setAlpha(1 - anim.getAnimatedFraction())); 927 mDismissAnimator = animator; 928 return animator; 929 } 930 931 /** Dismisses the bubble bar */ dismiss(Runnable onDismissed)932 public void dismiss(Runnable onDismissed) { 933 if (mDismissAnimator == null) { 934 createDismissAnimator().start(); 935 } 936 addAnimationCallBacks(mDismissAnimator, null, onDismissed, null); 937 } 938 939 /** 940 * Return child views in the order which they are shown on the screen. 941 * <p> 942 * Child views (bubbles) are always ordered based on recency. The most recent bubble is at index 943 * 0. 944 * For example if the child views are (1)(2)(3) then (1) is the most recent bubble and at index 945 * 0.<br> 946 * 947 * How bubbles show up on the screen depends on the bubble bar location. If the bar is on the 948 * left, the most recent bubble is shown on the right. The bubbles from the example above would 949 * be shown as: (3)(2)(1).<br> 950 * 951 * If bubble bar is on the right, then the most recent bubble is on the left. Bubbles from the 952 * example above would be shown as: (1)(2)(3). 953 */ getChildViewsInOnScreenOrder()954 private List<View> getChildViewsInOnScreenOrder() { 955 List<View> childViews = new ArrayList<>(getChildCount()); 956 for (int i = 0; i < getChildCount(); i++) { 957 childViews.add(getChildAt(i)); 958 } 959 if (mBubbleBarLocation.isOnLeft(isLayoutRtl())) { 960 // Visually child views are shown in reverse order when bar is on the left 961 return childViews.reversed(); 962 } 963 return childViews; 964 } 965 updateDotsAndBadgesIfCollapsed()966 private void updateDotsAndBadgesIfCollapsed() { 967 if (isExpanded()) { 968 return; 969 } 970 for (int i = 0; i < getChildCount(); i++) { 971 BubbleView bubbleView = (BubbleView) getChildAt(i); 972 // when we're collapsed, the first bubble should show the badge and the dot if it has 973 // it. the rest of the bubbles should hide their badges and dots. 974 if (i == 0) { 975 bubbleView.showBadge(); 976 if (bubbleView.hasUnseenContent()) { 977 bubbleView.showDotIfNeeded(/* animate= */ true); 978 } else { 979 bubbleView.hideDot(); 980 } 981 } else { 982 bubbleView.hideBadge(); 983 bubbleView.hideDot(); 984 } 985 } 986 } 987 updateLayoutParams()988 private void updateLayoutParams() { 989 LayoutParams lp = (FrameLayout.LayoutParams) getLayoutParams(); 990 lp.height = (int) getBubbleBarExpandedHeight(); 991 lp.width = (int) (mIsBarExpanded ? expandedWidth() : collapsedWidth()); 992 setLayoutParams(lp); 993 } 994 getBubbleBarHeight()995 private float getBubbleBarHeight() { 996 return mIsBarExpanded ? getBubbleBarExpandedHeight() 997 : getBubbleBarCollapsedHeight(); 998 } 999 1000 /** @return the horizontal margin between the bubble bar and the edge of the screen. */ getHorizontalMargin()1001 int getHorizontalMargin() { 1002 LayoutParams lp = (FrameLayout.LayoutParams) getLayoutParams(); 1003 return lp.getMarginEnd(); 1004 } 1005 1006 /** 1007 * Updates the z order, positions, and badge visibility of the bubble views in the bar based 1008 * on the expanded state. 1009 */ updateBubblesLayoutProperties(BubbleBarLocation bubbleBarLocation)1010 private void updateBubblesLayoutProperties(BubbleBarLocation bubbleBarLocation) { 1011 final float widthState = (float) mWidthAnimator.getAnimatedValue(); 1012 final float currentWidth = getWidth(); 1013 final float expandedWidth = expandedWidth(); 1014 final float collapsedWidth = collapsedWidth(); 1015 int childCount = getChildCount(); 1016 final float ty = getBubbleTranslationY(); 1017 final boolean onLeft = bubbleBarLocation.isOnLeft(isLayoutRtl()); 1018 // elevation state is opposite to widthState - when expanded all icons are flat 1019 float elevationState = (1 - widthState); 1020 for (int i = 0; i < childCount; i++) { 1021 BubbleView bv = (BubbleView) getChildAt(i); 1022 if (bv == mDraggedBubbleView || bv == mDismissedByDragBubbleView) { 1023 // Skip the dragged bubble. Its translation is managed by the drag controller. 1024 continue; 1025 } 1026 // Clear out drag translation and offset 1027 bv.setDragTranslationX(0f); 1028 bv.setOffsetX(0f); 1029 1030 if (mBubbleAnimator == null || !mBubbleAnimator.isRunning()) { 1031 // if the bubble animator is running don't set scale here, it will be set by the 1032 // animator 1033 bv.setScaleX(mIconScale); 1034 bv.setScaleY(mIconScale); 1035 } 1036 bv.setTranslationY(ty); 1037 1038 // the position of the bubble when the bar is fully expanded 1039 final float expandedX = getExpandedBubbleTranslationX(i, childCount, onLeft); 1040 // the position of the bubble when the bar is fully collapsed 1041 final float collapsedX = getCollapsedBubbleTranslationX(i, childCount, onLeft); 1042 1043 // slowly animate elevation while keeping correct Z ordering 1044 float fullElevationForChild = (MAX_BUBBLES * mBubbleElevation) - i; 1045 bv.setZ(fullElevationForChild * elevationState); 1046 1047 // only update the dot and badge scale if we're expanding or collapsing 1048 if (mWidthAnimator.isRunning()) { 1049 // The dot for the selected bubble scales in the opposite direction of the expansion 1050 // animation. 1051 bv.showDotIfNeeded(bv == mSelectedBubbleView ? 1 - widthState : widthState); 1052 // The badge for the selected bubble is always at full scale. All other bubbles 1053 // scale according to the expand animation. 1054 bv.setBadgeScale(bv == mSelectedBubbleView ? 1 : widthState); 1055 } 1056 1057 if (mIsBarExpanded) { 1058 // If bar is on the right, account for bubble bar expanding and shifting left 1059 final float expandedBarShift = onLeft ? 0 : currentWidth - expandedWidth; 1060 // where the bubble will end up when the animation ends 1061 final float targetX = expandedX + expandedBarShift; 1062 bv.setTranslationX(widthState * (targetX - collapsedX) + collapsedX); 1063 bv.setVisibility(VISIBLE); 1064 } else { 1065 // If bar is on the right, account for bubble bar expanding and shifting left 1066 final float collapsedBarShift = onLeft ? 0 : currentWidth - collapsedWidth; 1067 final float targetX = collapsedX + collapsedBarShift; 1068 bv.setTranslationX(widthState * (expandedX - targetX) + targetX); 1069 // If we're fully collapsed, hide all bubbles except for the first 2, excluding 1070 // the overflow. 1071 if (widthState == 0) { 1072 if (bv.isOverflow() || i > MAX_VISIBLE_BUBBLES_COLLAPSED - 1) { 1073 bv.setVisibility(INVISIBLE); 1074 } else { 1075 bv.setVisibility(VISIBLE); 1076 } 1077 } 1078 } 1079 } 1080 1081 // update the arrow position 1082 final float collapsedArrowPosition = arrowPositionForSelectedWhenCollapsed( 1083 bubbleBarLocation); 1084 final float expandedArrowPosition = arrowPositionForSelectedWhenExpanded(bubbleBarLocation); 1085 final float interpolatedWidth = 1086 widthState * (expandedWidth - collapsedWidth) + collapsedWidth; 1087 final float arrowPosition; 1088 1089 float interpolatedShift = (expandedArrowPosition - collapsedArrowPosition) * widthState; 1090 if (onLeft) { 1091 arrowPosition = collapsedArrowPosition + interpolatedShift; 1092 } else { 1093 if (mIsBarExpanded) { 1094 arrowPosition = currentWidth - interpolatedWidth + collapsedArrowPosition 1095 + interpolatedShift; 1096 } else { 1097 final float targetPosition = currentWidth - collapsedWidth + collapsedArrowPosition; 1098 arrowPosition = 1099 targetPosition + widthState * (expandedArrowPosition - targetPosition); 1100 } 1101 } 1102 mBubbleBarBackground.setArrowPosition(arrowPosition); 1103 mBubbleBarBackground.setArrowHeightFraction(widthState); 1104 mBubbleBarBackground.setWidth(interpolatedWidth); 1105 mBubbleBarBackground.setBackgroundHeight(getBubbleBarExpandedHeight()); 1106 } 1107 getScaleIconShift()1108 private float getScaleIconShift() { 1109 return (mIconSize - getScaledIconSize()) / 2; 1110 } 1111 getExpandedBubbleTranslationX(int bubbleIndex, int bubbleCount, boolean onLeft)1112 private float getExpandedBubbleTranslationX(int bubbleIndex, int bubbleCount, boolean onLeft) { 1113 if (bubbleIndex < 0 || bubbleIndex >= bubbleCount) { 1114 return 0; 1115 } 1116 final float iconAndSpacing = getScaledIconSize() + mExpandedBarIconsSpacing; 1117 float translationX; 1118 if (mBubbleAnimator != null && mBubbleAnimator.isRunning()) { 1119 return mBubbleAnimator.getBubbleTranslationX(bubbleIndex) + mBubbleBarPadding; 1120 } else if (onLeft) { 1121 translationX = mBubbleBarPadding + (bubbleCount - bubbleIndex - 1) * iconAndSpacing; 1122 } else { 1123 translationX = mBubbleBarPadding + bubbleIndex * iconAndSpacing; 1124 } 1125 return translationX - getScaleIconShift(); 1126 } 1127 getCollapsedBubbleTranslationX(int bubbleIndex, int childCount, boolean onLeft)1128 private float getCollapsedBubbleTranslationX(int bubbleIndex, int childCount, boolean onLeft) { 1129 if (bubbleIndex < 0 || bubbleIndex >= childCount) { 1130 return 0; 1131 } 1132 float translationX; 1133 if (onLeft) { 1134 // Shift the first bubble only if there are more bubbles 1135 if (bubbleIndex == 0 && getBubbleChildCount() >= MAX_VISIBLE_BUBBLES_COLLAPSED) { 1136 translationX = mIconOverlapAmount; 1137 } else { 1138 translationX = 0f; 1139 } 1140 } else { 1141 // when the bar is on the right, the first bubble always has translation 0. the only 1142 // case where another bubble has translation 0 is when we only have 1 bubble and the 1143 // overflow. otherwise all other bubbles should be shifted by the overlap amount. 1144 if (bubbleIndex == 0 || getBubbleChildCount() == 1) { 1145 translationX = 0f; 1146 } else { 1147 translationX = mIconOverlapAmount; 1148 } 1149 } 1150 return mBubbleBarPadding + translationX - getScaleIconShift(); 1151 } 1152 getBubbleTranslationY()1153 private float getBubbleTranslationY() { 1154 float viewBottom = mBubbleBarBounds.height() + (isExpanded() ? mPointerSize : 0); 1155 float bubbleBarAnimatedTop = viewBottom - getBubbleBarHeight(); 1156 // When translating X & Y the scale is ignored, so need to deduct it from the translations 1157 return mBubbleOffsetY + bubbleBarAnimatedTop + mBubbleBarPadding - getScaleIconShift(); 1158 } 1159 1160 /** 1161 * Reorders the views to match the provided list. 1162 */ reorder(List<BubbleView> viewOrder)1163 public void reorder(List<BubbleView> viewOrder) { 1164 if (isExpanded() || mWidthAnimator.isRunning()) { 1165 mReorderRunnable = () -> doReorder(viewOrder); 1166 } else { 1167 doReorder(viewOrder); 1168 } 1169 } 1170 1171 // TODO: (b/273592694) animate it doReorder(List<BubbleView> viewOrder)1172 private void doReorder(List<BubbleView> viewOrder) { 1173 if (!isExpanded()) { 1174 for (int i = 0; i < viewOrder.size(); i++) { 1175 View child = viewOrder.get(i); 1176 // this child view may have already been removed so verify that it still exists 1177 // before reordering it, otherwise it will be re-added. 1178 int indexOfChild = indexOfChild(child); 1179 if (child != null && indexOfChild >= 0) { 1180 removeViewInLayout(child); 1181 addViewInLayout(child, i, child.getLayoutParams()); 1182 } 1183 } 1184 updateBubblesLayoutProperties(mBubbleBarLocation); 1185 updateContentDescription(); 1186 updateDotsAndBadgesIfCollapsed(); 1187 } 1188 } 1189 setUpdateSelectedBubbleAfterCollapse( Consumer<String> updateSelectedBubbleAfterCollapse)1190 public void setUpdateSelectedBubbleAfterCollapse( 1191 Consumer<String> updateSelectedBubbleAfterCollapse) { 1192 mUpdateSelectedBubbleAfterCollapse = updateSelectedBubbleAfterCollapse; 1193 } 1194 setController(Controller controller)1195 void setController(Controller controller) { 1196 mController = controller; 1197 } 1198 1199 /** 1200 * Sets which bubble view should be shown as selected. 1201 */ setSelectedBubble(BubbleView view)1202 public void setSelectedBubble(BubbleView view) { 1203 BubbleView previouslySelectedBubble = mSelectedBubbleView; 1204 mSelectedBubbleView = view; 1205 mBubbleBarBackground.showArrow(view != null); 1206 1207 // if bubbles are being animated, the arrow position will be set as part of the animation 1208 if (mBubbleAnimator == null) { 1209 updateArrowForSelected(previouslySelectedBubble != null); 1210 } 1211 if (view != null) { 1212 if (isExpanded()) { 1213 view.markSeen(); 1214 } else { 1215 // when collapsed, the selected bubble should show the dot if it has it 1216 view.showDotIfNeeded(/* animate= */ true); 1217 } 1218 } 1219 } 1220 1221 /** 1222 * Sets the dragged bubble view to correctly apply Z order. Dragged view should appear on top 1223 */ setDraggedBubble(@ullable BubbleView view)1224 public void setDraggedBubble(@Nullable BubbleView view) { 1225 if (mDraggedBubbleView != null) { 1226 mDraggedBubbleView.setZ(0); 1227 } 1228 mDraggedBubbleView = view; 1229 if (view != null) { 1230 view.setZ(mDragElevation); 1231 // we started dragging a bubble. reset the bubble that was previously dismissed by drag 1232 mDismissedByDragBubbleView = null; 1233 } 1234 setIsDragging(view != null); 1235 } 1236 1237 /** 1238 * Update the arrow position to match the selected bubble. 1239 * 1240 * @param shouldAnimate whether or not to animate the arrow. If the bar was just expanded, this 1241 * should be set to {@code false}. Otherwise set this to {@code true}. 1242 */ updateArrowForSelected(boolean shouldAnimate)1243 private void updateArrowForSelected(boolean shouldAnimate) { 1244 if (mSelectedBubbleView == null) { 1245 Log.w(TAG, "trying to update selection arrow without a selected view!"); 1246 return; 1247 } 1248 // Find the center of the bubble when it's expanded, set the arrow position to it. 1249 final float tx = arrowPositionForSelectedWhenExpanded(mBubbleBarLocation); 1250 final float currentArrowPosition = mBubbleBarBackground.getArrowPositionX(); 1251 if (tx == currentArrowPosition) { 1252 // arrow position remains unchanged 1253 return; 1254 } 1255 if (shouldAnimate && currentArrowPosition > expandedWidth()) { 1256 Log.d(TAG, "arrow out of bounds of expanded view, skip animation"); 1257 shouldAnimate = false; 1258 } 1259 if (shouldAnimate) { 1260 ValueAnimator animator = ValueAnimator.ofFloat(currentArrowPosition, tx); 1261 animator.setDuration(ARROW_POSITION_ANIMATION_DURATION_MS); 1262 animator.addUpdateListener(animation -> { 1263 float x = (float) animation.getAnimatedValue(); 1264 mBubbleBarBackground.setArrowPosition(x); 1265 invalidate(); 1266 }); 1267 animator.start(); 1268 } else { 1269 mBubbleBarBackground.setArrowPosition(tx); 1270 invalidate(); 1271 } 1272 } 1273 arrowPositionForSelectedWhenExpanded(BubbleBarLocation bubbleBarLocation)1274 private float arrowPositionForSelectedWhenExpanded(BubbleBarLocation bubbleBarLocation) { 1275 if (mBubbleAnimator != null && mBubbleAnimator.isRunning()) { 1276 return mBubbleAnimator.getArrowPosition() + mBubbleBarPadding; 1277 } 1278 final int index = indexOfChild(mSelectedBubbleView); 1279 final float selectedBubbleTranslationX = getExpandedBubbleTranslationX( 1280 index, getChildCount(), bubbleBarLocation.isOnLeft(isLayoutRtl())); 1281 return selectedBubbleTranslationX + mIconSize / 2f; 1282 } 1283 arrowPositionForSelectedWhenCollapsed(BubbleBarLocation bubbleBarLocation)1284 private float arrowPositionForSelectedWhenCollapsed(BubbleBarLocation bubbleBarLocation) { 1285 final int index = indexOfChild(mSelectedBubbleView); 1286 final int bubblePosition; 1287 if (bubbleBarLocation.isOnLeft(isLayoutRtl())) { 1288 // Bubble positions are reversed. First bubble may be shifted, if there are more 1289 // bubbles than the current bubble and overflow. 1290 bubblePosition = index == 0 && getChildCount() > MAX_VISIBLE_BUBBLES_COLLAPSED ? 1 : 0; 1291 } else { 1292 bubblePosition = index >= MAX_VISIBLE_BUBBLES_COLLAPSED 1293 ? MAX_VISIBLE_BUBBLES_COLLAPSED - 1 : index; 1294 } 1295 return mBubbleBarPadding + bubblePosition * (mIconOverlapAmount) + getScaledIconSize() / 2f; 1296 } 1297 1298 @Override setOnClickListener(View.OnClickListener listener)1299 public void setOnClickListener(View.OnClickListener listener) { 1300 mOnClickListener = listener; 1301 setOrUnsetClickListener(); 1302 } 1303 1304 /** 1305 * The click listener used for the bubble view gets added / removed depending on whether 1306 * the bar is expanded or collapsed, this updates whether the listener is set based on state. 1307 */ setOrUnsetClickListener()1308 private void setOrUnsetClickListener() { 1309 super.setOnClickListener(mIsBarExpanded ? null : mOnClickListener); 1310 } 1311 1312 /** 1313 * Sets whether the bubble bar is expanded or collapsed. 1314 */ setExpanded(boolean isBarExpanded)1315 public void setExpanded(boolean isBarExpanded) { 1316 if (mIsBarExpanded != isBarExpanded) { 1317 mIsBarExpanded = isBarExpanded; 1318 updateArrowForSelected(/* shouldAnimate= */ false); 1319 setOrUnsetClickListener(); 1320 mWidthAnimator = createExpansionAnimator(isBarExpanded); 1321 mWidthAnimator.start(); 1322 updateBubbleAccessibilityStates(); 1323 announceExpandedStateChange(); 1324 } 1325 } 1326 1327 /** 1328 * Returns whether the bubble bar is expanded. 1329 */ isExpanded()1330 public boolean isExpanded() { 1331 return mIsBarExpanded; 1332 } 1333 1334 /** 1335 * Returns whether the bubble bar is expanding. 1336 */ isExpanding()1337 public boolean isExpanding() { 1338 return mWidthAnimator.isRunning() && mIsBarExpanded; 1339 } 1340 1341 /** 1342 * Get width of the bubble bar as if it would be expanded. 1343 * 1344 * @return width of the bubble bar in its expanded state, regardless of current width 1345 */ expandedWidth()1346 public float expandedWidth() { 1347 final int childCount = getChildCount(); 1348 final float horizontalPadding = 2 * mBubbleBarPadding; 1349 if (mBubbleAnimator != null && mBubbleAnimator.isRunning()) { 1350 return mBubbleAnimator.getExpandedWidth() + horizontalPadding; 1351 } 1352 // spaces amount is less than child count by 1, or 0 if no child views 1353 final float totalSpace = Math.max(childCount - 1, 0) * mExpandedBarIconsSpacing; 1354 final float totalIconSize = childCount * getScaledIconSize(); 1355 return totalIconSize + totalSpace + horizontalPadding; 1356 } 1357 1358 /** 1359 * Get width of the bubble bar if it is collapsed 1360 */ collapsedWidth()1361 float collapsedWidth() { 1362 final int bubbleChildCount = getBubbleChildCount(); 1363 final float horizontalPadding = 2 * mBubbleBarPadding; 1364 // If there are more than 2 bubbles, the first 2 should be visible when collapsed, 1365 // excluding the overflow. 1366 return bubbleChildCount >= MAX_VISIBLE_BUBBLES_COLLAPSED 1367 ? getCollapsedWidthWithMaxVisibleBubbles() 1368 : getScaledIconSize() + horizontalPadding; 1369 } 1370 getCollapsedWidthWithMaxVisibleBubbles()1371 float getCollapsedWidthWithMaxVisibleBubbles() { 1372 return getScaledIconSize() + mIconOverlapAmount + 2 * mBubbleBarPadding; 1373 } 1374 getCollapsedWidthForIconSizeAndPadding(int iconSize, int bubbleBarPadding)1375 float getCollapsedWidthForIconSizeAndPadding(int iconSize, int bubbleBarPadding) { 1376 final int bubbleChildCount = Math.min(getBubbleChildCount(), MAX_VISIBLE_BUBBLES_COLLAPSED); 1377 if (bubbleChildCount == 0) return 0; 1378 final int spacesCount = bubbleChildCount - 1; 1379 final float horizontalPadding = 2 * bubbleBarPadding; 1380 return iconSize * bubbleChildCount + mIconOverlapAmount * spacesCount + horizontalPadding; 1381 } 1382 1383 /** Returns the child count excluding the overflow if it's present. */ getBubbleChildCount()1384 int getBubbleChildCount() { 1385 return hasOverflow() ? getChildCount() - 1 : getChildCount(); 1386 } 1387 getBubbleBarExpandedHeight()1388 private float getBubbleBarExpandedHeight() { 1389 return getBubbleBarCollapsedHeight() + mPointerSize; 1390 } 1391 getArrowHeight()1392 float getArrowHeight() { 1393 return mPointerSize; 1394 } 1395 getBubbleBarCollapsedHeight()1396 float getBubbleBarCollapsedHeight() { 1397 // the pointer is invisible when collapsed 1398 return getScaledIconSize() + mBubbleBarPadding * 2; 1399 } 1400 1401 /** 1402 * Returns whether the given MotionEvent, *in screen coordinates*, is within bubble bar 1403 * touch bounds. 1404 */ isEventOverAnyItem(MotionEvent ev)1405 public boolean isEventOverAnyItem(MotionEvent ev) { 1406 if (getVisibility() == VISIBLE) { 1407 getBoundsOnScreen(mTempRect); 1408 return mTempRect.contains((int) ev.getX(), (int) ev.getY()); 1409 } 1410 return false; 1411 } 1412 1413 @Override onInterceptTouchEvent(MotionEvent ev)1414 public boolean onInterceptTouchEvent(MotionEvent ev) { 1415 mController.onBubbleBarTouched(); 1416 if (!mIsBarExpanded) { 1417 // When the bar is collapsed, all taps on it should expand it. 1418 return true; 1419 } 1420 return super.onInterceptTouchEvent(ev); 1421 } 1422 hasOverflow()1423 private boolean hasOverflow() { 1424 // Overflow is always the last bubble 1425 View lastChild = getChildAt(getChildCount() - 1); 1426 if (lastChild instanceof BubbleView bubbleView) { 1427 return bubbleView.getBubble() instanceof BubbleBarOverflow; 1428 } 1429 return false; 1430 } 1431 updateBubbleAccessibilityStates()1432 private void updateBubbleAccessibilityStates() { 1433 if (mIsBarExpanded) { 1434 // Bar is expanded, focus on the bubbles 1435 setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_NO); 1436 1437 // Set up a11y navigation order. Get list of child views in the order they are shown 1438 // on screen. And use that to set up navigation so that swiping left focuses the view 1439 // on the left and swiping right focuses view on the right. 1440 View prevChild = null; 1441 for (View childView : getChildViewsInOnScreenOrder()) { 1442 childView.setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_YES); 1443 childView.setFocusable(true); 1444 final View finalPrevChild = prevChild; 1445 // Always need to set a new delegate to clear out any previous. 1446 childView.setAccessibilityDelegate(new AccessibilityDelegate() { 1447 @Override 1448 public void onInitializeAccessibilityNodeInfo(View host, 1449 AccessibilityNodeInfo info) { 1450 super.onInitializeAccessibilityNodeInfo(host, info); 1451 if (finalPrevChild != null) { 1452 info.setTraversalAfter(finalPrevChild); 1453 } 1454 } 1455 }); 1456 prevChild = childView; 1457 } 1458 } else { 1459 // Bar is collapsed, only focus on the bar 1460 setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_YES); 1461 for (int i = 0; i < getChildCount(); i++) { 1462 View childView = getChildAt(i); 1463 childView.setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_NO); 1464 childView.setFocusable(false); 1465 } 1466 } 1467 } 1468 updateContentDescription()1469 private void updateContentDescription() { 1470 View firstChild = getChildAt(0); 1471 CharSequence contentDesc = firstChild != null ? firstChild.getContentDescription() : ""; 1472 1473 // Don't count overflow if it exists 1474 int bubbleCount = getChildCount() - (hasOverflow() ? 1 : 0); 1475 if (bubbleCount > 1) { 1476 contentDesc = getResources().getString(R.string.bubble_bar_description_multiple_bubbles, 1477 contentDesc, bubbleCount - 1); 1478 } 1479 setContentDescription(contentDesc); 1480 } 1481 announceExpandedStateChange()1482 private void announceExpandedStateChange() { 1483 final CharSequence selectedBubbleContentDesc; 1484 if (mSelectedBubbleView != null) { 1485 selectedBubbleContentDesc = mSelectedBubbleView.getContentDescription(); 1486 } else { 1487 selectedBubbleContentDesc = getResources().getString( 1488 R.string.bubble_bar_bubble_fallback_description); 1489 } 1490 1491 final String msg; 1492 if (mIsBarExpanded) { 1493 msg = getResources().getString(R.string.bubble_bar_accessibility_announce_expand, 1494 selectedBubbleContentDesc); 1495 } else { 1496 msg = getResources().getString(R.string.bubble_bar_accessibility_announce_collapse, 1497 selectedBubbleContentDesc); 1498 } 1499 announceForAccessibility(msg); 1500 } 1501 isIconSizeOrPaddingUpdated(float newIconSize, float newBubbleBarPadding)1502 private boolean isIconSizeOrPaddingUpdated(float newIconSize, float newBubbleBarPadding) { 1503 return isIconSizeUpdated(newIconSize) || isPaddingUpdated(newBubbleBarPadding); 1504 } 1505 isIconSizeUpdated(float newIconSize)1506 private boolean isIconSizeUpdated(float newIconSize) { 1507 return Float.compare(mIconSize, newIconSize) != 0; 1508 } 1509 isPaddingUpdated(float newBubbleBarPadding)1510 private boolean isPaddingUpdated(float newBubbleBarPadding) { 1511 return Float.compare(mBubbleBarPadding, newBubbleBarPadding) != 0; 1512 } 1513 addAnimationCallBacks(@onNull ValueAnimator animator, @Nullable Runnable onStart, @Nullable Runnable onEnd, @Nullable ValueAnimator.AnimatorUpdateListener onUpdate)1514 private void addAnimationCallBacks(@NonNull ValueAnimator animator, 1515 @Nullable Runnable onStart, 1516 @Nullable Runnable onEnd, 1517 @Nullable ValueAnimator.AnimatorUpdateListener onUpdate) { 1518 if (onUpdate != null) animator.addUpdateListener(onUpdate); 1519 animator.addListener(new Animator.AnimatorListener() { 1520 @Override 1521 public void onAnimationCancel(Animator animator) { 1522 1523 } 1524 1525 @Override 1526 public void onAnimationStart(Animator animator) { 1527 if (onStart != null) onStart.run(); 1528 } 1529 1530 @Override 1531 public void onAnimationEnd(Animator animator) { 1532 if (onEnd != null) onEnd.run(); 1533 } 1534 1535 @Override 1536 public void onAnimationRepeat(Animator animator) { 1537 1538 } 1539 }); 1540 } 1541 1542 /** Dumps the current state of BubbleBarView. */ dump(PrintWriter pw)1543 public void dump(PrintWriter pw) { 1544 pw.println("BubbleBarView state:"); 1545 pw.println(" visibility: " + getVisibility()); 1546 pw.println(" alpha: " + getAlpha()); 1547 pw.println(" translationY: " + getTranslationY()); 1548 pw.println(" childCount: " + getChildCount()); 1549 pw.println(" hasOverflow: " + hasOverflow()); 1550 for (BubbleView bubbleView: getBubbles()) { 1551 BubbleBarItem bubble = bubbleView.getBubble(); 1552 String key = bubble == null ? "null" : bubble.getKey(); 1553 pw.println(" bubble key: " + key); 1554 } 1555 pw.println(" isExpanded: " + isExpanded()); 1556 if (mBubbleAnimator != null) { 1557 pw.println(" mBubbleAnimator.isRunning(): " + mBubbleAnimator.isRunning()); 1558 pw.println(" mBubbleAnimator is null"); 1559 } 1560 pw.println(" mDragging: " + mDragging); 1561 } 1562 getBubbles()1563 private List<BubbleView> getBubbles() { 1564 List<BubbleView> bubbles = new ArrayList<>(); 1565 for (int i = 0; i < getChildCount(); i++) { 1566 View child = getChildAt(i); 1567 if (child instanceof BubbleView bubble) { 1568 bubbles.add(bubble); 1569 } 1570 } 1571 return bubbles; 1572 } 1573 1574 /** Creates an animator based on the expanding or collapsing action. */ createExpansionAnimator(boolean expanding)1575 private ValueAnimator createExpansionAnimator(boolean expanding) { 1576 float startValue = expanding ? 0 : 1; 1577 if ((mWidthAnimator != null && mWidthAnimator.isRunning())) { 1578 startValue = (float) mWidthAnimator.getAnimatedValue(); 1579 mWidthAnimator.cancel(); 1580 } 1581 float endValue = expanding ? 1 : 0; 1582 ValueAnimator animator = ValueAnimator.ofFloat(startValue, endValue); 1583 animator.setDuration(WIDTH_ANIMATION_DURATION_MS); 1584 animator.setInterpolator(Interpolators.EMPHASIZED); 1585 addAnimationCallBacks(animator, 1586 /* onStart= */ () -> mBubbleBarBackground.showArrow(true), 1587 /* onEnd= */ () -> { 1588 mBubbleBarBackground.showArrow(mIsBarExpanded); 1589 if (!mIsBarExpanded && mReorderRunnable != null) { 1590 mReorderRunnable.run(); 1591 mReorderRunnable = null; 1592 } 1593 // If the bar was just collapsed and the overflow was the last bubble that was 1594 // selected, set the first bubble as selected. 1595 if (!mIsBarExpanded && mUpdateSelectedBubbleAfterCollapse != null 1596 && mSelectedBubbleView != null 1597 && mSelectedBubbleView.getBubble() instanceof BubbleBarOverflow) { 1598 BubbleView firstBubble = (BubbleView) getChildAt(0); 1599 mUpdateSelectedBubbleAfterCollapse.accept(firstBubble.getBubble().getKey()); 1600 } 1601 // If the bar was just expanded, remove the dot from the selected bubble. 1602 if (mIsBarExpanded && mSelectedBubbleView != null) { 1603 mSelectedBubbleView.markSeen(); 1604 } 1605 updateLayoutParams(); 1606 }, 1607 /* onUpdate= */ anim -> { 1608 updateBubblesLayoutProperties(mBubbleBarLocation); 1609 invalidate(); 1610 }); 1611 return animator; 1612 } 1613 1614 /** 1615 * Returns the distance between the top left corner of the bubble bar to the center of the dot 1616 * of the selected bubble. 1617 */ getSelectedBubbleDotDistanceFromTopLeft()1618 PointF getSelectedBubbleDotDistanceFromTopLeft() { 1619 if (mSelectedBubbleView == null) { 1620 return new PointF(0, 0); 1621 } 1622 final int indexOfSelectedBubble = indexOfChild(mSelectedBubbleView); 1623 final boolean onLeft = mBubbleBarLocation.isOnLeft(isLayoutRtl()); 1624 final float selectedBubbleTx = isExpanded() 1625 ? getExpandedBubbleTranslationX(indexOfSelectedBubble, getChildCount(), onLeft) 1626 : getCollapsedBubbleTranslationX(indexOfSelectedBubble, getChildCount(), onLeft); 1627 PointF selectedBubbleDotCenter = mSelectedBubbleView.getDotCenter(); 1628 1629 return new PointF( 1630 selectedBubbleTx + selectedBubbleDotCenter.x, 1631 mBubbleBarPadding + mPointerSize + selectedBubbleDotCenter.y); 1632 } 1633 getSelectedBubbleDotColor()1634 int getSelectedBubbleDotColor() { 1635 return mSelectedBubbleView == null ? 0 : mSelectedBubbleView.getDotColor(); 1636 } 1637 getPointerSize()1638 int getPointerSize() { 1639 return mPointerSize; 1640 } 1641 getBubbleElevation()1642 float getBubbleElevation() { 1643 return mBubbleElevation; 1644 } 1645 1646 /** Interface for BubbleBarView to communicate with its controller. */ 1647 interface Controller { 1648 1649 /** Returns the translation Y that the bubble bar should have. */ getBubbleBarTranslationY()1650 float getBubbleBarTranslationY(); 1651 1652 /** Notifies the controller that the bubble bar was touched. */ onBubbleBarTouched()1653 void onBubbleBarTouched(); 1654 1655 /** Requests the controller to expand bubble bar */ expandBubbleBar()1656 void expandBubbleBar(); 1657 1658 /** Requests the controller to dismiss the bubble bar */ dismissBubbleBar()1659 void dismissBubbleBar(); 1660 1661 /** Requests the controller to update bubble bar location to the given value */ updateBubbleBarLocation(BubbleBarLocation location, @BubbleBarLocation.UpdateSource int source)1662 void updateBubbleBarLocation(BubbleBarLocation location, 1663 @BubbleBarLocation.UpdateSource int source); 1664 1665 /** Notifies the controller that bubble bar is being dragged */ setIsDragging(boolean dragging)1666 void setIsDragging(boolean dragging); 1667 } 1668 } 1669