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