Move BubbleView methods into BadgedImageView; remove BubbleView
* Removed BubbleView
* Moved the icon / badge logic into BubbleIconFactory
* Moved everything else into BadgedImageView
* Introduced dot states in BadgedImageView which hopefully makes that
easier to read
* Altered Bubble#setShowDot to also animate the dot visibility
* Altered BubbleFlyoutView to be able to animate the dot away for
DND scenario
Test: atest SystemUITests (existing bubble tests pass)
Bug: 144719337
Bug: 145245204 (fixes the bit where tapping on bubble in expanded state
doesn't animate the dot away but jump cuts instead)
Change-Id: I8cebf3e7f93db1920ede95eb6f7392560270767f
This commit is contained in:
@@ -14,16 +14,8 @@
|
||||
~ See the License for the specific language governing permissions and
|
||||
~ limitations under the License
|
||||
-->
|
||||
<com.android.systemui.bubbles.BubbleView
|
||||
<com.android.systemui.bubbles.BadgedImageView
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_width="wrap_content"
|
||||
android:id="@+id/bubble_view">
|
||||
|
||||
<com.android.systemui.bubbles.BadgedImageView
|
||||
android:id="@+id/bubble_image"
|
||||
android:layout_width="@dimen/individual_bubble_size"
|
||||
android:layout_height="@dimen/individual_bubble_size"
|
||||
android:clipToPadding="false"/>
|
||||
|
||||
</com.android.systemui.bubbles.BubbleView>
|
||||
android:id="@+id/bubble_view"
|
||||
android:layout_width="@dimen/individual_bubble_size"
|
||||
android:layout_height="@dimen/individual_bubble_size"/>
|
||||
|
||||
@@ -15,35 +15,61 @@
|
||||
*/
|
||||
package com.android.systemui.bubbles;
|
||||
|
||||
import android.annotation.Nullable;
|
||||
import android.app.Notification;
|
||||
import android.content.Context;
|
||||
import android.content.res.TypedArray;
|
||||
import android.content.pm.LauncherApps;
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.Color;
|
||||
import android.graphics.Matrix;
|
||||
import android.graphics.Path;
|
||||
import android.graphics.Rect;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.graphics.drawable.Icon;
|
||||
import android.util.AttributeSet;
|
||||
import android.util.PathParser;
|
||||
import android.widget.ImageView;
|
||||
|
||||
import com.android.internal.graphics.ColorUtils;
|
||||
import com.android.launcher3.icons.BitmapInfo;
|
||||
import com.android.launcher3.icons.DotRenderer;
|
||||
import com.android.systemui.Interpolators;
|
||||
import com.android.systemui.R;
|
||||
|
||||
/**
|
||||
* View that circle crops its contents and supports displaying a coloured dot on a top corner.
|
||||
* View that displays an adaptive icon with an app-badge and a dot.
|
||||
*
|
||||
* Dot = a small colored circle that indicates whether this bubble has an unread update.
|
||||
* Badge = the icon associated with the app that created this bubble, this will show work profile
|
||||
* badge if appropriate.
|
||||
*/
|
||||
public class BadgedImageView extends ImageView {
|
||||
|
||||
private Rect mTempBounds = new Rect();
|
||||
/** Same value as Launcher3 dot code */
|
||||
private static final float WHITE_SCRIM_ALPHA = 0.54f;
|
||||
/** Same as value in Launcher3 IconShape */
|
||||
private static final int DEFAULT_PATH_SIZE = 100;
|
||||
|
||||
static final int DOT_STATE_DEFAULT = 0;
|
||||
static final int DOT_STATE_SUPPRESSED_FOR_FLYOUT = 1;
|
||||
static final int DOT_STATE_ANIMATING = 2;
|
||||
|
||||
// Flyout gets shown before the dot
|
||||
private int mCurrentDotState = DOT_STATE_SUPPRESSED_FOR_FLYOUT;
|
||||
|
||||
private Bubble mBubble;
|
||||
private BubbleIconFactory mBubbleIconFactory;
|
||||
|
||||
private int mIconBitmapSize;
|
||||
private DotRenderer mDotRenderer;
|
||||
private DotRenderer.DrawParams mDrawParams;
|
||||
private int mIconBitmapSize;
|
||||
private int mDotColor;
|
||||
private float mDotScale = 0f;
|
||||
private boolean mShowDot;
|
||||
private boolean mOnLeft;
|
||||
|
||||
/** Same as value in Launcher3 IconShape */
|
||||
static final int DEFAULT_PATH_SIZE = 100;
|
||||
private int mDotColor;
|
||||
private float mDotScale = 0f;
|
||||
private boolean mDotDrawn;
|
||||
|
||||
private Rect mTempBounds = new Rect();
|
||||
|
||||
public BadgedImageView(Context context) {
|
||||
this(context, null);
|
||||
@@ -63,17 +89,19 @@ public class BadgedImageView extends ImageView {
|
||||
mIconBitmapSize = getResources().getDimensionPixelSize(R.dimen.bubble_icon_bitmap_size);
|
||||
mDrawParams = new DotRenderer.DrawParams();
|
||||
|
||||
TypedArray ta = context.obtainStyledAttributes(
|
||||
new int[]{android.R.attr.colorBackgroundFloating});
|
||||
ta.recycle();
|
||||
Path iconPath = PathParser.createPathFromPathData(
|
||||
getResources().getString(com.android.internal.R.string.config_icon_mask));
|
||||
mDotRenderer = new DotRenderer(mIconBitmapSize, iconPath, DEFAULT_PATH_SIZE);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDraw(Canvas canvas) {
|
||||
super.onDraw(canvas);
|
||||
if (!mShowDot) {
|
||||
if (isDotHidden()) {
|
||||
mDotDrawn = false;
|
||||
return;
|
||||
}
|
||||
mDotDrawn = mDotScale > 0.1f;
|
||||
getDrawingRect(mTempBounds);
|
||||
|
||||
mDrawParams.color = mDotColor;
|
||||
@@ -81,15 +109,28 @@ public class BadgedImageView extends ImageView {
|
||||
mDrawParams.leftAlign = mOnLeft;
|
||||
mDrawParams.scale = mDotScale;
|
||||
|
||||
if (mDotRenderer == null) {
|
||||
Path circlePath = new Path();
|
||||
float radius = DEFAULT_PATH_SIZE * 0.5f;
|
||||
circlePath.addCircle(radius /* x */, radius /* y */, radius, Path.Direction.CW);
|
||||
mDotRenderer = new DotRenderer(mIconBitmapSize, circlePath, DEFAULT_PATH_SIZE);
|
||||
}
|
||||
mDotRenderer.draw(canvas, mDrawParams);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the dot state, does not animate changes.
|
||||
*/
|
||||
void setDotState(int state) {
|
||||
mCurrentDotState = state;
|
||||
if (state == DOT_STATE_SUPPRESSED_FOR_FLYOUT || state == DOT_STATE_DEFAULT) {
|
||||
mDotScale = mBubble.showDot() ? 1f : 0f;
|
||||
invalidate();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether the dot should be hidden based on current dot state.
|
||||
*/
|
||||
private boolean isDotHidden() {
|
||||
return (mCurrentDotState == DOT_STATE_DEFAULT && !mBubble.showDot())
|
||||
|| mCurrentDotState == DOT_STATE_SUPPRESSED_FOR_FLYOUT;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set whether the dot should appear on left or right side of the view.
|
||||
*/
|
||||
@@ -98,29 +139,10 @@ public class BadgedImageView extends ImageView {
|
||||
invalidate();
|
||||
}
|
||||
|
||||
boolean getDotOnLeft() {
|
||||
return mOnLeft;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set whether the dot should show or not.
|
||||
*/
|
||||
void setShowDot(boolean showDot) {
|
||||
mShowDot = showDot;
|
||||
invalidate();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return whether the dot is being displayed.
|
||||
*/
|
||||
boolean isShowingDot() {
|
||||
return mShowDot;
|
||||
}
|
||||
|
||||
/**
|
||||
* The colour to use for the dot.
|
||||
*/
|
||||
public void setDotColor(int color) {
|
||||
void setDotColor(int color) {
|
||||
mDotColor = ColorUtils.setAlphaComponent(color, 255 /* alpha */);
|
||||
invalidate();
|
||||
}
|
||||
@@ -128,7 +150,7 @@ public class BadgedImageView extends ImageView {
|
||||
/**
|
||||
* @param iconPath The new icon path to use when calculating dot position.
|
||||
*/
|
||||
public void drawDot(Path iconPath) {
|
||||
void drawDot(Path iconPath) {
|
||||
mDotRenderer = new DotRenderer(mIconBitmapSize, iconPath, DEFAULT_PATH_SIZE);
|
||||
invalidate();
|
||||
}
|
||||
@@ -141,6 +163,13 @@ public class BadgedImageView extends ImageView {
|
||||
invalidate();
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether decorations (badges or dots) are on the left.
|
||||
*/
|
||||
boolean getDotOnLeft() {
|
||||
return mOnLeft;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return dot position relative to bubble view container bounds.
|
||||
*/
|
||||
@@ -149,11 +178,146 @@ public class BadgedImageView extends ImageView {
|
||||
if (mOnLeft) {
|
||||
dotPosition = mDotRenderer.getLeftDotPosition();
|
||||
} else {
|
||||
dotPosition = mDotRenderer.getRightDotPosition();
|
||||
dotPosition = mDotRenderer.getRightDotPosition();
|
||||
}
|
||||
getDrawingRect(mTempBounds);
|
||||
float dotCenterX = mTempBounds.width() * dotPosition[0];
|
||||
float dotCenterY = mTempBounds.height() * dotPosition[1];
|
||||
return new float[]{dotCenterX, dotCenterY};
|
||||
}
|
||||
|
||||
/**
|
||||
* Populates this view with a bubble.
|
||||
* <p>
|
||||
* This should only be called when a new bubble is being set on the view, updates to the
|
||||
* current bubble should use {@link #update(Bubble)}.
|
||||
*
|
||||
* @param bubble the bubble to display in this view.
|
||||
*/
|
||||
public void setBubble(Bubble bubble) {
|
||||
mBubble = bubble;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param factory Factory for creating normalized bubble icons.
|
||||
*/
|
||||
public void setBubbleIconFactory(BubbleIconFactory factory) {
|
||||
mBubbleIconFactory = factory;
|
||||
}
|
||||
|
||||
/**
|
||||
* The key for the {@link Bubble} associated with this view, if one exists.
|
||||
*/
|
||||
@Nullable
|
||||
public String getKey() {
|
||||
return (mBubble != null) ? mBubble.getKey() : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the UI based on the bubble, updates badge and animates messages as needed.
|
||||
*/
|
||||
public void update(Bubble bubble) {
|
||||
mBubble = bubble;
|
||||
setDotState(DOT_STATE_SUPPRESSED_FOR_FLYOUT);
|
||||
updateViews();
|
||||
}
|
||||
|
||||
int getDotColor() {
|
||||
return mDotColor;
|
||||
}
|
||||
|
||||
/** Sets the position of the 'new' dot, animating it out and back in if requested. */
|
||||
void setDotPosition(boolean onLeft, boolean animate) {
|
||||
if (animate && onLeft != getDotOnLeft() && !isDotHidden()) {
|
||||
animateDot(false /* showDot */, () -> {
|
||||
setDotOnLeft(onLeft);
|
||||
animateDot(true /* showDot */, null);
|
||||
});
|
||||
} else {
|
||||
setDotOnLeft(onLeft);
|
||||
}
|
||||
}
|
||||
|
||||
boolean getDotPositionOnLeft() {
|
||||
return getDotOnLeft();
|
||||
}
|
||||
|
||||
/** Changes the dot's visibility to match the bubble view's state. */
|
||||
void animateDot() {
|
||||
if (mCurrentDotState == DOT_STATE_DEFAULT) {
|
||||
animateDot(mBubble.showDot(), null);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Animates the dot to show or hide.
|
||||
*/
|
||||
private void animateDot(boolean showDot, Runnable after) {
|
||||
if (mDotDrawn == showDot) {
|
||||
// State is consistent, do nothing.
|
||||
return;
|
||||
}
|
||||
|
||||
setDotState(DOT_STATE_ANIMATING);
|
||||
|
||||
// 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);
|
||||
setDotState(DOT_STATE_DEFAULT);
|
||||
if (after != null) {
|
||||
after.run();
|
||||
}
|
||||
}).start();
|
||||
}
|
||||
|
||||
void updateViews() {
|
||||
if (mBubble == null || mBubbleIconFactory == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
Drawable bubbleDrawable = getBubbleDrawable(mContext);
|
||||
BitmapInfo badgeBitmapInfo = mBubbleIconFactory.getBadgedBitmap(mBubble);
|
||||
BitmapInfo bubbleBitmapInfo = mBubbleIconFactory.getBubbleBitmap(bubbleDrawable,
|
||||
badgeBitmapInfo);
|
||||
setImageBitmap(bubbleBitmapInfo.icon);
|
||||
|
||||
// Update badge.
|
||||
mDotColor = ColorUtils.blendARGB(badgeBitmapInfo.color, Color.WHITE, WHITE_SCRIM_ALPHA);
|
||||
setDotColor(mDotColor);
|
||||
|
||||
// Update dot.
|
||||
Path iconPath = PathParser.createPathFromPathData(
|
||||
getResources().getString(com.android.internal.R.string.config_icon_mask));
|
||||
Matrix matrix = new Matrix();
|
||||
float scale = mBubbleIconFactory.getNormalizer().getScale(bubbleDrawable,
|
||||
null /* outBounds */, null /* path */, null /* outMaskShape */);
|
||||
float radius = BadgedImageView.DEFAULT_PATH_SIZE / 2f;
|
||||
matrix.setScale(scale /* x scale */, scale /* y scale */, radius /* pivot x */,
|
||||
radius /* pivot y */);
|
||||
iconPath.transform(matrix);
|
||||
drawDot(iconPath);
|
||||
|
||||
animateDot();
|
||||
}
|
||||
|
||||
Drawable getBubbleDrawable(Context context) {
|
||||
if (mBubble.getShortcutInfo() != null && mBubble.usingShortcutInfo()) {
|
||||
LauncherApps launcherApps =
|
||||
(LauncherApps) getContext().getSystemService(Context.LAUNCHER_APPS_SERVICE);
|
||||
int density = getContext().getResources().getConfiguration().densityDpi;
|
||||
return launcherApps.getShortcutIconDrawable(mBubble.getShortcutInfo(), density);
|
||||
} else {
|
||||
Notification.BubbleMetadata metadata = mBubble.getEntry().getBubbleMetadata();
|
||||
Icon ic = metadata.getIcon();
|
||||
return ic.loadDrawable(context);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -64,8 +64,9 @@ class Bubble {
|
||||
private ShortcutInfo mShortcutInfo;
|
||||
|
||||
private boolean mInflated;
|
||||
private BubbleView mIconView;
|
||||
private BadgedImageView mIconView;
|
||||
private BubbleExpandedView mExpandedView;
|
||||
private BubbleIconFactory mBubbleIconFactory;
|
||||
|
||||
private long mLastUpdated;
|
||||
private long mLastAccessed;
|
||||
@@ -146,7 +147,7 @@ class Bubble {
|
||||
return mAppName;
|
||||
}
|
||||
|
||||
public Drawable getUserBadgedAppIcon() {
|
||||
Drawable getUserBadgedAppIcon() {
|
||||
return mUserBadgedAppIcon;
|
||||
}
|
||||
|
||||
@@ -165,17 +166,15 @@ class Bubble {
|
||||
return BubbleExperimentConfig.isShortcutIntent(getBubbleIntent());
|
||||
}
|
||||
|
||||
void setBubbleIconFactory(BubbleIconFactory factory) {
|
||||
mBubbleIconFactory = factory;
|
||||
}
|
||||
|
||||
boolean isInflated() {
|
||||
return mInflated;
|
||||
}
|
||||
|
||||
void updateDotVisibility() {
|
||||
if (mIconView != null) {
|
||||
mIconView.updateDotVisibility(true /* animate */);
|
||||
}
|
||||
}
|
||||
|
||||
BubbleView getIconView() {
|
||||
BadgedImageView getIconView() {
|
||||
return mIconView;
|
||||
}
|
||||
|
||||
@@ -193,8 +192,9 @@ class Bubble {
|
||||
if (mInflated) {
|
||||
return;
|
||||
}
|
||||
mIconView = (BubbleView) inflater.inflate(
|
||||
mIconView = (BadgedImageView) inflater.inflate(
|
||||
R.layout.bubble_view, stackView, false /* attachToRoot */);
|
||||
mIconView.setBubbleIconFactory(mBubbleIconFactory);
|
||||
mIconView.setBubble(this);
|
||||
|
||||
mExpandedView = (BubbleExpandedView) inflater.inflate(
|
||||
@@ -260,15 +260,15 @@ class Bubble {
|
||||
*/
|
||||
void markAsAccessedAt(long lastAccessedMillis) {
|
||||
mLastAccessed = lastAccessedMillis;
|
||||
setShowInShadeWhenBubble(false);
|
||||
setShowBubbleDot(false);
|
||||
setShowInShade(false);
|
||||
setShowDot(false /* show */, true /* animate */);
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether this notification should be shown in the shade when it is also displayed as a
|
||||
* bubble.
|
||||
*/
|
||||
boolean showInShadeWhenBubble() {
|
||||
boolean showInShade() {
|
||||
return !mEntry.isRowDismissed() && !shouldSuppressNotification()
|
||||
&& (!mEntry.isClearable() || mShowInShadeWhenBubble);
|
||||
}
|
||||
@@ -277,28 +277,33 @@ class Bubble {
|
||||
* Sets whether this notification should be shown in the shade when it is also displayed as a
|
||||
* bubble.
|
||||
*/
|
||||
void setShowInShadeWhenBubble(boolean showInShade) {
|
||||
void setShowInShade(boolean showInShade) {
|
||||
mShowInShadeWhenBubble = showInShade;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets whether the bubble for this notification should show a dot indicating updated content.
|
||||
*/
|
||||
void setShowBubbleDot(boolean showDot) {
|
||||
void setShowDot(boolean showDot, boolean animate) {
|
||||
mShowBubbleUpdateDot = showDot;
|
||||
if (animate && mIconView != null) {
|
||||
mIconView.animateDot();
|
||||
} else if (mIconView != null) {
|
||||
mIconView.invalidate();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether the bubble for this notification should show a dot indicating updated content.
|
||||
*/
|
||||
boolean showBubbleDot() {
|
||||
boolean showDot() {
|
||||
return mShowBubbleUpdateDot && !mEntry.shouldSuppressNotificationDot();
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether the flyout for the bubble should be shown.
|
||||
*/
|
||||
boolean showFlyoutForBubble() {
|
||||
boolean showFlyout() {
|
||||
return !mSuppressFlyout && !mEntry.shouldSuppressPeek()
|
||||
&& !mEntry.shouldSuppressNotificationList();
|
||||
}
|
||||
@@ -470,9 +475,9 @@ class Bubble {
|
||||
public void dump(
|
||||
@NonNull FileDescriptor fd, @NonNull PrintWriter pw, @NonNull String[] args) {
|
||||
pw.print("key: "); pw.println(mKey);
|
||||
pw.print(" showInShade: "); pw.println(showInShadeWhenBubble());
|
||||
pw.print(" showDot: "); pw.println(showBubbleDot());
|
||||
pw.print(" showFlyout: "); pw.println(showFlyoutForBubble());
|
||||
pw.print(" showInShade: "); pw.println(showInShade());
|
||||
pw.print(" showDot: "); pw.println(showDot());
|
||||
pw.print(" showFlyout: "); pw.println(showFlyout());
|
||||
pw.print(" desiredHeight: "); pw.println(getDesiredHeightString());
|
||||
pw.print(" suppressNotif: "); pw.println(shouldSuppressNotification());
|
||||
pw.print(" autoExpand: "); pw.println(shouldAutoExpand());
|
||||
|
||||
@@ -251,15 +251,15 @@ public class BubbleController implements ConfigurationController.ConfigurationLi
|
||||
mZenModeController.addCallback(new ZenModeController.Callback() {
|
||||
@Override
|
||||
public void onZenChanged(int zen) {
|
||||
if (mStackView != null) {
|
||||
mStackView.updateDots();
|
||||
for (Bubble b : mBubbleData.getBubbles()) {
|
||||
b.setShowDot(b.showInShade(), true /* animate */);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onConfigChanged(ZenModeConfig config) {
|
||||
if (mStackView != null) {
|
||||
mStackView.updateDots();
|
||||
for (Bubble b : mBubbleData.getBubbles()) {
|
||||
b.setShowDot(b.showInShade(), true /* animate */);
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -465,7 +465,7 @@ public class BubbleController implements ConfigurationController.ConfigurationLi
|
||||
*/
|
||||
public boolean isBubbleNotificationSuppressedFromShade(String key) {
|
||||
boolean isBubbleAndSuppressed = mBubbleData.hasBubbleWithKey(key)
|
||||
&& !mBubbleData.getBubbleWithKey(key).showInShadeWhenBubble();
|
||||
&& !mBubbleData.getBubbleWithKey(key).showInShade();
|
||||
NotificationEntry entry = mNotificationEntryManager.getActiveNotificationUnfiltered(key);
|
||||
String groupKey = entry != null ? entry.getSbn().getGroupKey() : null;
|
||||
boolean isSuppressedSummary = mBubbleData.isSummarySuppressed(groupKey);
|
||||
@@ -630,11 +630,8 @@ public class BubbleController implements ConfigurationController.ConfigurationLi
|
||||
Bubble bubble = mBubbleData.getBubbleWithKey(key);
|
||||
boolean bubbleExtended = entry != null && entry.isBubble() && userRemovedNotif;
|
||||
if (bubbleExtended) {
|
||||
bubble.setShowInShadeWhenBubble(false);
|
||||
bubble.setShowBubbleDot(false);
|
||||
if (mStackView != null) {
|
||||
mStackView.updateDotVisibility(entry.getKey());
|
||||
}
|
||||
bubble.setShowInShade(false);
|
||||
bubble.setShowDot(false /* show */, true /* animate */);
|
||||
mNotificationEntryManager.updateNotifications(
|
||||
"BubbleController.onNotificationRemoveRequested");
|
||||
return true;
|
||||
@@ -660,11 +657,8 @@ public class BubbleController implements ConfigurationController.ConfigurationLi
|
||||
// As far as group manager is concerned, once a child is no longer shown
|
||||
// in the shade, it is essentially removed.
|
||||
mNotificationGroupManager.onEntryRemoved(bubbleChild.getEntry());
|
||||
bubbleChild.setShowInShadeWhenBubble(false);
|
||||
bubbleChild.setShowBubbleDot(false);
|
||||
if (mStackView != null) {
|
||||
mStackView.updateDotVisibility(bubbleChild.getKey());
|
||||
}
|
||||
bubbleChild.setShowInShade(false);
|
||||
bubbleChild.setShowDot(false /* show */, true /* animate */);
|
||||
}
|
||||
// And since all children are removed, remove the summary.
|
||||
mNotificationGroupManager.onEntryRemoved(summary);
|
||||
@@ -765,7 +759,7 @@ public class BubbleController implements ConfigurationController.ConfigurationLi
|
||||
// If the bubble is removed for user switching, leave the notification in place.
|
||||
if (reason != DISMISS_USER_CHANGED) {
|
||||
if (!mBubbleData.hasBubbleWithKey(bubble.getKey())
|
||||
&& !bubble.showInShadeWhenBubble()) {
|
||||
&& !bubble.showInShade()) {
|
||||
// The bubble is gone & the notification is gone, time to actually remove it
|
||||
mNotificationEntryManager.performRemoveNotification(
|
||||
bubble.getEntry().getSbn(), UNDEFINED_DISMISS_REASON);
|
||||
|
||||
@@ -210,8 +210,8 @@ public class BubbleData {
|
||||
setSelectedBubbleInternal(bubble);
|
||||
}
|
||||
boolean isBubbleExpandedAndSelected = mExpanded && mSelectedBubble == bubble;
|
||||
bubble.setShowInShadeWhenBubble(!isBubbleExpandedAndSelected && showInShade);
|
||||
bubble.setShowBubbleDot(!isBubbleExpandedAndSelected);
|
||||
bubble.setShowInShade(!isBubbleExpandedAndSelected && showInShade);
|
||||
bubble.setShowDot(!isBubbleExpandedAndSelected /* show */, true /* animate */);
|
||||
dispatchPendingChanges();
|
||||
}
|
||||
|
||||
@@ -303,9 +303,8 @@ public class BubbleData {
|
||||
if (notif.getRanking().visuallyInterruptive()) {
|
||||
return true;
|
||||
}
|
||||
final boolean suppressedFromShade = hasBubbleWithKey(notif.getKey())
|
||||
&& !getBubbleWithKey(notif.getKey()).showInShadeWhenBubble();
|
||||
return suppressedFromShade;
|
||||
return hasBubbleWithKey(notif.getKey())
|
||||
&& !getBubbleWithKey(notif.getKey()).showInShade();
|
||||
}
|
||||
|
||||
private void doAdd(Bubble bubble) {
|
||||
|
||||
@@ -624,7 +624,7 @@ public class BubbleExpandedView extends LinearLayout implements View.OnClickList
|
||||
action,
|
||||
mStackView.getNormalizedXPosition(),
|
||||
mStackView.getNormalizedYPosition(),
|
||||
bubble.showInShadeWhenBubble(),
|
||||
bubble.showInShade(),
|
||||
bubble.isOngoing(),
|
||||
false /* isAppForeground (unused) */);
|
||||
}
|
||||
|
||||
@@ -206,7 +206,7 @@ public class BubbleFlyoutView extends FrameLayout {
|
||||
void setupFlyoutStartingAsDot(
|
||||
CharSequence updateMessage, PointF stackPos, float parentWidth,
|
||||
boolean arrowPointingLeft, int dotColor, @Nullable Runnable onLayoutComplete,
|
||||
@Nullable Runnable onHide, float[] dotCenter) {
|
||||
@Nullable Runnable onHide, float[] dotCenter, boolean hideDot) {
|
||||
mArrowPointingLeft = arrowPointingLeft;
|
||||
mDotColor = dotColor;
|
||||
mOnHide = onHide;
|
||||
@@ -242,12 +242,14 @@ public class BubbleFlyoutView extends FrameLayout {
|
||||
|
||||
// Calculate the difference in size between the flyout and the 'dot' so that we can
|
||||
// transform into the dot later.
|
||||
mFlyoutToDotWidthDelta = getWidth() - mNewDotSize;
|
||||
mFlyoutToDotHeightDelta = getHeight() - mNewDotSize;
|
||||
final float newDotSize = hideDot ? 0f : mNewDotSize;
|
||||
mFlyoutToDotWidthDelta = getWidth() - newDotSize;
|
||||
mFlyoutToDotHeightDelta = getHeight() - newDotSize;
|
||||
|
||||
// Calculate the translation values needed to be in the correct 'new dot' position.
|
||||
final float dotPositionX = stackPos.x + mDotCenter[0] - (mOriginalDotSize / 2f);
|
||||
final float dotPositionY = stackPos.y + mDotCenter[1] - (mOriginalDotSize / 2f);
|
||||
final float adjustmentForScaleAway = hideDot ? 0f : (mOriginalDotSize / 2f);
|
||||
final float dotPositionX = stackPos.x + mDotCenter[0] - adjustmentForScaleAway;
|
||||
final float dotPositionY = stackPos.y + mDotCenter[1] - adjustmentForScaleAway;
|
||||
|
||||
final float distanceFromFlyoutLeftToDotCenterX = mRestingTranslationX - dotPositionX;
|
||||
final float distanceFromLayoutTopToDotCenterY = restingTranslationY - dotPositionY;
|
||||
@@ -319,8 +321,7 @@ public class BubbleFlyoutView extends FrameLayout {
|
||||
// percentage.
|
||||
final float width = getWidth() - (mFlyoutToDotWidthDelta * mPercentTransitionedToDot);
|
||||
final float height = getHeight() - (mFlyoutToDotHeightDelta * mPercentTransitionedToDot);
|
||||
final float interpolatedRadius = mNewDotRadius * mPercentTransitionedToDot
|
||||
+ mCornerRadius * (1 - mPercentTransitionedToDot);
|
||||
final float interpolatedRadius = getInterpolatedRadius();
|
||||
|
||||
// Translate the flyout background towards the collapsed 'dot' state.
|
||||
mBgTranslationX = mTranslationXWhenDot * mPercentTransitionedToDot;
|
||||
@@ -387,8 +388,7 @@ public class BubbleFlyoutView extends FrameLayout {
|
||||
if (!mTriangleOutline.isEmpty()) {
|
||||
// Draw the rect into the outline as a path so we can merge the triangle path into it.
|
||||
final Path rectPath = new Path();
|
||||
final float interpolatedRadius = mNewDotRadius * mPercentTransitionedToDot
|
||||
+ mCornerRadius * (1 - mPercentTransitionedToDot);
|
||||
final float interpolatedRadius = getInterpolatedRadius();
|
||||
rectPath.addRoundRect(mBgRect, interpolatedRadius,
|
||||
interpolatedRadius, Path.Direction.CW);
|
||||
outline.setConvexPath(rectPath);
|
||||
@@ -420,4 +420,9 @@ public class BubbleFlyoutView extends FrameLayout {
|
||||
outline.mPath.transform(outlineMatrix);
|
||||
}
|
||||
}
|
||||
|
||||
private float getInterpolatedRadius() {
|
||||
return mNewDotRadius * mPercentTransitionedToDot
|
||||
+ mCornerRadius * (1 - mPercentTransitionedToDot);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,8 +16,14 @@
|
||||
package com.android.systemui.bubbles;
|
||||
|
||||
import android.content.Context;
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.drawable.BitmapDrawable;
|
||||
import android.graphics.drawable.Drawable;
|
||||
|
||||
import com.android.launcher3.icons.BaseIconFactory;
|
||||
import com.android.launcher3.icons.BitmapInfo;
|
||||
import com.android.launcher3.icons.ShadowGenerator;
|
||||
import com.android.systemui.R;
|
||||
|
||||
/**
|
||||
@@ -26,13 +32,37 @@ import com.android.systemui.R;
|
||||
* so there is no need to manage a pool across multiple threads.
|
||||
*/
|
||||
public class BubbleIconFactory extends BaseIconFactory {
|
||||
|
||||
protected BubbleIconFactory(Context context) {
|
||||
super(context, context.getResources().getConfiguration().densityDpi,
|
||||
context.getResources().getDimensionPixelSize(R.dimen.individual_bubble_size));
|
||||
}
|
||||
|
||||
public int getBadgeSize() {
|
||||
int getBadgeSize() {
|
||||
return mContext.getResources().getDimensionPixelSize(
|
||||
com.android.launcher3.icons.R.dimen.profile_badge_size);
|
||||
}
|
||||
|
||||
BitmapInfo getBadgedBitmap(Bubble b) {
|
||||
Bitmap userBadgedBitmap = createIconBitmap(
|
||||
b.getUserBadgedAppIcon(), 1f, getBadgeSize());
|
||||
|
||||
Canvas c = new Canvas();
|
||||
ShadowGenerator shadowGenerator = new ShadowGenerator(getBadgeSize());
|
||||
c.setBitmap(userBadgedBitmap);
|
||||
shadowGenerator.recreateIcon(Bitmap.createBitmap(userBadgedBitmap), c);
|
||||
BitmapInfo bitmapInfo = createIconBitmap(userBadgedBitmap);
|
||||
return bitmapInfo;
|
||||
}
|
||||
|
||||
BitmapInfo getBubbleBitmap(Drawable bubble, BitmapInfo badge) {
|
||||
BitmapInfo bubbleIconInfo = createBadgedIconBitmap(bubble,
|
||||
null /* user */,
|
||||
true /* shrinkNonAdaptiveIcons */);
|
||||
|
||||
badgeWithDrawable(bubbleIconInfo.icon,
|
||||
new BitmapDrawable(mContext.getResources(), badge.icon));
|
||||
return bubbleIconInfo;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -19,6 +19,8 @@ package com.android.systemui.bubbles;
|
||||
import static android.view.ViewGroup.LayoutParams.MATCH_PARENT;
|
||||
import static android.view.ViewGroup.LayoutParams.WRAP_CONTENT;
|
||||
|
||||
import static com.android.systemui.bubbles.BadgedImageView.DOT_STATE_DEFAULT;
|
||||
import static com.android.systemui.bubbles.BadgedImageView.DOT_STATE_SUPPRESSED_FOR_FLYOUT;
|
||||
import static com.android.systemui.bubbles.BubbleDebugConfig.DEBUG_BUBBLE_STACK_VIEW;
|
||||
import static com.android.systemui.bubbles.BubbleDebugConfig.TAG_BUBBLES;
|
||||
import static com.android.systemui.bubbles.BubbleDebugConfig.TAG_WITH_CLASS_NAME;
|
||||
@@ -169,7 +171,7 @@ public class BubbleStackView extends FrameLayout {
|
||||
* Callback to run after the flyout hides. Also called if a new flyout is shown before the
|
||||
* previous one animates out.
|
||||
*/
|
||||
private Runnable mFlyoutOnHide;
|
||||
private Runnable mAfterFlyoutHidden;
|
||||
|
||||
/** Layout change listener that moves the stack to the nearest valid position on rotation. */
|
||||
private OnLayoutChangeListener mOrientationChangedListener;
|
||||
@@ -673,18 +675,6 @@ public class BubbleStackView extends FrameLayout {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the visibility of the 'dot' indicating an update on the bubble.
|
||||
*
|
||||
* @param key the {@link NotificationEntry#key} associated with the bubble.
|
||||
*/
|
||||
public void updateDotVisibility(String key) {
|
||||
Bubble b = mBubbleData.getBubbleWithKey(key);
|
||||
if (b != null) {
|
||||
b.updateDotVisibility();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the listener to notify when the bubble stack is expanded.
|
||||
*/
|
||||
@@ -707,9 +697,9 @@ public class BubbleStackView extends FrameLayout {
|
||||
}
|
||||
|
||||
/**
|
||||
* The {@link BubbleView} that is expanded, null if one does not exist.
|
||||
* The {@link BadgedImageView} that is expanded, null if one does not exist.
|
||||
*/
|
||||
BubbleView getExpandedBubbleView() {
|
||||
BadgedImageView getExpandedBubbleView() {
|
||||
return mExpandedBubble != null ? mExpandedBubble.getIconView() : null;
|
||||
}
|
||||
|
||||
@@ -731,7 +721,7 @@ public class BubbleStackView extends FrameLayout {
|
||||
Bubble bubbleToExpand = mBubbleData.getBubbleWithKey(key);
|
||||
if (bubbleToExpand != null) {
|
||||
setSelectedBubble(bubbleToExpand);
|
||||
bubbleToExpand.setShowInShadeWhenBubble(false);
|
||||
bubbleToExpand.setShowInShade(false);
|
||||
setExpanded(true);
|
||||
}
|
||||
}
|
||||
@@ -746,8 +736,8 @@ public class BubbleStackView extends FrameLayout {
|
||||
mStackOnLeftOrWillBe = mStackAnimationController.isStackOnLeftSide();
|
||||
}
|
||||
|
||||
bubble.setBubbleIconFactory(mBubbleIconFactory);
|
||||
bubble.inflate(mInflater, this);
|
||||
bubble.getIconView().setBubbleIconFactory(mBubbleIconFactory);
|
||||
bubble.getIconView().updateViews();
|
||||
|
||||
// Set the dot position to the opposite of the side the stack is resting on, since the stack
|
||||
@@ -884,7 +874,7 @@ public class BubbleStackView extends FrameLayout {
|
||||
if (isIntersecting(mBubbleContainer, x, y)) {
|
||||
// Could be tapping or dragging a bubble while expanded
|
||||
for (int i = 0; i < mBubbleContainer.getChildCount(); i++) {
|
||||
BubbleView view = (BubbleView) mBubbleContainer.getChildAt(i);
|
||||
BadgedImageView view = (BadgedImageView) mBubbleContainer.getChildAt(i);
|
||||
if (isIntersecting(view, x, y)) {
|
||||
return view;
|
||||
}
|
||||
@@ -1028,9 +1018,9 @@ public class BubbleStackView extends FrameLayout {
|
||||
}
|
||||
|
||||
/** Return the BubbleView at the given index from the bubble container. */
|
||||
public BubbleView getBubbleAt(int i) {
|
||||
public BadgedImageView getBubbleAt(int i) {
|
||||
return mBubbleContainer.getChildCount() > i
|
||||
? (BubbleView) mBubbleContainer.getChildAt(i)
|
||||
? (BadgedImageView) mBubbleContainer.getChildAt(i)
|
||||
: null;
|
||||
}
|
||||
|
||||
@@ -1382,16 +1372,6 @@ public class BubbleStackView extends FrameLayout {
|
||||
: 0f);
|
||||
}
|
||||
|
||||
/** Updates the dot visibility, this is used in response to a zen mode config change. */
|
||||
void updateDots() {
|
||||
int bubbsCount = mBubbleContainer.getChildCount();
|
||||
for (int i = 0; i < bubbsCount; i++) {
|
||||
BubbleView bv = (BubbleView) mBubbleContainer.getChildAt(i);
|
||||
// If nothing changed the animation won't happen
|
||||
bv.updateDotVisibility(true /* animate */);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the y position of the expanded view when it is expanded.
|
||||
*/
|
||||
@@ -1405,37 +1385,40 @@ public class BubbleStackView extends FrameLayout {
|
||||
@VisibleForTesting
|
||||
void animateInFlyoutForBubble(Bubble bubble) {
|
||||
final CharSequence updateMessage = bubble.getUpdateMessage(getContext());
|
||||
if (!bubble.showFlyoutForBubble()) {
|
||||
// In case flyout was suppressed for this update, reset now.
|
||||
bubble.setSuppressFlyout(false);
|
||||
return;
|
||||
}
|
||||
final BadgedImageView bubbleView = bubble.getIconView();
|
||||
if (updateMessage == null
|
||||
|| !bubble.showFlyout()
|
||||
|| isExpanded()
|
||||
|| mIsExpansionAnimating
|
||||
|| mIsGestureInProgress
|
||||
|| mBubbleToExpandAfterFlyoutCollapse != null
|
||||
|| bubble.getIconView() == null) {
|
||||
|| bubbleView == null) {
|
||||
if (bubbleView != null) {
|
||||
bubbleView.setDotState(DOT_STATE_DEFAULT);
|
||||
}
|
||||
// Skip the message if none exists, we're expanded or animating expansion, or we're
|
||||
// about to expand a bubble from the previous tapped flyout, or if bubble view is null.
|
||||
return;
|
||||
}
|
||||
|
||||
mFlyoutDragDeltaX = 0f;
|
||||
clearFlyoutOnHide();
|
||||
mFlyoutOnHide = () -> {
|
||||
resetDot(bubble);
|
||||
if (mBubbleToExpandAfterFlyoutCollapse == null) {
|
||||
return;
|
||||
mAfterFlyoutHidden = () -> {
|
||||
// Null it out to ensure it runs once.
|
||||
mAfterFlyoutHidden = null;
|
||||
|
||||
if (mBubbleToExpandAfterFlyoutCollapse != null) {
|
||||
// User tapped on the flyout and we should expand
|
||||
mBubbleData.setSelectedBubble(mBubbleToExpandAfterFlyoutCollapse);
|
||||
mBubbleData.setExpanded(true);
|
||||
mBubbleToExpandAfterFlyoutCollapse = null;
|
||||
}
|
||||
mBubbleData.setSelectedBubble(mBubbleToExpandAfterFlyoutCollapse);
|
||||
mBubbleData.setExpanded(true);
|
||||
mBubbleToExpandAfterFlyoutCollapse = null;
|
||||
bubbleView.setDotState(DOT_STATE_DEFAULT);
|
||||
};
|
||||
mFlyout.setVisibility(INVISIBLE);
|
||||
|
||||
// Temporarily suppress the dot while the flyout is visible.
|
||||
bubble.getIconView().setSuppressDot(
|
||||
true /* suppressDot */, false /* animate */);
|
||||
// Don't show the dot when we're animating the flyout
|
||||
bubbleView.setDotState(DOT_STATE_SUPPRESSED_FOR_FLYOUT);
|
||||
|
||||
// Start flyout expansion. Post in case layout isn't complete and getWidth returns 0.
|
||||
post(() -> {
|
||||
@@ -1461,8 +1444,9 @@ public class BubbleStackView extends FrameLayout {
|
||||
mStackAnimationController.isStackOnLeftSide(),
|
||||
bubble.getIconView().getDotColor() /* dotColor */,
|
||||
expandFlyoutAfterDelay /* onLayoutComplete */,
|
||||
mFlyoutOnHide,
|
||||
bubble.getIconView().getDotCenter());
|
||||
mAfterFlyoutHidden,
|
||||
bubble.getIconView().getDotCenter(),
|
||||
!bubble.showDot());
|
||||
mFlyout.bringToFront();
|
||||
});
|
||||
mFlyout.removeCallbacks(mHideFlyout);
|
||||
@@ -1470,24 +1454,6 @@ public class BubbleStackView extends FrameLayout {
|
||||
logBubbleEvent(bubble, StatsLog.BUBBLE_UICHANGED__ACTION__FLYOUT);
|
||||
}
|
||||
|
||||
private void resetDot(Bubble bubble) {
|
||||
final boolean suppressDot = !bubble.showBubbleDot();
|
||||
// If we're going to suppress the dot, make it visible first so it'll
|
||||
// visibly animate away.
|
||||
|
||||
if (suppressDot) {
|
||||
bubble.getIconView().setSuppressDot(
|
||||
false /* suppressDot */, false /* animate */);
|
||||
}
|
||||
// Reset dot suppression. If we're not suppressing due to DND, then
|
||||
// stop suppressing it with no animation (since the flyout has
|
||||
// transformed into the dot). If we are suppressing due to DND, animate
|
||||
// it away.
|
||||
bubble.getIconView().setSuppressDot(
|
||||
suppressDot /* suppressDot */,
|
||||
suppressDot /* animate */);
|
||||
}
|
||||
|
||||
/** Hide the flyout immediately and cancel any pending hide runnables. */
|
||||
private void hideFlyoutImmediate() {
|
||||
clearFlyoutOnHide();
|
||||
@@ -1498,11 +1464,11 @@ public class BubbleStackView extends FrameLayout {
|
||||
|
||||
private void clearFlyoutOnHide() {
|
||||
mFlyout.removeCallbacks(mAnimateInFlyout);
|
||||
if (mFlyoutOnHide == null) {
|
||||
if (mAfterFlyoutHidden == null) {
|
||||
return;
|
||||
}
|
||||
mFlyoutOnHide.run();
|
||||
mFlyoutOnHide = null;
|
||||
mAfterFlyoutHidden.run();
|
||||
mAfterFlyoutHidden = null;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -1599,8 +1565,7 @@ public class BubbleStackView extends FrameLayout {
|
||||
private void updateBubbleZOrdersAndDotPosition(boolean animate) {
|
||||
int bubbleCount = mBubbleContainer.getChildCount();
|
||||
for (int i = 0; i < bubbleCount; i++) {
|
||||
BubbleView bv = (BubbleView) mBubbleContainer.getChildAt(i);
|
||||
bv.updateDotVisibility(true /* animate */);
|
||||
BadgedImageView bv = (BadgedImageView) mBubbleContainer.getChildAt(i);
|
||||
bv.setZ((mMaxBubbles * mBubbleElevation) - i);
|
||||
// If the dot is on the left, and so is the stack, we need to change the dot position.
|
||||
if (bv.getDotPositionOnLeft() == mStackOnLeftOrWillBe) {
|
||||
@@ -1705,7 +1670,7 @@ public class BubbleStackView extends FrameLayout {
|
||||
action,
|
||||
getNormalizedXPosition(),
|
||||
getNormalizedYPosition(),
|
||||
bubble.showInShadeWhenBubble(),
|
||||
bubble.showInShade(),
|
||||
bubble.isOngoing(),
|
||||
false /* isAppForeground (unused) */);
|
||||
}
|
||||
@@ -1727,8 +1692,8 @@ public class BubbleStackView extends FrameLayout {
|
||||
List<Bubble> bubbles = new ArrayList<>();
|
||||
for (int i = 0; i < mBubbleContainer.getChildCount(); i++) {
|
||||
View child = mBubbleContainer.getChildAt(i);
|
||||
if (child instanceof BubbleView) {
|
||||
String key = ((BubbleView) child).getKey();
|
||||
if (child instanceof BadgedImageView) {
|
||||
String key = ((BadgedImageView) child).getKey();
|
||||
Bubble bubble = mBubbleData.getBubbleWithKey(key);
|
||||
bubbles.add(bubble);
|
||||
}
|
||||
|
||||
@@ -95,7 +95,7 @@ class BubbleTouchHandler implements View.OnTouchListener {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!(mTouchedView instanceof BubbleView)
|
||||
if (!(mTouchedView instanceof BadgedImageView)
|
||||
&& !(mTouchedView instanceof BubbleStackView)
|
||||
&& !(mTouchedView instanceof BubbleFlyoutView)) {
|
||||
// Not touching anything touchable, but we shouldn't collapse (e.g. touching edge
|
||||
@@ -187,7 +187,7 @@ class BubbleTouchHandler implements View.OnTouchListener {
|
||||
mStack.onFlyoutDragFinished(rawX - mTouchDown.x /* deltaX */, velX);
|
||||
} else if (shouldDismiss) {
|
||||
final String individualBubbleKey =
|
||||
isStack ? null : ((BubbleView) mTouchedView).getKey();
|
||||
isStack ? null : ((BadgedImageView) mTouchedView).getKey();
|
||||
mStack.magnetToStackIfNeededThenAnimateDismissal(mTouchedView, velX, velY,
|
||||
() -> {
|
||||
if (isStack) {
|
||||
@@ -214,7 +214,7 @@ class BubbleTouchHandler implements View.OnTouchListener {
|
||||
// Toggle expansion
|
||||
mBubbleData.setExpanded(!mBubbleData.isExpanded());
|
||||
} else {
|
||||
final String key = ((BubbleView) mTouchedView).getKey();
|
||||
final String key = ((BadgedImageView) mTouchedView).getKey();
|
||||
mBubbleData.setSelectedBubble(mBubbleData.getBubbleWithKey(key));
|
||||
}
|
||||
|
||||
|
||||
@@ -1,280 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2018 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.systemui.bubbles;
|
||||
|
||||
import android.annotation.Nullable;
|
||||
import android.app.Notification;
|
||||
import android.content.Context;
|
||||
import android.content.pm.LauncherApps;
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.Color;
|
||||
import android.graphics.Matrix;
|
||||
import android.graphics.Path;
|
||||
import android.graphics.drawable.BitmapDrawable;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.graphics.drawable.Icon;
|
||||
import android.util.AttributeSet;
|
||||
import android.util.PathParser;
|
||||
import android.widget.FrameLayout;
|
||||
|
||||
import com.android.internal.graphics.ColorUtils;
|
||||
import com.android.launcher3.icons.BitmapInfo;
|
||||
import com.android.launcher3.icons.ColorExtractor;
|
||||
import com.android.launcher3.icons.ShadowGenerator;
|
||||
import com.android.systemui.Interpolators;
|
||||
import com.android.systemui.R;
|
||||
import com.android.systemui.statusbar.notification.collection.NotificationEntry;
|
||||
|
||||
/**
|
||||
* A floating object on the screen that can post message updates.
|
||||
*/
|
||||
public class BubbleView extends FrameLayout {
|
||||
|
||||
// Same value as Launcher3 badge code
|
||||
private static final float WHITE_SCRIM_ALPHA = 0.54f;
|
||||
private Context mContext;
|
||||
|
||||
private BadgedImageView mBadgedImageView;
|
||||
private int mDotColor;
|
||||
private ColorExtractor mColorExtractor;
|
||||
|
||||
// mBubbleIconFactory cannot be static because it depends on Context.
|
||||
private BubbleIconFactory mBubbleIconFactory;
|
||||
|
||||
private boolean mSuppressDot;
|
||||
|
||||
private Bubble mBubble;
|
||||
|
||||
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);
|
||||
mContext = context;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onFinishInflate() {
|
||||
super.onFinishInflate();
|
||||
mBadgedImageView = findViewById(R.id.bubble_image);
|
||||
mColorExtractor = new ColorExtractor();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onAttachedToWindow() {
|
||||
super.onAttachedToWindow();
|
||||
}
|
||||
|
||||
/**
|
||||
* Populates this view with a bubble.
|
||||
* <p>
|
||||
* This should only be called when a new bubble is being set on the view, updates to the
|
||||
* current bubble should use {@link #update(Bubble)}.
|
||||
*
|
||||
* @param bubble the bubble to display in this view.
|
||||
*/
|
||||
public void setBubble(Bubble bubble) {
|
||||
mBubble = bubble;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param factory Factory for creating normalized bubble icons.
|
||||
*/
|
||||
public void setBubbleIconFactory(BubbleIconFactory factory) {
|
||||
mBubbleIconFactory = factory;
|
||||
}
|
||||
|
||||
/**
|
||||
* The {@link NotificationEntry} associated with this view, if one exists.
|
||||
*/
|
||||
@Nullable
|
||||
public NotificationEntry getEntry() {
|
||||
return mBubble != null ? mBubble.getEntry() : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* The key for the {@link NotificationEntry} associated with this view, if one exists.
|
||||
*/
|
||||
@Nullable
|
||||
public String getKey() {
|
||||
return (mBubble != null) ? mBubble.getKey() : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the UI based on the bubble, updates badge and animates messages as needed.
|
||||
*/
|
||||
public void update(Bubble bubble) {
|
||||
mBubble = bubble;
|
||||
updateViews();
|
||||
}
|
||||
|
||||
/** Changes the dot's visibility to match the bubble view's state. */
|
||||
void updateDotVisibility(boolean animate) {
|
||||
updateDotVisibility(animate, null /* after */);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets whether or not to hide the dot even if we'd otherwise show it. This is used while the
|
||||
* flyout is visible or animating, to hide the dot until the flyout visually transforms into it.
|
||||
*/
|
||||
void setSuppressDot(boolean suppressDot, boolean animate) {
|
||||
mSuppressDot = suppressDot;
|
||||
updateDotVisibility(animate);
|
||||
}
|
||||
|
||||
boolean isDotShowing() {
|
||||
return mBubble.showBubbleDot() && !mSuppressDot;
|
||||
}
|
||||
|
||||
int getDotColor() {
|
||||
return mDotColor;
|
||||
}
|
||||
|
||||
/** Sets the position of the 'new' dot, animating it out and back in if requested. */
|
||||
void setDotPosition(boolean onLeft, boolean animate) {
|
||||
if (animate && onLeft != mBadgedImageView.getDotOnLeft() && isDotShowing()) {
|
||||
animateDot(false /* showDot */, () -> {
|
||||
mBadgedImageView.setDotOnLeft(onLeft);
|
||||
animateDot(true /* showDot */, null);
|
||||
});
|
||||
} else {
|
||||
mBadgedImageView.setDotOnLeft(onLeft);
|
||||
}
|
||||
}
|
||||
|
||||
float[] getDotCenter() {
|
||||
float[] unscaled = mBadgedImageView.getDotCenter();
|
||||
return new float[]{unscaled[0], unscaled[1]};
|
||||
}
|
||||
|
||||
boolean getDotPositionOnLeft() {
|
||||
return mBadgedImageView.getDotOnLeft();
|
||||
}
|
||||
|
||||
/**
|
||||
* Changes the dot's visibility to match the bubble view's state, running the provided callback
|
||||
* after animation if requested.
|
||||
*/
|
||||
private void updateDotVisibility(boolean animate, Runnable after) {
|
||||
final boolean showDot = isDotShowing();
|
||||
if (animate) {
|
||||
animateDot(showDot, after);
|
||||
} else {
|
||||
mBadgedImageView.setShowDot(showDot);
|
||||
mBadgedImageView.setDotScale(showDot ? 1f : 0f);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Animates the badge to show or hide.
|
||||
*/
|
||||
private void animateDot(boolean showDot, Runnable after) {
|
||||
if (mBadgedImageView.isShowingDot() == showDot) {
|
||||
return;
|
||||
}
|
||||
// Do NOT wait until after animation ends to setShowDot
|
||||
// to avoid overriding more recent showDot states.
|
||||
mBadgedImageView.setShowDot(showDot);
|
||||
mBadgedImageView.clearAnimation();
|
||||
mBadgedImageView.animate().setDuration(200)
|
||||
.setInterpolator(Interpolators.FAST_OUT_SLOW_IN)
|
||||
.setUpdateListener((valueAnimator) -> {
|
||||
float fraction = valueAnimator.getAnimatedFraction();
|
||||
fraction = showDot ? fraction : 1f - fraction;
|
||||
mBadgedImageView.setDotScale(fraction);
|
||||
}).withEndAction(() -> {
|
||||
mBadgedImageView.setDotScale(showDot ? 1f : 0f);
|
||||
if (after != null) {
|
||||
after.run();
|
||||
}
|
||||
}).start();
|
||||
}
|
||||
|
||||
void updateViews() {
|
||||
if (mBubble == null || mBubbleIconFactory == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
Drawable bubbleDrawable = getBubbleDrawable(mContext);
|
||||
BitmapInfo badgeBitmapInfo = getBadgedBitmap();
|
||||
BitmapInfo bubbleBitmapInfo = getBubbleBitmap(bubbleDrawable, badgeBitmapInfo);
|
||||
mBadgedImageView.setImageBitmap(bubbleBitmapInfo.icon);
|
||||
|
||||
// Update badge.
|
||||
mDotColor = ColorUtils.blendARGB(badgeBitmapInfo.color, Color.WHITE, WHITE_SCRIM_ALPHA);
|
||||
mBadgedImageView.setDotColor(mDotColor);
|
||||
|
||||
// Update dot.
|
||||
Path iconPath = PathParser.createPathFromPathData(
|
||||
getResources().getString(com.android.internal.R.string.config_icon_mask));
|
||||
Matrix matrix = new Matrix();
|
||||
float scale = mBubbleIconFactory.getNormalizer().getScale(bubbleDrawable,
|
||||
null /* outBounds */, null /* path */, null /* outMaskShape */);
|
||||
float radius = BadgedImageView.DEFAULT_PATH_SIZE / 2f;
|
||||
matrix.setScale(scale /* x scale */, scale /* y scale */, radius /* pivot x */,
|
||||
radius /* pivot y */);
|
||||
iconPath.transform(matrix);
|
||||
mBadgedImageView.drawDot(iconPath);
|
||||
|
||||
animateDot(isDotShowing(), null /* after */);
|
||||
}
|
||||
|
||||
Drawable getBubbleDrawable(Context context) {
|
||||
if (mBubble.getShortcutInfo() != null && mBubble.usingShortcutInfo()) {
|
||||
LauncherApps launcherApps =
|
||||
(LauncherApps) getContext().getSystemService(Context.LAUNCHER_APPS_SERVICE);
|
||||
int density = getContext().getResources().getConfiguration().densityDpi;
|
||||
return launcherApps.getShortcutIconDrawable(mBubble.getShortcutInfo(), density);
|
||||
} else {
|
||||
Notification.BubbleMetadata metadata = getEntry().getBubbleMetadata();
|
||||
Icon ic = metadata.getIcon();
|
||||
return ic.loadDrawable(context);
|
||||
}
|
||||
}
|
||||
|
||||
BitmapInfo getBadgedBitmap() {
|
||||
Bitmap userBadgedBitmap = mBubbleIconFactory.createIconBitmap(
|
||||
mBubble.getUserBadgedAppIcon(), 1f, mBubbleIconFactory.getBadgeSize());
|
||||
|
||||
Canvas c = new Canvas();
|
||||
ShadowGenerator shadowGenerator = new ShadowGenerator(mBubbleIconFactory.getBadgeSize());
|
||||
c.setBitmap(userBadgedBitmap);
|
||||
shadowGenerator.recreateIcon(Bitmap.createBitmap(userBadgedBitmap), c);
|
||||
BitmapInfo bitmapInfo = mBubbleIconFactory.createIconBitmap(userBadgedBitmap);
|
||||
return bitmapInfo;
|
||||
}
|
||||
|
||||
BitmapInfo getBubbleBitmap(Drawable bubble, BitmapInfo badge) {
|
||||
BitmapInfo bubbleIconInfo = mBubbleIconFactory.createBadgedIconBitmap(bubble,
|
||||
null /* user */,
|
||||
true /* shrinkNonAdaptiveIcons */);
|
||||
|
||||
mBubbleIconFactory.badgeWithDrawable(bubbleIconInfo.icon,
|
||||
new BitmapDrawable(mContext.getResources(), badge.icon));
|
||||
return bubbleIconInfo;
|
||||
}
|
||||
}
|
||||
@@ -157,9 +157,14 @@ public class BubbleControllerTest extends SysuiTestCase {
|
||||
private SuperStatusBarViewFactory mSuperStatusBarViewFactory;
|
||||
private BubbleData mBubbleData;
|
||||
|
||||
private TestableLooper mTestableLooper;
|
||||
|
||||
@Before
|
||||
public void setUp() throws Exception {
|
||||
MockitoAnnotations.initMocks(this);
|
||||
|
||||
mTestableLooper = TestableLooper.get(this);
|
||||
|
||||
mContext.addMockSystemService(FaceManager.class, mFaceManager);
|
||||
when(mColorExtractor.getNeutralColors()).thenReturn(mGradientColors);
|
||||
|
||||
@@ -262,7 +267,7 @@ public class BubbleControllerTest extends SysuiTestCase {
|
||||
mRow.getEntry().getKey()));
|
||||
|
||||
// Make it look like dismissed notif
|
||||
mBubbleData.getBubbleWithKey(mRow.getEntry().getKey()).setShowInShadeWhenBubble(false);
|
||||
mBubbleData.getBubbleWithKey(mRow.getEntry().getKey()).setShowInShade(false);
|
||||
|
||||
// Now remove the bubble
|
||||
mBubbleController.removeBubble(
|
||||
@@ -346,14 +351,14 @@ public class BubbleControllerTest extends SysuiTestCase {
|
||||
verify(mBubbleExpandListener).onBubbleExpandChanged(true, mRow2.getEntry().getKey());
|
||||
|
||||
// Last added is the one that is expanded
|
||||
assertEquals(mRow2.getEntry(), stackView.getExpandedBubbleView().getEntry());
|
||||
assertEquals(mRow2.getEntry(), mBubbleData.getSelectedBubble().getEntry());
|
||||
assertTrue(mBubbleController.isBubbleNotificationSuppressedFromShade(
|
||||
mRow2.getEntry().getKey()));
|
||||
|
||||
// Switch which bubble is expanded
|
||||
mBubbleController.selectBubble(mRow.getEntry().getKey());
|
||||
stackView.setExpandedBubble(mRow.getEntry().getKey());
|
||||
assertEquals(mRow.getEntry(), stackView.getExpandedBubbleView().getEntry());
|
||||
assertEquals(mRow.getEntry(), stackView.getExpandedBubble().getEntry());
|
||||
assertTrue(mBubbleController.isBubbleNotificationSuppressedFromShade(
|
||||
mRow.getEntry().getKey()));
|
||||
|
||||
@@ -377,7 +382,9 @@ public class BubbleControllerTest extends SysuiTestCase {
|
||||
assertTrue(mBubbleController.hasBubbles());
|
||||
assertFalse(mBubbleController.isBubbleNotificationSuppressedFromShade(
|
||||
mRow.getEntry().getKey()));
|
||||
assertTrue(mBubbleData.getBubbleWithKey(mRow.getEntry().getKey()).showBubbleDot());
|
||||
|
||||
mTestableLooper.processAllMessages();
|
||||
assertTrue(mBubbleData.getBubbleWithKey(mRow.getEntry().getKey()).showDot());
|
||||
|
||||
// Expand
|
||||
mBubbleController.expandStack();
|
||||
@@ -388,7 +395,7 @@ public class BubbleControllerTest extends SysuiTestCase {
|
||||
assertTrue(mBubbleController.isBubbleNotificationSuppressedFromShade(
|
||||
mRow.getEntry().getKey()));
|
||||
// Notif shouldn't show dot after expansion
|
||||
assertFalse(mBubbleData.getBubbleWithKey(mRow.getEntry().getKey()).showBubbleDot());
|
||||
assertFalse(mBubbleData.getBubbleWithKey(mRow.getEntry().getKey()).showDot());
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -401,10 +408,11 @@ public class BubbleControllerTest extends SysuiTestCase {
|
||||
assertTrue(mBubbleController.hasBubbles());
|
||||
assertFalse(mBubbleController.isBubbleNotificationSuppressedFromShade(
|
||||
mRow.getEntry().getKey()));
|
||||
assertTrue(mBubbleData.getBubbleWithKey(mRow.getEntry().getKey()).showBubbleDot());
|
||||
|
||||
mTestableLooper.processAllMessages();
|
||||
assertTrue(mBubbleData.getBubbleWithKey(mRow.getEntry().getKey()).showDot());
|
||||
|
||||
// Expand
|
||||
BubbleStackView stackView = mBubbleController.getStackView();
|
||||
mBubbleController.expandStack();
|
||||
assertTrue(mBubbleController.isStackExpanded());
|
||||
verify(mBubbleExpandListener).onBubbleExpandChanged(true, mRow.getEntry().getKey());
|
||||
@@ -413,7 +421,7 @@ public class BubbleControllerTest extends SysuiTestCase {
|
||||
assertTrue(mBubbleController.isBubbleNotificationSuppressedFromShade(
|
||||
mRow.getEntry().getKey()));
|
||||
// Notif shouldn't show dot after expansion
|
||||
assertFalse(mBubbleData.getBubbleWithKey(mRow.getEntry().getKey()).showBubbleDot());
|
||||
assertFalse(mBubbleData.getBubbleWithKey(mRow.getEntry().getKey()).showDot());
|
||||
|
||||
// Send update
|
||||
mEntryListener.onPreEntryUpdated(mRow.getEntry());
|
||||
@@ -423,7 +431,7 @@ public class BubbleControllerTest extends SysuiTestCase {
|
||||
assertTrue(mBubbleController.isBubbleNotificationSuppressedFromShade(
|
||||
mRow.getEntry().getKey()));
|
||||
// Notif shouldn't show dot after expansion
|
||||
assertFalse(mBubbleData.getBubbleWithKey(mRow.getEntry().getKey()).showBubbleDot());
|
||||
assertFalse(mBubbleData.getBubbleWithKey(mRow.getEntry().getKey()).showDot());
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -443,7 +451,7 @@ public class BubbleControllerTest extends SysuiTestCase {
|
||||
verify(mBubbleExpandListener).onBubbleExpandChanged(true, mRow2.getEntry().getKey());
|
||||
|
||||
// Last added is the one that is expanded
|
||||
assertEquals(mRow2.getEntry(), stackView.getExpandedBubbleView().getEntry());
|
||||
assertEquals(mRow2.getEntry(), stackView.getExpandedBubble().getEntry());
|
||||
assertTrue(mBubbleController.isBubbleNotificationSuppressedFromShade(
|
||||
mRow2.getEntry().getKey()));
|
||||
|
||||
@@ -453,7 +461,7 @@ public class BubbleControllerTest extends SysuiTestCase {
|
||||
verify(mBubbleExpandListener).onBubbleExpandChanged(false, mRow2.getEntry().getKey());
|
||||
|
||||
// Make sure first bubble is selected
|
||||
assertEquals(mRow.getEntry(), stackView.getExpandedBubbleView().getEntry());
|
||||
assertEquals(mRow.getEntry(), stackView.getExpandedBubble().getEntry());
|
||||
verify(mBubbleExpandListener).onBubbleExpandChanged(true, mRow.getEntry().getKey());
|
||||
|
||||
// Dismiss that one
|
||||
@@ -555,7 +563,9 @@ public class BubbleControllerTest extends SysuiTestCase {
|
||||
mEntryListener.onPendingEntryAdded(mRow.getEntry());
|
||||
assertFalse(mBubbleController.isBubbleNotificationSuppressedFromShade(
|
||||
mRow.getEntry().getKey()));
|
||||
assertTrue(mBubbleData.getBubbleWithKey(mRow.getEntry().getKey()).showBubbleDot());
|
||||
|
||||
mTestableLooper.processAllMessages();
|
||||
assertTrue(mBubbleData.getBubbleWithKey(mRow.getEntry().getKey()).showDot());
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
@@ -183,7 +183,7 @@ public class BubbleDataTest extends SysuiTestCase {
|
||||
// Verify
|
||||
verifyUpdateReceived();
|
||||
BubbleData.Update update = mUpdateCaptor.getValue();
|
||||
assertThat(update.addedBubble.showFlyoutForBubble()).isFalse();
|
||||
assertThat(update.addedBubble.showFlyout()).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -199,7 +199,7 @@ public class BubbleDataTest extends SysuiTestCase {
|
||||
// Verify
|
||||
verifyUpdateReceived();
|
||||
BubbleData.Update update = mUpdateCaptor.getValue();
|
||||
assertThat(update.addedBubble.showFlyoutForBubble()).isTrue();
|
||||
assertThat(update.addedBubble.showFlyout()).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -218,7 +218,7 @@ public class BubbleDataTest extends SysuiTestCase {
|
||||
|
||||
// Verify
|
||||
BubbleData.Update update = mUpdateCaptor.getValue();
|
||||
assertThat(update.updatedBubble.showFlyoutForBubble()).isFalse();
|
||||
assertThat(update.updatedBubble.showFlyout()).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -239,7 +239,7 @@ public class BubbleDataTest extends SysuiTestCase {
|
||||
|
||||
// Verify
|
||||
BubbleData.Update update = mUpdateCaptor.getValue();
|
||||
assertThat(update.updatedBubble.showFlyoutForBubble()).isTrue();
|
||||
assertThat(update.updatedBubble.showFlyout()).isTrue();
|
||||
}
|
||||
|
||||
// COLLAPSED / ADD
|
||||
|
||||
@@ -60,7 +60,8 @@ public class BubbleFlyoutViewTest extends SysuiTestCase {
|
||||
@Test
|
||||
public void testShowFlyout_isVisible() {
|
||||
mFlyout.setupFlyoutStartingAsDot(
|
||||
"Hello", new PointF(100, 100), 500, true, Color.WHITE, null, null, mDotCenter);
|
||||
"Hello", new PointF(100, 100), 500, true, Color.WHITE, null, null, mDotCenter,
|
||||
false);
|
||||
mFlyout.setVisibility(View.VISIBLE);
|
||||
|
||||
assertEquals("Hello", mFlyoutText.getText());
|
||||
@@ -71,7 +72,8 @@ public class BubbleFlyoutViewTest extends SysuiTestCase {
|
||||
public void testFlyoutHide_runsCallback() {
|
||||
Runnable after = Mockito.mock(Runnable.class);
|
||||
mFlyout.setupFlyoutStartingAsDot(
|
||||
"Hello", new PointF(100, 100), 500, true, Color.WHITE, null, after, mDotCenter);
|
||||
"Hello", new PointF(100, 100), 500, true, Color.WHITE, null, after, mDotCenter,
|
||||
false);
|
||||
mFlyout.hideFlyout();
|
||||
|
||||
verify(after).run();
|
||||
@@ -80,7 +82,8 @@ public class BubbleFlyoutViewTest extends SysuiTestCase {
|
||||
@Test
|
||||
public void testSetCollapsePercent() {
|
||||
mFlyout.setupFlyoutStartingAsDot(
|
||||
"Hello", new PointF(100, 100), 500, true, Color.WHITE, null, null, mDotCenter);
|
||||
"Hello", new PointF(100, 100), 500, true, Color.WHITE, null, null, mDotCenter,
|
||||
false);
|
||||
mFlyout.setVisibility(View.VISIBLE);
|
||||
|
||||
mFlyout.setCollapsePercent(1f);
|
||||
|
||||
Reference in New Issue
Block a user