d7fe32ab8e
Create a new API to animate bubble bar position. Animating it will not update its laid out location. To update bubble bar laid out location, BubbleBarUpdate can be used. Applying a location change from BubbleBarUpdate no longer animates the change. Bug: 330585402 Flag: ACONFIG com.android.wm.shell.enable_bubble_bar DEVELOPMENT Test: long press bubble bar and drag it to left and right Change-Id: I2572da4c063fc8e07cf07c4303778d343baa4ec4
850 lines
35 KiB
Java
850 lines
35 KiB
Java
/*
|
|
* Copyright (C) 2023 The Android Open Source Project
|
|
*
|
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
* you may not use this file except in compliance with the License.
|
|
* You may obtain a copy of the License at
|
|
*
|
|
* http://www.apache.org/licenses/LICENSE-2.0
|
|
*
|
|
* Unless required by applicable law or agreed to in writing, software
|
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
* See the License for the specific language governing permissions and
|
|
* limitations under the License.
|
|
*/
|
|
package com.android.launcher3.taskbar.bubbles;
|
|
|
|
import static com.android.app.animation.Interpolators.EMPHASIZED_ACCELERATE;
|
|
import static com.android.launcher3.LauncherAnimUtils.VIEW_TRANSLATE_X;
|
|
|
|
import android.animation.Animator;
|
|
import android.animation.AnimatorListenerAdapter;
|
|
import android.animation.AnimatorSet;
|
|
import android.animation.ObjectAnimator;
|
|
import android.animation.ValueAnimator;
|
|
import android.annotation.Nullable;
|
|
import android.annotation.SuppressLint;
|
|
import android.content.Context;
|
|
import android.graphics.PointF;
|
|
import android.graphics.Rect;
|
|
import android.util.AttributeSet;
|
|
import android.util.LayoutDirection;
|
|
import android.util.Log;
|
|
import android.view.Gravity;
|
|
import android.view.MotionEvent;
|
|
import android.view.View;
|
|
import android.view.ViewGroup;
|
|
import android.widget.FrameLayout;
|
|
|
|
import androidx.dynamicanimation.animation.SpringForce;
|
|
|
|
import com.android.launcher3.R;
|
|
import com.android.launcher3.anim.SpringAnimationBuilder;
|
|
import com.android.wm.shell.common.bubbles.BubbleBarLocation;
|
|
|
|
import java.util.List;
|
|
import java.util.function.Consumer;
|
|
|
|
/**
|
|
* The view that holds all the bubble views. Modifying this view should happen through
|
|
* {@link BubbleBarViewController}. Updates to the bubbles themselves (adds, removes, updates,
|
|
* selection) should happen through {@link BubbleBarController} which is the source of truth
|
|
* for state information about the bubbles.
|
|
* <p>
|
|
* The bubble bar has a couple of visual states:
|
|
* - stashed as a handle
|
|
* - unstashed but collapsed, in this state the bar is showing but the bubbles are stacked within it
|
|
* - unstashed and expanded, in this state the bar is showing and the bubbles are shown in a row
|
|
* with one of the bubbles being selected. Additionally, WMShell will display the expanded bubble
|
|
* view above the bar.
|
|
* <p>
|
|
* The bubble bar has some behavior related to taskbar:
|
|
* - When taskbar is unstashed, bubble bar will also become unstashed (but in its "collapsed"
|
|
* state)
|
|
* - When taskbar is stashed, bubble bar will also become stashed (unless bubble bar is in its
|
|
* "expanded" state)
|
|
* - When bubble bar is in its "expanded" state, taskbar becomes stashed
|
|
* <p>
|
|
* If there are no bubbles, the bubble bar and bubble stashed handle are not shown. Additionally
|
|
* the bubble bar and stashed handle are not shown on lockscreen.
|
|
* <p>
|
|
* When taskbar is in persistent or 3 button nav mode, the bubble bar is not available, and instead
|
|
* the bubbles are shown fully by WMShell in their floating mode.
|
|
*/
|
|
public class BubbleBarView extends FrameLayout {
|
|
|
|
private static final String TAG = BubbleBarView.class.getSimpleName();
|
|
|
|
// TODO: (b/273594744) calculate the amount of space we have and base the max on that
|
|
// if it's smaller than 5.
|
|
private static final int MAX_BUBBLES = 5;
|
|
private static final int ARROW_POSITION_ANIMATION_DURATION_MS = 200;
|
|
private static final int WIDTH_ANIMATION_DURATION_MS = 200;
|
|
|
|
private static final long FADE_OUT_ANIM_ALPHA_DURATION_MS = 50L;
|
|
private static final long FADE_OUT_ANIM_ALPHA_DELAY_MS = 50L;
|
|
private static final long FADE_OUT_ANIM_POSITION_DURATION_MS = 100L;
|
|
// During fade out animation we shift the bubble bar 1/80th of the screen width
|
|
private static final float FADE_OUT_ANIM_POSITION_SHIFT = 1 / 80f;
|
|
|
|
private static final long FADE_IN_ANIM_ALPHA_DURATION_MS = 100L;
|
|
// Use STIFFNESS_MEDIUMLOW which is not defined in the API constants
|
|
private static final float FADE_IN_ANIM_POSITION_SPRING_STIFFNESS = 400f;
|
|
// During fade in animation we shift the bubble bar 1/60th of the screen width
|
|
private static final float FADE_IN_ANIM_POSITION_SHIFT = 1 / 60f;
|
|
|
|
private final BubbleBarBackground mBubbleBarBackground;
|
|
|
|
private boolean mIsAnimatingNewBubble = false;
|
|
|
|
/**
|
|
* The current bounds of all the bubble bar. Note that these bounds may not account for
|
|
* translation. The bounds should be retrieved using {@link #getBubbleBarBounds()} which
|
|
* updates the bounds and accounts for translation.
|
|
*/
|
|
private final Rect mBubbleBarBounds = new Rect();
|
|
// The amount the bubbles overlap when they are stacked in the bubble bar
|
|
private final float mIconOverlapAmount;
|
|
// The spacing between the bubbles when bubble bar is expanded
|
|
private final float mExpandedBarIconsSpacing;
|
|
// The spacing between the bubbles and the borders of the bubble bar
|
|
private float mBubbleBarPadding;
|
|
// The size of a bubble in the bar
|
|
private float mIconSize;
|
|
// The elevation of the bubbles within the bar
|
|
private final float mBubbleElevation;
|
|
private final float mDragElevation;
|
|
private final int mPointerSize;
|
|
|
|
// Whether the bar is expanded (i.e. the bubble activity is being displayed).
|
|
private boolean mIsBarExpanded = false;
|
|
// The currently selected bubble view.
|
|
@Nullable
|
|
private BubbleView mSelectedBubbleView;
|
|
private BubbleBarLocation mBubbleBarLocation = BubbleBarLocation.DEFAULT;
|
|
// The click listener when the bubble bar is collapsed.
|
|
private View.OnClickListener mOnClickListener;
|
|
|
|
private final Rect mTempRect = new Rect();
|
|
private float mRelativePivotX = 1f;
|
|
private float mRelativePivotY = 1f;
|
|
|
|
// An animator that represents the expansion state of the bubble bar, where 0 corresponds to the
|
|
// collapsed state and 1 to the fully expanded state.
|
|
private final ValueAnimator mWidthAnimator = ValueAnimator.ofFloat(0, 1);
|
|
|
|
@Nullable
|
|
private Animator mBubbleBarLocationAnimator = null;
|
|
|
|
// We don't reorder the bubbles when they are expanded as it could be jarring for the user
|
|
// this runnable will be populated with any reordering of the bubbles that should be applied
|
|
// once they are collapsed.
|
|
@Nullable
|
|
private Runnable mReorderRunnable;
|
|
|
|
@Nullable
|
|
private Consumer<String> mUpdateSelectedBubbleAfterCollapse;
|
|
|
|
private boolean mDragging;
|
|
|
|
@Nullable
|
|
private BubbleView mDraggedBubbleView;
|
|
|
|
private int mPreviousLayoutDirection = LayoutDirection.UNDEFINED;
|
|
|
|
public BubbleBarView(Context context) {
|
|
this(context, null);
|
|
}
|
|
|
|
public BubbleBarView(Context context, AttributeSet attrs) {
|
|
this(context, attrs, 0);
|
|
}
|
|
|
|
public BubbleBarView(Context context, AttributeSet attrs, int defStyleAttr) {
|
|
this(context, attrs, defStyleAttr, 0);
|
|
}
|
|
|
|
public BubbleBarView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
|
|
super(context, attrs, defStyleAttr, defStyleRes);
|
|
setAlpha(0);
|
|
setVisibility(INVISIBLE);
|
|
mIconOverlapAmount = getResources().getDimensionPixelSize(R.dimen.bubblebar_icon_overlap);
|
|
mBubbleBarPadding = getResources().getDimensionPixelSize(R.dimen.bubblebar_icon_spacing);
|
|
mIconSize = getResources().getDimensionPixelSize(R.dimen.bubblebar_icon_size);
|
|
mExpandedBarIconsSpacing = getResources().getDimensionPixelSize(
|
|
R.dimen.bubblebar_expanded_icon_spacing);
|
|
mBubbleElevation = getResources().getDimensionPixelSize(R.dimen.bubblebar_icon_elevation);
|
|
mDragElevation = getResources().getDimensionPixelSize(R.dimen.bubblebar_drag_elevation);
|
|
mPointerSize = getResources()
|
|
.getDimensionPixelSize(R.dimen.bubblebar_pointer_visible_size);
|
|
|
|
setClipToPadding(false);
|
|
|
|
mBubbleBarBackground = new BubbleBarBackground(context, getBubbleBarExpandedHeight());
|
|
setBackgroundDrawable(mBubbleBarBackground);
|
|
|
|
mWidthAnimator.setDuration(WIDTH_ANIMATION_DURATION_MS);
|
|
mWidthAnimator.addUpdateListener(animation -> {
|
|
updateChildrenRenderNodeProperties(mBubbleBarLocation);
|
|
invalidate();
|
|
});
|
|
mWidthAnimator.addListener(new Animator.AnimatorListener() {
|
|
@Override
|
|
public void onAnimationCancel(Animator animation) {
|
|
}
|
|
|
|
@Override
|
|
public void onAnimationEnd(Animator animation) {
|
|
mBubbleBarBackground.showArrow(mIsBarExpanded);
|
|
if (!mIsBarExpanded && mReorderRunnable != null) {
|
|
mReorderRunnable.run();
|
|
mReorderRunnable = null;
|
|
}
|
|
// If the bar was just collapsed and the overflow was the last bubble that was
|
|
// selected, set the first bubble as selected.
|
|
if (!mIsBarExpanded && mUpdateSelectedBubbleAfterCollapse != null
|
|
&& mSelectedBubbleView != null
|
|
&& mSelectedBubbleView.getBubble() instanceof BubbleBarOverflow) {
|
|
BubbleView firstBubble = (BubbleView) getChildAt(0);
|
|
mUpdateSelectedBubbleAfterCollapse.accept(firstBubble.getBubble().getKey());
|
|
}
|
|
updateWidth();
|
|
}
|
|
|
|
@Override
|
|
public void onAnimationRepeat(Animator animation) {
|
|
}
|
|
|
|
@Override
|
|
public void onAnimationStart(Animator animation) {
|
|
mBubbleBarBackground.showArrow(true);
|
|
}
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Sets new icon size and spacing between icons and bubble bar borders.
|
|
*
|
|
* @param newIconSize new icon size
|
|
* @param spacing spacing between icons and bubble bar borders.
|
|
*/
|
|
// TODO(b/335575529): animate bubble bar icons size change
|
|
public void setIconSizeAndPadding(float newIconSize, float spacing) {
|
|
// TODO(b/335457839): handle new bubble animation during the size change
|
|
mBubbleBarPadding = spacing;
|
|
mIconSize = newIconSize;
|
|
int childCount = getChildCount();
|
|
for (int i = 0; i < childCount; i++) {
|
|
View childView = getChildAt(i);
|
|
FrameLayout.LayoutParams params = (LayoutParams) childView.getLayoutParams();
|
|
params.height = (int) mIconSize;
|
|
params.width = (int) mIconSize;
|
|
childView.setLayoutParams(params);
|
|
}
|
|
mBubbleBarBackground.setHeight(getBubbleBarExpandedHeight());
|
|
updateLayoutParams();
|
|
}
|
|
|
|
@Override
|
|
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
|
|
super.onLayout(changed, left, top, right, bottom);
|
|
mBubbleBarBounds.left = left;
|
|
mBubbleBarBounds.top = top + mPointerSize;
|
|
mBubbleBarBounds.right = right;
|
|
mBubbleBarBounds.bottom = bottom;
|
|
|
|
// The bubble bar handle is aligned according to the relative pivot,
|
|
// by default it's aligned to the bottom edge of the screen so scale towards that
|
|
setPivotX(mRelativePivotX * getWidth());
|
|
setPivotY(mRelativePivotY * getHeight());
|
|
|
|
// Position the views
|
|
updateChildrenRenderNodeProperties(mBubbleBarLocation);
|
|
}
|
|
|
|
@Override
|
|
public void onRtlPropertiesChanged(int layoutDirection) {
|
|
if (mBubbleBarLocation == BubbleBarLocation.DEFAULT
|
|
&& mPreviousLayoutDirection != layoutDirection) {
|
|
Log.d(TAG, "BubbleBar RTL properties changed, new layoutDirection=" + layoutDirection
|
|
+ " previous layoutDirection=" + mPreviousLayoutDirection);
|
|
mPreviousLayoutDirection = layoutDirection;
|
|
onBubbleBarLocationChanged();
|
|
}
|
|
}
|
|
|
|
@SuppressLint("RtlHardcoded")
|
|
private void onBubbleBarLocationChanged() {
|
|
final boolean onLeft = mBubbleBarLocation.isOnLeft(isLayoutRtl());
|
|
mBubbleBarBackground.setAnchorLeft(onLeft);
|
|
mRelativePivotX = onLeft ? 0f : 1f;
|
|
if (getLayoutParams() instanceof LayoutParams lp) {
|
|
lp.gravity = Gravity.BOTTOM | (onLeft ? Gravity.LEFT : Gravity.RIGHT);
|
|
setLayoutParams(lp);
|
|
}
|
|
invalidate();
|
|
}
|
|
|
|
/**
|
|
* @return current {@link BubbleBarLocation}
|
|
*/
|
|
public BubbleBarLocation getBubbleBarLocation() {
|
|
return mBubbleBarLocation;
|
|
}
|
|
|
|
/**
|
|
* Update {@link BubbleBarLocation}
|
|
*/
|
|
public void setBubbleBarLocation(BubbleBarLocation bubbleBarLocation) {
|
|
if (mBubbleBarLocationAnimator != null) {
|
|
mBubbleBarLocationAnimator.removeAllListeners();
|
|
mBubbleBarLocationAnimator.cancel();
|
|
mBubbleBarLocationAnimator = null;
|
|
}
|
|
setTranslationX(0f);
|
|
setAlpha(1f);
|
|
if (bubbleBarLocation != mBubbleBarLocation) {
|
|
mBubbleBarLocation = bubbleBarLocation;
|
|
onBubbleBarLocationChanged();
|
|
invalidate();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Set whether this view is being currently being dragged
|
|
*/
|
|
public void setIsDragging(boolean dragging) {
|
|
if (mDragging == dragging) {
|
|
return;
|
|
}
|
|
mDragging = dragging;
|
|
setElevation(dragging ? mDragElevation : mBubbleElevation);
|
|
}
|
|
|
|
/**
|
|
* Get translation for bubble bar when drag is released and it needs to animate back to the
|
|
* resting position.
|
|
* Resting position is based on the supplied location. If the supplied location is different
|
|
* from the internal location that was used to lay out the bubble bar, translation values are
|
|
* calculated to position the bar at the desired location.
|
|
*
|
|
* @param initialTranslation initial bubble bar translation at the start of drag
|
|
* @param location desired location of the bubble bar when drag is released
|
|
* @return point with x and y values representing translation on x and y-axis
|
|
*/
|
|
public PointF getBubbleBarDragReleaseTranslation(PointF initialTranslation,
|
|
BubbleBarLocation location) {
|
|
// Start with the initial translation. Value on y-axis can be reused.
|
|
final PointF dragEndTranslation = new PointF(initialTranslation);
|
|
// Bubble bar is laid out on left or right side of the screen. And the desired new
|
|
// location is on the other side. Calculate x translation value required to shift
|
|
// bubble bar from one side to the other.
|
|
final float shift = getDistanceFromOtherSide();
|
|
if (location.isOnLeft(isLayoutRtl())) {
|
|
// New location is on the left, shift left
|
|
// before -> |......ooo.| after -> |.ooo......|
|
|
dragEndTranslation.x = -shift;
|
|
} else {
|
|
// New location is on the right, shift right
|
|
// before -> |.ooo......| after -> |......ooo.|
|
|
dragEndTranslation.x = shift;
|
|
}
|
|
return dragEndTranslation;
|
|
}
|
|
|
|
private float getDistanceFromOtherSide() {
|
|
// Calculate the shift needed to position the bubble bar on the other side
|
|
int displayWidth = getResources().getDisplayMetrics().widthPixels;
|
|
int margin = 0;
|
|
if (getLayoutParams() instanceof MarginLayoutParams lp) {
|
|
margin += lp.leftMargin;
|
|
margin += lp.rightMargin;
|
|
}
|
|
return (float) (displayWidth - getWidth() - margin);
|
|
}
|
|
|
|
/**
|
|
* Animate bubble bar to the given location transiently. Does not modify the layout or the value
|
|
* returned by {@link #getBubbleBarLocation()}.
|
|
*/
|
|
public void animateToBubbleBarLocation(BubbleBarLocation bubbleBarLocation) {
|
|
if (mBubbleBarLocationAnimator != null && mBubbleBarLocationAnimator.isRunning()) {
|
|
mBubbleBarLocationAnimator.removeAllListeners();
|
|
mBubbleBarLocationAnimator.cancel();
|
|
}
|
|
|
|
// Location animation uses two separate animators.
|
|
// First animator hides the bar.
|
|
// After it completes, bubble positions in the bar and arrow position is updated.
|
|
// Second animator is started to show the bar.
|
|
mBubbleBarLocationAnimator = getLocationUpdateFadeOutAnimator(bubbleBarLocation);
|
|
mBubbleBarLocationAnimator.addListener(new AnimatorListenerAdapter() {
|
|
@Override
|
|
public void onAnimationEnd(Animator animation) {
|
|
updateChildrenRenderNodeProperties(bubbleBarLocation);
|
|
mBubbleBarBackground.setAnchorLeft(bubbleBarLocation.isOnLeft(isLayoutRtl()));
|
|
|
|
// Animate it in
|
|
mBubbleBarLocationAnimator = getLocationUpdateFadeInAnimator(bubbleBarLocation);
|
|
mBubbleBarLocationAnimator.start();
|
|
}
|
|
});
|
|
mBubbleBarLocationAnimator.start();
|
|
}
|
|
|
|
private Animator getLocationUpdateFadeOutAnimator(BubbleBarLocation bubbleBarLocation) {
|
|
final float shift =
|
|
getResources().getDisplayMetrics().widthPixels * FADE_OUT_ANIM_POSITION_SHIFT;
|
|
final boolean onLeft = bubbleBarLocation.isOnLeft(isLayoutRtl());
|
|
final float tx = getTranslationX() + (onLeft ? shift : -shift);
|
|
|
|
ObjectAnimator positionAnim = ObjectAnimator.ofFloat(this, TRANSLATION_X, tx)
|
|
.setDuration(FADE_OUT_ANIM_POSITION_DURATION_MS);
|
|
positionAnim.setInterpolator(EMPHASIZED_ACCELERATE);
|
|
|
|
ObjectAnimator alphaAnim = ObjectAnimator.ofFloat(this, ALPHA, 0f)
|
|
.setDuration(FADE_OUT_ANIM_ALPHA_DURATION_MS);
|
|
alphaAnim.setStartDelay(FADE_OUT_ANIM_ALPHA_DELAY_MS);
|
|
|
|
AnimatorSet animatorSet = new AnimatorSet();
|
|
animatorSet.playTogether(positionAnim, alphaAnim);
|
|
return animatorSet;
|
|
}
|
|
|
|
private Animator getLocationUpdateFadeInAnimator(BubbleBarLocation animatedLocation) {
|
|
final float shift =
|
|
getResources().getDisplayMetrics().widthPixels * FADE_IN_ANIM_POSITION_SHIFT;
|
|
|
|
final boolean onLeft = animatedLocation.isOnLeft(isLayoutRtl());
|
|
final float startTx;
|
|
final float finalTx;
|
|
if (animatedLocation == mBubbleBarLocation) {
|
|
// Animated location matches layout location.
|
|
finalTx = 0;
|
|
} else {
|
|
// We are animating in to a transient location, need to move the bar accordingly.
|
|
finalTx = getDistanceFromOtherSide() * (onLeft ? -1 : 1);
|
|
}
|
|
if (onLeft) {
|
|
// Bar will be shown on the left side. Start point is shifted right.
|
|
startTx = finalTx + shift;
|
|
} else {
|
|
// Bar will be shown on the right side. Start point is shifted left.
|
|
startTx = finalTx - shift;
|
|
}
|
|
|
|
ValueAnimator positionAnim = new SpringAnimationBuilder(getContext())
|
|
.setStartValue(startTx)
|
|
.setEndValue(finalTx)
|
|
.setDampingRatio(SpringForce.DAMPING_RATIO_LOW_BOUNCY)
|
|
.setStiffness(FADE_IN_ANIM_POSITION_SPRING_STIFFNESS)
|
|
.build(this, VIEW_TRANSLATE_X);
|
|
|
|
ObjectAnimator alphaAnim = ObjectAnimator.ofFloat(this, ALPHA, 1f)
|
|
.setDuration(FADE_IN_ANIM_ALPHA_DURATION_MS);
|
|
|
|
AnimatorSet animatorSet = new AnimatorSet();
|
|
animatorSet.playTogether(positionAnim, alphaAnim);
|
|
return animatorSet;
|
|
}
|
|
|
|
/**
|
|
* Updates the bounds with translation that may have been applied and returns the result.
|
|
*/
|
|
public Rect getBubbleBarBounds() {
|
|
mBubbleBarBounds.top = getTop() + (int) getTranslationY() + mPointerSize;
|
|
mBubbleBarBounds.bottom = getBottom() + (int) getTranslationY();
|
|
return mBubbleBarBounds;
|
|
}
|
|
|
|
/**
|
|
* Set bubble bar relative pivot value for X and Y, applied as a fraction of view width/height
|
|
* respectively. If the value is not in range of 0 to 1 it will be normalized.
|
|
* @param x relative X pivot value in range 0..1
|
|
* @param y relative Y pivot value in range 0..1
|
|
*/
|
|
public void setRelativePivot(float x, float y) {
|
|
mRelativePivotX = Float.max(Float.min(x, 1), 0);
|
|
mRelativePivotY = Float.max(Float.min(y, 1), 0);
|
|
requestLayout();
|
|
}
|
|
|
|
/** Like {@link #setRelativePivot(float, float)} but only updates pivot y. */
|
|
public void setRelativePivotY(float y) {
|
|
setRelativePivot(mRelativePivotX, y);
|
|
}
|
|
|
|
/**
|
|
* Get current relative pivot for X axis
|
|
*/
|
|
public float getRelativePivotX() {
|
|
return mRelativePivotX;
|
|
}
|
|
|
|
/**
|
|
* Get current relative pivot for Y axis
|
|
*/
|
|
public float getRelativePivotY() {
|
|
return mRelativePivotY;
|
|
}
|
|
|
|
/** Notifies the bubble bar that a new bubble animation is starting. */
|
|
public void onAnimatingBubbleStarted() {
|
|
mIsAnimatingNewBubble = true;
|
|
}
|
|
|
|
/** Notifies the bubble bar that a new bubble animation is complete. */
|
|
public void onAnimatingBubbleCompleted() {
|
|
mIsAnimatingNewBubble = false;
|
|
}
|
|
|
|
// TODO: (b/280605790) animate it
|
|
@Override
|
|
public void addView(View child, int index, ViewGroup.LayoutParams params) {
|
|
if (getChildCount() + 1 > MAX_BUBBLES) {
|
|
// the last child view is the overflow bubble and we shouldn't remove that. remove the
|
|
// second to last child view.
|
|
removeViewInLayout(getChildAt(getChildCount() - 2));
|
|
}
|
|
super.addView(child, index, params);
|
|
updateWidth();
|
|
}
|
|
|
|
// TODO: (b/283309949) animate it
|
|
@Override
|
|
public void removeView(View view) {
|
|
super.removeView(view);
|
|
if (view == mSelectedBubbleView) {
|
|
mSelectedBubbleView = null;
|
|
mBubbleBarBackground.showArrow(false);
|
|
}
|
|
updateWidth();
|
|
}
|
|
|
|
private void updateWidth() {
|
|
LayoutParams lp = (FrameLayout.LayoutParams) getLayoutParams();
|
|
lp.width = (int) (mIsBarExpanded ? expandedWidth() : collapsedWidth());
|
|
setLayoutParams(lp);
|
|
}
|
|
|
|
private void updateLayoutParams() {
|
|
LayoutParams lp = (FrameLayout.LayoutParams) getLayoutParams();
|
|
lp.height = (int) getBubbleBarExpandedHeight();
|
|
lp.width = (int) (mIsBarExpanded ? expandedWidth() : collapsedWidth());
|
|
setLayoutParams(lp);
|
|
}
|
|
|
|
/** @return the horizontal margin between the bubble bar and the edge of the screen. */
|
|
int getHorizontalMargin() {
|
|
LayoutParams lp = (FrameLayout.LayoutParams) getLayoutParams();
|
|
return lp.getMarginEnd();
|
|
}
|
|
|
|
/**
|
|
* Updates the z order, positions, and badge visibility of the bubble views in the bar based
|
|
* on the expanded state.
|
|
*/
|
|
private void updateChildrenRenderNodeProperties(BubbleBarLocation bubbleBarLocation) {
|
|
final float widthState = (float) mWidthAnimator.getAnimatedValue();
|
|
final float currentWidth = getWidth();
|
|
final float expandedWidth = expandedWidth();
|
|
final float collapsedWidth = collapsedWidth();
|
|
int bubbleCount = getChildCount();
|
|
final float ty = (mBubbleBarBounds.height() - mIconSize) / 2f;
|
|
final boolean animate = getVisibility() == VISIBLE;
|
|
final boolean onLeft = bubbleBarLocation.isOnLeft(isLayoutRtl());
|
|
// elevation state is opposite to widthState - when expanded all icons are flat
|
|
float elevationState = (1 - widthState);
|
|
for (int i = 0; i < bubbleCount; i++) {
|
|
BubbleView bv = (BubbleView) getChildAt(i);
|
|
bv.setTranslationY(ty);
|
|
|
|
// the position of the bubble when the bar is fully expanded
|
|
final float expandedX;
|
|
// the position of the bubble when the bar is fully collapsed
|
|
final float collapsedX;
|
|
if (onLeft) {
|
|
// If bar is on the left, bubbles are ordered right to left
|
|
expandedX = (bubbleCount - i - 1) * (mIconSize + mExpandedBarIconsSpacing);
|
|
// Shift the first bubble only if there are more bubbles in addition to overflow
|
|
collapsedX = i == 0 && bubbleCount > 2 ? mIconOverlapAmount : 0;
|
|
} else {
|
|
// Bubbles ordered left to right, don't move the first bubble
|
|
expandedX = i * (mIconSize + mExpandedBarIconsSpacing);
|
|
collapsedX = i == 0 ? 0 : mIconOverlapAmount;
|
|
}
|
|
if (bv == mDraggedBubbleView) {
|
|
// if bubble is dragged set the elevation to bubble drag elevation
|
|
bv.setZ(mDragElevation);
|
|
} else {
|
|
// otherwise slowly animate elevation while keeping correct Z ordering
|
|
float fullElevationForChild = (MAX_BUBBLES * mBubbleElevation) - i;
|
|
bv.setZ(fullElevationForChild * elevationState);
|
|
}
|
|
if (mIsBarExpanded) {
|
|
// If bar is on the right, account for bubble bar expanding and shifting left
|
|
final float expandedBarShift = onLeft ? 0 : currentWidth - expandedWidth;
|
|
// where the bubble will end up when the animation ends
|
|
final float targetX = expandedX + expandedBarShift;
|
|
bv.setTranslationX(widthState * (targetX - collapsedX) + collapsedX);
|
|
// When we're expanded, we're not stacked so we're not behind the stack
|
|
bv.setBehindStack(false, animate);
|
|
bv.setAlpha(1);
|
|
} else {
|
|
// If bar is on the right, account for bubble bar expanding and shifting left
|
|
final float collapsedBarShift = onLeft ? 0 : currentWidth - collapsedWidth;
|
|
final float targetX = collapsedX + collapsedBarShift;
|
|
bv.setTranslationX(widthState * (expandedX - targetX) + targetX);
|
|
// If we're not the first bubble we're behind the stack
|
|
bv.setBehindStack(i > 0, animate);
|
|
// If we're fully collapsed, hide all bubbles except for the first 2. If there are
|
|
// only 2 bubbles, hide the second bubble as well because it's the overflow.
|
|
if (widthState == 0) {
|
|
if (i > 1) {
|
|
bv.setAlpha(0);
|
|
} else if (i == 1 && bubbleCount == 2) {
|
|
bv.setAlpha(0);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// update the arrow position
|
|
final float collapsedArrowPosition = arrowPositionForSelectedWhenCollapsed(
|
|
bubbleBarLocation);
|
|
final float expandedArrowPosition = arrowPositionForSelectedWhenExpanded(bubbleBarLocation);
|
|
final float interpolatedWidth =
|
|
widthState * (expandedWidth - collapsedWidth) + collapsedWidth;
|
|
final float arrowPosition;
|
|
|
|
float interpolatedShift = (expandedArrowPosition - collapsedArrowPosition) * widthState;
|
|
if (onLeft) {
|
|
arrowPosition = collapsedArrowPosition + interpolatedShift;
|
|
} else {
|
|
if (mIsBarExpanded) {
|
|
arrowPosition = currentWidth - interpolatedWidth + collapsedArrowPosition
|
|
+ interpolatedShift;
|
|
} else {
|
|
final float targetPosition = currentWidth - collapsedWidth + collapsedArrowPosition;
|
|
arrowPosition =
|
|
targetPosition + widthState * (expandedArrowPosition - targetPosition);
|
|
}
|
|
}
|
|
mBubbleBarBackground.setArrowPosition(arrowPosition);
|
|
mBubbleBarBackground.setArrowAlpha((int) (255 * widthState));
|
|
mBubbleBarBackground.setWidth(interpolatedWidth);
|
|
}
|
|
|
|
/**
|
|
* Reorders the views to match the provided list.
|
|
*/
|
|
public void reorder(List<BubbleView> viewOrder) {
|
|
if (isExpanded() || mWidthAnimator.isRunning()) {
|
|
mReorderRunnable = () -> doReorder(viewOrder);
|
|
} else {
|
|
doReorder(viewOrder);
|
|
}
|
|
}
|
|
|
|
// TODO: (b/273592694) animate it
|
|
private void doReorder(List<BubbleView> viewOrder) {
|
|
if (!isExpanded()) {
|
|
for (int i = 0; i < viewOrder.size(); i++) {
|
|
View child = viewOrder.get(i);
|
|
// this child view may have already been removed so verify that it still exists
|
|
// before reordering it, otherwise it will be re-added.
|
|
int indexOfChild = indexOfChild(child);
|
|
if (child != null && indexOfChild >= 0) {
|
|
removeViewInLayout(child);
|
|
addViewInLayout(child, i, child.getLayoutParams());
|
|
}
|
|
}
|
|
updateChildrenRenderNodeProperties(mBubbleBarLocation);
|
|
}
|
|
}
|
|
|
|
public void setUpdateSelectedBubbleAfterCollapse(
|
|
Consumer<String> updateSelectedBubbleAfterCollapse) {
|
|
mUpdateSelectedBubbleAfterCollapse = updateSelectedBubbleAfterCollapse;
|
|
}
|
|
|
|
/**
|
|
* Sets which bubble view should be shown as selected.
|
|
*/
|
|
public void setSelectedBubble(BubbleView view) {
|
|
BubbleView previouslySelectedBubble = mSelectedBubbleView;
|
|
mSelectedBubbleView = view;
|
|
mBubbleBarBackground.showArrow(view != null);
|
|
// TODO: (b/283309949) remove animation should be implemented first, so than arrow
|
|
// animation is adjusted, skip animation for now
|
|
updateArrowForSelected(previouslySelectedBubble != null);
|
|
}
|
|
|
|
/**
|
|
* Sets the dragged bubble view to correctly apply Z order. Dragged view should appear on top
|
|
*/
|
|
public void setDraggedBubble(@Nullable BubbleView view) {
|
|
mDraggedBubbleView = view;
|
|
requestLayout();
|
|
}
|
|
|
|
/**
|
|
* Update the arrow position to match the selected bubble.
|
|
*
|
|
* @param shouldAnimate whether or not to animate the arrow. If the bar was just expanded, this
|
|
* should be set to {@code false}. Otherwise set this to {@code true}.
|
|
*/
|
|
private void updateArrowForSelected(boolean shouldAnimate) {
|
|
if (mSelectedBubbleView == null) {
|
|
Log.w(TAG, "trying to update selection arrow without a selected view!");
|
|
return;
|
|
}
|
|
// Find the center of the bubble when it's expanded, set the arrow position to it.
|
|
final float tx = arrowPositionForSelectedWhenExpanded(mBubbleBarLocation);
|
|
final float currentArrowPosition = mBubbleBarBackground.getArrowPositionX();
|
|
if (tx == currentArrowPosition) {
|
|
// arrow position remains unchanged
|
|
return;
|
|
}
|
|
if (shouldAnimate && currentArrowPosition > expandedWidth()) {
|
|
Log.d(TAG, "arrow out of bounds of expanded view, skip animation");
|
|
shouldAnimate = false;
|
|
}
|
|
if (shouldAnimate) {
|
|
ValueAnimator animator = ValueAnimator.ofFloat(currentArrowPosition, tx);
|
|
animator.setDuration(ARROW_POSITION_ANIMATION_DURATION_MS);
|
|
animator.addUpdateListener(animation -> {
|
|
float x = (float) animation.getAnimatedValue();
|
|
mBubbleBarBackground.setArrowPosition(x);
|
|
invalidate();
|
|
});
|
|
animator.start();
|
|
} else {
|
|
mBubbleBarBackground.setArrowPosition(tx);
|
|
invalidate();
|
|
}
|
|
}
|
|
|
|
private float arrowPositionForSelectedWhenExpanded(BubbleBarLocation bubbleBarLocation) {
|
|
final int index = indexOfChild(mSelectedBubbleView);
|
|
final int bubblePosition;
|
|
if (bubbleBarLocation.isOnLeft(isLayoutRtl())) {
|
|
// Bubble positions are reversed. First bubble is on the right.
|
|
bubblePosition = getChildCount() - index - 1;
|
|
} else {
|
|
bubblePosition = index;
|
|
}
|
|
return getPaddingStart() + bubblePosition * (mIconSize + mExpandedBarIconsSpacing)
|
|
+ mIconSize / 2f;
|
|
}
|
|
|
|
private float arrowPositionForSelectedWhenCollapsed(BubbleBarLocation bubbleBarLocation) {
|
|
final int index = indexOfChild(mSelectedBubbleView);
|
|
final int bubblePosition;
|
|
if (bubbleBarLocation.isOnLeft(isLayoutRtl())) {
|
|
// Bubble positions are reversed. First bubble may be shifted, if there are more
|
|
// bubbles than the current bubble and overflow.
|
|
bubblePosition = index == 0 && getChildCount() > 2 ? 1 : 0;
|
|
} else {
|
|
bubblePosition = index;
|
|
}
|
|
return getPaddingStart() + bubblePosition * (mIconOverlapAmount) + mIconSize / 2f;
|
|
}
|
|
|
|
@Override
|
|
public void setOnClickListener(View.OnClickListener listener) {
|
|
mOnClickListener = listener;
|
|
setOrUnsetClickListener();
|
|
}
|
|
|
|
/**
|
|
* The click listener used for the bubble view gets added / removed depending on whether
|
|
* the bar is expanded or collapsed, this updates whether the listener is set based on state.
|
|
*/
|
|
private void setOrUnsetClickListener() {
|
|
super.setOnClickListener(mIsBarExpanded ? null : mOnClickListener);
|
|
}
|
|
|
|
/**
|
|
* Sets whether the bubble bar is expanded or collapsed.
|
|
*/
|
|
public void setExpanded(boolean isBarExpanded) {
|
|
if (mIsBarExpanded != isBarExpanded) {
|
|
mIsBarExpanded = isBarExpanded;
|
|
updateArrowForSelected(/* shouldAnimate= */ false);
|
|
setOrUnsetClickListener();
|
|
if (isBarExpanded) {
|
|
mWidthAnimator.start();
|
|
} else {
|
|
mWidthAnimator.reverse();
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Returns whether the bubble bar is expanded.
|
|
*/
|
|
public boolean isExpanded() {
|
|
return mIsBarExpanded;
|
|
}
|
|
|
|
/**
|
|
* Get width of the bubble bar as if it would be expanded.
|
|
*
|
|
* @return width of the bubble bar in its expanded state, regardless of current width
|
|
*/
|
|
public float expandedWidth() {
|
|
final int childCount = getChildCount();
|
|
final int horizontalPadding = getPaddingStart() + getPaddingEnd();
|
|
// spaces amount is less than child count by 1, or 0 if no child views
|
|
int spacesCount = Math.max(childCount - 1, 0);
|
|
return childCount * mIconSize + spacesCount * mExpandedBarIconsSpacing + horizontalPadding;
|
|
}
|
|
|
|
private float collapsedWidth() {
|
|
final int childCount = getChildCount();
|
|
final int horizontalPadding = getPaddingStart() + getPaddingEnd();
|
|
// If there are more than 2 bubbles, the first 2 should be visible when collapsed.
|
|
// Otherwise just the first bubble should be visible because we don't show the overflow.
|
|
return childCount > 2
|
|
? mIconSize + mIconOverlapAmount + horizontalPadding
|
|
: mIconSize + horizontalPadding;
|
|
}
|
|
|
|
private float getBubbleBarExpandedHeight() {
|
|
return getBubbleBarCollapsedHeight() + mPointerSize;
|
|
}
|
|
|
|
float getBubbleBarCollapsedHeight() {
|
|
// the pointer is invisible when collapsed
|
|
return mIconSize + mBubbleBarPadding * 2;
|
|
}
|
|
|
|
/**
|
|
* Returns whether the given MotionEvent, *in screen coordinates*, is within bubble bar
|
|
* touch bounds.
|
|
*/
|
|
public boolean isEventOverAnyItem(MotionEvent ev) {
|
|
if (getVisibility() == View.VISIBLE) {
|
|
getBoundsOnScreen(mTempRect);
|
|
return mTempRect.contains((int) ev.getX(), (int) ev.getY());
|
|
}
|
|
return false;
|
|
}
|
|
|
|
@Override
|
|
public boolean onInterceptTouchEvent(MotionEvent ev) {
|
|
if (!mIsBarExpanded && !mIsAnimatingNewBubble) {
|
|
// When the bar is collapsed, all taps on it should expand it.
|
|
return true;
|
|
}
|
|
return super.onInterceptTouchEvent(ev);
|
|
}
|
|
|
|
/** Whether a new bubble is currently animating. */
|
|
public boolean isAnimatingNewBubble() {
|
|
return mIsAnimatingNewBubble;
|
|
}
|
|
}
|