Loading packages/SystemUI/res/layout/bubble_view.xml +4 −12 Original line number Diff line number Diff line Loading @@ -14,16 +14,8 @@ ~ See the License for the specific language governing permissions and ~ limitations under the License --> <com.android.systemui.bubbles.BubbleView 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" xmlns:android="http://schemas.android.com/apk/res/android" android:id="@+id/bubble_view" android:layout_width="@dimen/individual_bubble_size" android:layout_height="@dimen/individual_bubble_size" android:clipToPadding="false"/> </com.android.systemui.bubbles.BubbleView> android:layout_height="@dimen/individual_bubble_size"/> packages/SystemUI/src/com/android/systemui/bubbles/BadgedImageView.java +200 −36 Original line number Diff line number Diff line Loading @@ -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 boolean mOnLeft; private int mDotColor; private float mDotScale = 0f; private boolean mShowDot; private boolean mOnLeft; private boolean mDotDrawn; /** Same as value in Launcher3 IconShape */ static final int DEFAULT_PATH_SIZE = 100; private Rect mTempBounds = new Rect(); public BadgedImageView(Context context) { this(context, null); Loading @@ -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; Loading @@ -81,46 +109,40 @@ 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); } /** * Set whether the dot should appear on left or right side of the view. * Sets the dot state, does not animate changes. */ void setDotOnLeft(boolean onLeft) { mOnLeft = onLeft; void setDotState(int state) { mCurrentDotState = state; if (state == DOT_STATE_SUPPRESSED_FOR_FLYOUT || state == DOT_STATE_DEFAULT) { mDotScale = mBubble.showDot() ? 1f : 0f; invalidate(); } boolean getDotOnLeft() { return mOnLeft; } /** * Set whether the dot should show or not. * Whether the dot should be hidden based on current dot state. */ void setShowDot(boolean showDot) { mShowDot = showDot; invalidate(); private boolean isDotHidden() { return (mCurrentDotState == DOT_STATE_DEFAULT && !mBubble.showDot()) || mCurrentDotState == DOT_STATE_SUPPRESSED_FOR_FLYOUT; } /** * @return whether the dot is being displayed. * Set whether the dot should appear on left or right side of the view. */ boolean isShowingDot() { return mShowDot; void setDotOnLeft(boolean onLeft) { mOnLeft = onLeft; invalidate(); } /** * The colour to use for the dot. */ public void setDotColor(int color) { void setDotColor(int color) { mDotColor = ColorUtils.setAlphaComponent(color, 255 /* alpha */); invalidate(); } Loading @@ -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(); } Loading @@ -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. */ Loading @@ -156,4 +185,139 @@ public class BadgedImageView extends ImageView { 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); } } } packages/SystemUI/src/com/android/systemui/bubbles/Bubble.java +25 −20 Original line number Diff line number Diff line Loading @@ -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; Loading Loading @@ -146,7 +147,7 @@ class Bubble { return mAppName; } public Drawable getUserBadgedAppIcon() { Drawable getUserBadgedAppIcon() { return mUserBadgedAppIcon; } Loading @@ -165,17 +166,15 @@ class Bubble { return BubbleExperimentConfig.isShortcutIntent(getBubbleIntent()); } boolean isInflated() { return mInflated; void setBubbleIconFactory(BubbleIconFactory factory) { mBubbleIconFactory = factory; } void updateDotVisibility() { if (mIconView != null) { mIconView.updateDotVisibility(true /* animate */); } boolean isInflated() { return mInflated; } BubbleView getIconView() { BadgedImageView getIconView() { return mIconView; } Loading @@ -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( Loading Loading @@ -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); } Loading @@ -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(); } Loading Loading @@ -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()); Loading packages/SystemUI/src/com/android/systemui/bubbles/BubbleController.java +10 −16 Original line number Diff line number Diff line Loading @@ -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 */); } } }); Loading Loading @@ -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); Loading Loading @@ -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; Loading @@ -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); Loading Loading @@ -767,7 +761,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); Loading packages/SystemUI/src/com/android/systemui/bubbles/BubbleData.java +4 −5 Original line number Diff line number Diff line Loading @@ -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(); } Loading Loading @@ -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) { Loading Loading
packages/SystemUI/res/layout/bubble_view.xml +4 −12 Original line number Diff line number Diff line Loading @@ -14,16 +14,8 @@ ~ See the License for the specific language governing permissions and ~ limitations under the License --> <com.android.systemui.bubbles.BubbleView 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" xmlns:android="http://schemas.android.com/apk/res/android" android:id="@+id/bubble_view" android:layout_width="@dimen/individual_bubble_size" android:layout_height="@dimen/individual_bubble_size" android:clipToPadding="false"/> </com.android.systemui.bubbles.BubbleView> android:layout_height="@dimen/individual_bubble_size"/>
packages/SystemUI/src/com/android/systemui/bubbles/BadgedImageView.java +200 −36 Original line number Diff line number Diff line Loading @@ -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 boolean mOnLeft; private int mDotColor; private float mDotScale = 0f; private boolean mShowDot; private boolean mOnLeft; private boolean mDotDrawn; /** Same as value in Launcher3 IconShape */ static final int DEFAULT_PATH_SIZE = 100; private Rect mTempBounds = new Rect(); public BadgedImageView(Context context) { this(context, null); Loading @@ -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; Loading @@ -81,46 +109,40 @@ 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); } /** * Set whether the dot should appear on left or right side of the view. * Sets the dot state, does not animate changes. */ void setDotOnLeft(boolean onLeft) { mOnLeft = onLeft; void setDotState(int state) { mCurrentDotState = state; if (state == DOT_STATE_SUPPRESSED_FOR_FLYOUT || state == DOT_STATE_DEFAULT) { mDotScale = mBubble.showDot() ? 1f : 0f; invalidate(); } boolean getDotOnLeft() { return mOnLeft; } /** * Set whether the dot should show or not. * Whether the dot should be hidden based on current dot state. */ void setShowDot(boolean showDot) { mShowDot = showDot; invalidate(); private boolean isDotHidden() { return (mCurrentDotState == DOT_STATE_DEFAULT && !mBubble.showDot()) || mCurrentDotState == DOT_STATE_SUPPRESSED_FOR_FLYOUT; } /** * @return whether the dot is being displayed. * Set whether the dot should appear on left or right side of the view. */ boolean isShowingDot() { return mShowDot; void setDotOnLeft(boolean onLeft) { mOnLeft = onLeft; invalidate(); } /** * The colour to use for the dot. */ public void setDotColor(int color) { void setDotColor(int color) { mDotColor = ColorUtils.setAlphaComponent(color, 255 /* alpha */); invalidate(); } Loading @@ -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(); } Loading @@ -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. */ Loading @@ -156,4 +185,139 @@ public class BadgedImageView extends ImageView { 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); } } }
packages/SystemUI/src/com/android/systemui/bubbles/Bubble.java +25 −20 Original line number Diff line number Diff line Loading @@ -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; Loading Loading @@ -146,7 +147,7 @@ class Bubble { return mAppName; } public Drawable getUserBadgedAppIcon() { Drawable getUserBadgedAppIcon() { return mUserBadgedAppIcon; } Loading @@ -165,17 +166,15 @@ class Bubble { return BubbleExperimentConfig.isShortcutIntent(getBubbleIntent()); } boolean isInflated() { return mInflated; void setBubbleIconFactory(BubbleIconFactory factory) { mBubbleIconFactory = factory; } void updateDotVisibility() { if (mIconView != null) { mIconView.updateDotVisibility(true /* animate */); } boolean isInflated() { return mInflated; } BubbleView getIconView() { BadgedImageView getIconView() { return mIconView; } Loading @@ -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( Loading Loading @@ -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); } Loading @@ -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(); } Loading Loading @@ -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()); Loading
packages/SystemUI/src/com/android/systemui/bubbles/BubbleController.java +10 −16 Original line number Diff line number Diff line Loading @@ -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 */); } } }); Loading Loading @@ -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); Loading Loading @@ -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; Loading @@ -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); Loading Loading @@ -767,7 +761,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); Loading
packages/SystemUI/src/com/android/systemui/bubbles/BubbleData.java +4 −5 Original line number Diff line number Diff line Loading @@ -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(); } Loading Loading @@ -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) { Loading