• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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