Files
Lawnchair/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleView.java
T
2026-01-10 20:51:01 +07:00

342 lines
12 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 android.annotation.Nullable;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Outline;
import android.graphics.Rect;
import android.text.TextUtils;
import android.util.AttributeSet;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewOutlineProvider;
import android.widget.ImageView;
import androidx.constraintlayout.widget.ConstraintLayout;
import com.android.launcher3.R;
import com.android.launcher3.icons.DotRenderer;
import com.android.launcher3.icons.IconNormalizer;
import com.android.wm.shell.animation.Interpolators;
import com.patrykmichalik.opto.core.PreferenceExtensionsKt;
import app.lawnchair.preferences2.PreferenceManager2;
import app.lawnchair.theme.color.ColorOption;
import java.util.EnumSet;
// TODO: (b/276978250) This is will be similar to WMShell's BadgedImageView, it'd be nice to share.
/**
* View that displays a bubble icon, along with an app badge on either the left
* or
* right side of the view.
*/
public class BubbleView extends ConstraintLayout {
public static final int DEFAULT_PATH_SIZE = 100;
/**
* Flags that suppress the visibility of the 'new' dot or the app badge, for one
* reason or
* another. If any of these flags are set, the dot will not be shown.
* If {@link SuppressionFlag#BEHIND_STACK} then the app badge will not be shown.
*/
enum SuppressionFlag {
// TODO: (b/277815200) implement flyout
// Suppressed because the flyout is visible - it will morph into the dot via
// animation.
FLYOUT_VISIBLE,
// Suppressed because this bubble is behind others in the collapsed stack.
BEHIND_STACK,
}
private final EnumSet<SuppressionFlag> mSuppressionFlags = EnumSet.noneOf(SuppressionFlag.class);
private final ImageView mBubbleIcon;
private final ImageView mAppIcon;
private final int mBubbleSize;
private float mDragTranslationX;
private float mOffsetX;
private DotRenderer mDotRenderer;
private DotRenderer.DrawParams mDrawParams;
private int mDotColor;
private Rect mTempBounds = new Rect();
// Whether the dot is animating
private boolean mDotIsAnimating;
// What scale value the dot is animating to
private float mAnimatingToDotScale;
// The current scale value of the dot
private float mDotScale;
// TODO: (b/273310265) handle RTL
// Whether the bubbles are positioned on the left or right side of the screen
private boolean mOnLeft = false;
private BubbleBarItem mBubble;
PreferenceManager2 preferenceManager2;
public BubbleView(Context context) {
this(context, null);
}
public BubbleView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public BubbleView(Context context, AttributeSet attrs, int defStyleAttr) {
this(context, attrs, defStyleAttr, 0);
}
public BubbleView(Context context, AttributeSet attrs, int defStyleAttr,
int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
// We manage positioning the badge ourselves
setLayoutDirection(LAYOUT_DIRECTION_LTR);
LayoutInflater.from(context).inflate(R.layout.bubble_view, this);
mBubbleSize = getResources().getDimensionPixelSize(R.dimen.bubblebar_icon_size);
mBubbleIcon = findViewById(R.id.icon_view);
mAppIcon = findViewById(R.id.app_icon_view);
preferenceManager2 = PreferenceManager2.INSTANCE.get(context);
mDrawParams = new DotRenderer.DrawParams();
setFocusable(true);
setClickable(true);
setOutlineProvider(new ViewOutlineProvider() {
@Override
public void getOutline(View view, Outline outline) {
BubbleView.this.getOutline(outline);
}
});
}
private void getOutline(Outline outline) {
final int normalizedSize = IconNormalizer.getNormalizedCircleSize(mBubbleSize);
final int inset = (mBubbleSize - normalizedSize) / 2;
outline.setOval(inset, inset, inset + normalizedSize, inset + normalizedSize);
}
/**
* Set translation-x while this bubble is being dragged.
* Translation applied to the view is a sum of {@code translationX} and offset
* defined by
* {@link #setOffsetX(float)}.
*/
public void setDragTranslationX(float translationX) {
mDragTranslationX = translationX;
applyDragTranslation();
}
/**
* Get translation value applied via {@link #setDragTranslationX(float)}.
*/
public float getDragTranslationX() {
return mDragTranslationX;
}
/**
* Set offset on x-axis while dragging.
* Used to counter parent translation in order to keep the dragged view at the
* current position
* on screen.
* Translation applied to the view is a sum of {@code offsetX} and translation
* defined by
* {@link #setDragTranslationX(float)}
*/
public void setOffsetX(float offsetX) {
mOffsetX = offsetX;
applyDragTranslation();
}
private void applyDragTranslation() {
setTranslationX(mDragTranslationX + mOffsetX);
}
@Override
public void dispatchDraw(Canvas canvas) {
super.dispatchDraw(canvas);
if (!shouldDrawDot()) {
return;
}
getDrawingRect(mTempBounds);
mDrawParams.appColor = mDotColor;
mDrawParams.iconBounds = mTempBounds;
mDrawParams.leftAlign = mOnLeft;
mDrawParams.scale = mDotScale;
mDotRenderer.draw(canvas, mDrawParams, -1);
}
/** Sets the bubble being rendered in this view. */
public void setBubble(BubbleBarBubble bubble) {
mBubble = bubble;
mBubbleIcon.setImageBitmap(bubble.getIcon());
mAppIcon.setImageBitmap(bubble.getBadge());
mDotColor = bubble.getDotColor();
ColorOption dotColorOption = PreferenceExtensionsKt.firstBlocking(preferenceManager2.getNotificationDotColor());
int dotColor = dotColorOption.getColorPreferenceEntry().getLightColor().invoke(getContext());
ColorOption counterColorOption = PreferenceExtensionsKt
.firstBlocking(preferenceManager2.getNotificationDotTextColor());
int countColor = counterColorOption.getColorPreferenceEntry().getLightColor().invoke(getContext());
mDotRenderer = new DotRenderer(mBubbleSize, bubble.getDotPath(), DEFAULT_PATH_SIZE, false, null, dotColor,
countColor);
String contentDesc = bubble.getInfo().getTitle();
if (TextUtils.isEmpty(contentDesc)) {
contentDesc = getResources().getString(R.string.bubble_bar_bubble_fallback_description);
}
String appName = bubble.getInfo().getAppName();
if (!TextUtils.isEmpty(appName)) {
contentDesc = getResources().getString(R.string.bubble_bar_bubble_description,
contentDesc, appName);
}
setContentDescription(contentDesc);
}
/**
* Sets that this bubble represents the overflow. The overflow appears in the
* list of bubbles
* but does not represent app content, instead it shows recent bubbles that
* couldn't fit into
* the list of bubbles. It doesn't show an app icon because it is part of system
* UI / doesn't
* come from an app.
*/
public void setOverflow(BubbleBarOverflow overflow, Bitmap bitmap) {
mBubble = overflow;
mBubbleIcon.setImageBitmap(bitmap);
mAppIcon.setVisibility(GONE); // Overflow doesn't show the app badge
setContentDescription(getResources().getString(R.string.bubble_bar_overflow_description));
}
/** Returns the bubble being rendered in this view. */
@Nullable
public BubbleBarItem getBubble() {
return mBubble;
}
void updateDotVisibility(boolean animate) {
final float targetScale = shouldDrawDot() ? 1f : 0f;
if (animate) {
animateDotScale();
} else {
mDotScale = targetScale;
mAnimatingToDotScale = targetScale;
invalidate();
}
}
void updateBadgeVisibility() {
if (mBubble instanceof BubbleBarOverflow) {
// The overflow bubble does not have a badge, so just bail.
return;
}
BubbleBarBubble bubble = (BubbleBarBubble) mBubble;
Bitmap appBadgeBitmap = bubble.getBadge();
int translationX = mOnLeft
? -(bubble.getIcon().getWidth() - appBadgeBitmap.getWidth())
: 0;
mAppIcon.setTranslationX(translationX);
mAppIcon.setVisibility(isBehindStack() ? GONE : VISIBLE);
}
/** Sets whether this bubble is in the stack & not the first bubble. **/
void setBehindStack(boolean behindStack, boolean animate) {
if (behindStack) {
mSuppressionFlags.add(SuppressionFlag.BEHIND_STACK);
} else {
mSuppressionFlags.remove(SuppressionFlag.BEHIND_STACK);
}
updateDotVisibility(animate);
updateBadgeVisibility();
}
/** Whether this bubble is in the stack & not the first bubble. **/
boolean isBehindStack() {
return mSuppressionFlags.contains(SuppressionFlag.BEHIND_STACK);
}
/** Whether the dot indicating unseen content in a bubble should be shown. */
private boolean shouldDrawDot() {
boolean bubbleHasUnseenContent = mBubble != null
&& mBubble instanceof BubbleBarBubble
&& mSuppressionFlags.isEmpty()
&& !((BubbleBarBubble) mBubble).getInfo().isNotificationSuppressed();
// Always render the dot if it's animating, since it could be animating out.
// Otherwise, show
// it if the bubble wants to show it, and we aren't suppressing it.
return bubbleHasUnseenContent || mDotIsAnimating;
}
/** How big the dot should be, fraction from 0 to 1. */
private void setDotScale(float fraction) {
mDotScale = fraction;
invalidate();
}
/**
* Animates the dot to the given scale.
*/
private void animateDotScale() {
float toScale = shouldDrawDot() ? 1f : 0f;
mDotIsAnimating = true;
// Don't restart the animation if we're already animating to the given value.
if (mAnimatingToDotScale == toScale || !shouldDrawDot()) {
mDotIsAnimating = false;
return;
}
mAnimatingToDotScale = toScale;
final boolean showDot = toScale > 0f;
// Do NOT wait until after animation ends to setShowDot
// to avoid overriding more recent showDot states.
clearAnimation();
animate()
.setDuration(200)
.setInterpolator(Interpolators.FAST_OUT_SLOW_IN)
.setUpdateListener((valueAnimator) -> {
float fraction = valueAnimator.getAnimatedFraction();
fraction = showDot ? fraction : 1f - fraction;
setDotScale(fraction);
}).withEndAction(() -> {
setDotScale(showDot ? 1f : 0f);
mDotIsAnimating = false;
}).start();
}
@Override
public String toString() {
String toString = mBubble != null ? mBubble.getKey() : "null";
return "BubbleView{" + toString + "}";
}
}