Flexible maximum number of bubbles to show
* adjust the default spacing so that its small enough that 5 bubbles
can show on crosshatch (5 is the default)
* Add max bubbles to BubblePositioner, replace the other usages
Test: atest BubbleDataTest
Test: manual - have 5 bubbles
- adjust screen size to largest
- open the stack
=> notice that the number of bubbles have changed and
they all fit on the screen without overlap
- repeat & check the different densities
Bug: 187715444
Change-Id: I4794ed89541eeb573c367748f980cf7ffe45d3f3
This commit is contained in:
@@ -102,7 +102,7 @@
|
||||
<!-- Padding between status bar and bubbles when displayed in expanded state -->
|
||||
<dimen name="bubble_padding_top">16dp</dimen>
|
||||
<!-- Space between bubbles when expanded. -->
|
||||
<dimen name="bubble_spacing">8dp</dimen>
|
||||
<dimen name="bubble_spacing">3dp</dimen>
|
||||
<!-- Size of the bubble. -->
|
||||
<dimen name="bubble_size">60dp</dimen>
|
||||
<!-- Size of the badge shown on the bubble. -->
|
||||
|
||||
@@ -710,6 +710,7 @@ public class BubbleController {
|
||||
|| !newConfig.windowConfiguration.getBounds().equals(mScreenBounds)) {
|
||||
mDensityDpi = newConfig.densityDpi;
|
||||
mScreenBounds.set(newConfig.windowConfiguration.getBounds());
|
||||
mBubbleData.onMaxBubblesChanged();
|
||||
mBubbleIconFactory = new BubbleIconFactory(mContext);
|
||||
mStackView.onDisplaySizeChanged();
|
||||
}
|
||||
|
||||
@@ -141,9 +141,11 @@ public class BubbleData {
|
||||
private final BubbleOverflow mOverflow;
|
||||
private boolean mShowingOverflow;
|
||||
private boolean mExpanded;
|
||||
private final int mMaxBubbles;
|
||||
private int mMaxBubbles;
|
||||
private int mMaxOverflowBubbles;
|
||||
|
||||
private boolean mNeedsTrimming;
|
||||
|
||||
// State tracked during an operation -- keeps track of what listener events to dispatch.
|
||||
private Update mStateChange;
|
||||
|
||||
@@ -180,7 +182,7 @@ public class BubbleData {
|
||||
mOverflowBubbles = new ArrayList<>();
|
||||
mPendingBubbles = new HashMap<>();
|
||||
mStateChange = new Update(mBubbles, mOverflowBubbles);
|
||||
mMaxBubbles = mContext.getResources().getInteger(R.integer.bubbles_max_rendered);
|
||||
mMaxBubbles = mPositioner.getMaxBubbles();
|
||||
mMaxOverflowBubbles = mContext.getResources().getInteger(R.integer.bubbles_max_overflow);
|
||||
}
|
||||
|
||||
@@ -194,6 +196,16 @@ public class BubbleData {
|
||||
mCancelledListener = listener;
|
||||
}
|
||||
|
||||
public void onMaxBubblesChanged() {
|
||||
mMaxBubbles = mPositioner.getMaxBubbles();
|
||||
if (!mExpanded) {
|
||||
trim();
|
||||
dispatchPendingChanges();
|
||||
} else {
|
||||
mNeedsTrimming = true;
|
||||
}
|
||||
}
|
||||
|
||||
public boolean hasBubbles() {
|
||||
return !mBubbles.isEmpty();
|
||||
}
|
||||
@@ -455,13 +467,19 @@ public class BubbleData {
|
||||
|
||||
private void trim() {
|
||||
if (mBubbles.size() > mMaxBubbles) {
|
||||
int numtoRemove = mBubbles.size() - mMaxBubbles;
|
||||
ArrayList<Bubble> toRemove = new ArrayList<>();
|
||||
mBubbles.stream()
|
||||
// sort oldest first (ascending lastActivity)
|
||||
.sorted(Comparator.comparingLong(Bubble::getLastActivity))
|
||||
// skip the selected bubble
|
||||
.filter((b) -> !b.equals(mSelectedBubble))
|
||||
.findFirst()
|
||||
.ifPresent((b) -> doRemove(b.getKey(), Bubbles.DISMISS_AGED));
|
||||
.forEachOrdered((b) -> {
|
||||
if (toRemove.size() < numtoRemove) {
|
||||
toRemove.add(b);
|
||||
}
|
||||
});
|
||||
toRemove.forEach((b) -> doRemove(b.getKey(), Bubbles.DISMISS_AGED));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -770,6 +788,10 @@ public class BubbleData {
|
||||
}
|
||||
}
|
||||
}
|
||||
if (mNeedsTrimming) {
|
||||
mNeedsTrimming = false;
|
||||
trim();
|
||||
}
|
||||
mExpanded = shouldExpand;
|
||||
mStateChange.expanded = shouldExpand;
|
||||
mStateChange.expandedChanged = true;
|
||||
|
||||
@@ -64,9 +64,12 @@ public class BubblePositioner {
|
||||
private Rect mPositionRect;
|
||||
private @Surface.Rotation int mRotation = Surface.ROTATION_0;
|
||||
private Insets mInsets;
|
||||
private int mDefaultMaxBubbles;
|
||||
private int mMaxBubbles;
|
||||
|
||||
private int mBubbleSize;
|
||||
private int mBubbleBadgeSize;
|
||||
private int mSpacingBetweenBubbles;
|
||||
private int mExpandedViewLargeScreenWidth;
|
||||
private int mExpandedViewPadding;
|
||||
private int mPointerMargin;
|
||||
@@ -149,17 +152,45 @@ public class BubblePositioner {
|
||||
Resources res = mContext.getResources();
|
||||
mBubbleSize = res.getDimensionPixelSize(R.dimen.bubble_size);
|
||||
mBubbleBadgeSize = res.getDimensionPixelSize(R.dimen.bubble_badge_size);
|
||||
mSpacingBetweenBubbles = res.getDimensionPixelSize(R.dimen.bubble_spacing);
|
||||
mDefaultMaxBubbles = res.getInteger(R.integer.bubbles_max_rendered);
|
||||
|
||||
mExpandedViewLargeScreenWidth = res.getDimensionPixelSize(
|
||||
R.dimen.bubble_expanded_view_tablet_width);
|
||||
mExpandedViewPadding = res.getDimensionPixelSize(R.dimen.bubble_expanded_view_padding);
|
||||
mPointerWidth = res.getDimensionPixelSize(R.dimen.bubble_pointer_width);
|
||||
mPointerHeight = res.getDimensionPixelSize(R.dimen.bubble_pointer_height);
|
||||
mPointerMargin = res.getDimensionPixelSize(R.dimen.bubble_pointer_margin);
|
||||
|
||||
mMaxBubbles = calculateMaxBubbles();
|
||||
|
||||
if (mShowingInTaskbar) {
|
||||
adjustForTaskbar();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the maximum number of bubbles that can fit on the screen when expanded. If the
|
||||
* screen size / screen density is too small to support the default maximum number, then
|
||||
* the number will be adjust to something lower to ensure everything is presented nicely.
|
||||
*/
|
||||
private int calculateMaxBubbles() {
|
||||
// Use the shortest edge.
|
||||
// In portrait the bubbles should align with the expanded view so subtract its padding.
|
||||
// We always show the overflow so subtract one bubble size.
|
||||
int padding = showBubblesVertically() ? 0 : (mExpandedViewPadding * 2);
|
||||
int availableSpace = Math.min(mPositionRect.width(), mPositionRect.height())
|
||||
- padding
|
||||
- mBubbleSize;
|
||||
// Each of the bubbles have spacing because the overflow is at the end.
|
||||
int howManyFit = availableSpace / (mBubbleSize + mSpacingBetweenBubbles);
|
||||
if (howManyFit < mDefaultMaxBubbles) {
|
||||
// Not enough space for the default.
|
||||
return howManyFit;
|
||||
}
|
||||
return mDefaultMaxBubbles;
|
||||
}
|
||||
|
||||
/**
|
||||
* Taskbar insets appear as navigationBar insets, however, unlike navigationBar this should
|
||||
* not inset bubbles UI as bubbles floats above the taskbar. This adjust the available space
|
||||
@@ -229,6 +260,11 @@ public class BubblePositioner {
|
||||
: mBubbleSize;
|
||||
}
|
||||
|
||||
/** The maximum number of bubbles that can be displayed comfortably on screen. */
|
||||
public int getMaxBubbles() {
|
||||
return mMaxBubbles;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the left & right padding for the bubble expanded view.
|
||||
*
|
||||
|
||||
@@ -243,7 +243,6 @@ public class BubbleStackView extends FrameLayout
|
||||
|
||||
@Nullable private RelativeStackPosition mRelativeStackPositionBeforeRotation;
|
||||
|
||||
private int mMaxBubbles;
|
||||
private int mBubbleSize;
|
||||
private int mBubbleElevation;
|
||||
private int mBubbleTouchPadding;
|
||||
@@ -764,7 +763,6 @@ public class BubbleStackView extends FrameLayout
|
||||
mBubbleData = data;
|
||||
|
||||
Resources res = getResources();
|
||||
mMaxBubbles = res.getInteger(R.integer.bubbles_max_rendered);
|
||||
mBubbleSize = res.getDimensionPixelSize(R.dimen.bubble_size);
|
||||
mBubbleElevation = res.getDimensionPixelSize(R.dimen.bubble_elevation);
|
||||
mBubbleTouchPadding = res.getDimensionPixelSize(R.dimen.bubble_touch_padding);
|
||||
@@ -2725,7 +2723,7 @@ public class BubbleStackView extends FrameLayout
|
||||
private void updateBubbleShadows(boolean showForAllBubbles) {
|
||||
int bubbleCount = getBubbleCount();
|
||||
for (int i = 0; i < bubbleCount; i++) {
|
||||
final float z = (mMaxBubbles * mBubbleElevation) - i;
|
||||
final float z = (mPositioner.getMaxBubbles() * mBubbleElevation) - i;
|
||||
BadgedImageView bv = (BadgedImageView) mBubbleContainer.getChildAt(i);
|
||||
boolean isDraggedOut = mMagnetizedObject != null
|
||||
&& mMagnetizedObject.getUnderlyingObject().equals(bv);
|
||||
@@ -2759,7 +2757,7 @@ public class BubbleStackView extends FrameLayout
|
||||
for (int i = 0; i < bubbleCount; i++) {
|
||||
BadgedImageView bv = (BadgedImageView) mBubbleContainer.getChildAt(i);
|
||||
bv.setZ(i < NUM_VISIBLE_WHEN_RESTING
|
||||
? (mMaxBubbles * mBubbleElevation) - i
|
||||
? (mPositioner.getMaxBubbles() * mBubbleElevation) - i
|
||||
: 0f);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -211,16 +211,8 @@ public class ExpandedAnimationController
|
||||
mBubblePaddingTop = res.getDimensionPixelSize(R.dimen.bubble_padding_top);
|
||||
mStackOffsetPx = res.getDimensionPixelSize(R.dimen.bubble_stack_offset);
|
||||
mBubbleSizePx = mPositioner.getBubbleSize();
|
||||
mBubblesMaxRendered = res.getInteger(R.integer.bubbles_max_rendered);
|
||||
mBubblesMaxSpace = res.getDimensionPixelSize(R.dimen.bubble_spacing);
|
||||
final float availableSpace = mPositioner.isLandscape()
|
||||
? mPositioner.getAvailableRect().height()
|
||||
: mPositioner.getAvailableRect().width();
|
||||
final float spaceForMaxBubbles = (mExpandedViewPadding * 2)
|
||||
+ (mBubblesMaxRendered + 1) * mBubbleSizePx;
|
||||
float spaceBetweenBubbles =
|
||||
(availableSpace - spaceForMaxBubbles) / mBubblesMaxRendered;
|
||||
mSpaceBetweenBubbles = Math.min(spaceBetweenBubbles, mBubblesMaxSpace);
|
||||
mBubblesMaxRendered = mPositioner.getMaxBubbles();
|
||||
mSpaceBetweenBubbles = res.getDimensionPixelSize(R.dimen.bubble_spacing);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -19,9 +19,12 @@ package com.android.wm.shell.bubbles;
|
||||
import static com.google.common.truth.Truth.assertThat;
|
||||
import static com.google.common.truth.Truth.assertWithMessage;
|
||||
|
||||
import static junit.framework.Assert.assertNotNull;
|
||||
import static junit.framework.TestCase.assertEquals;
|
||||
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.reset;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.verifyZeroInteractions;
|
||||
@@ -90,6 +93,7 @@ public class BubbleDataTest extends ShellTestCase {
|
||||
private Bubble mBubbleDismissed;
|
||||
|
||||
private BubbleData mBubbleData;
|
||||
private TestableBubblePositioner mPositioner;
|
||||
|
||||
@Mock
|
||||
private TimeSource mTimeSource;
|
||||
@@ -150,9 +154,9 @@ public class BubbleDataTest extends ShellTestCase {
|
||||
mMainExecutor);
|
||||
mBubbleC1 = new Bubble(mEntryC1, mSuppressionListener, mPendingIntentCanceledListener,
|
||||
mMainExecutor);
|
||||
TestableBubblePositioner positioner = new TestableBubblePositioner(mContext,
|
||||
mPositioner = new TestableBubblePositioner(mContext,
|
||||
mock(WindowManager.class));
|
||||
mBubbleData = new BubbleData(getContext(), mBubbleLogger, positioner,
|
||||
mBubbleData = new BubbleData(getContext(), mBubbleLogger, mPositioner,
|
||||
mMainExecutor);
|
||||
|
||||
// Used by BubbleData to set lastAccessedTime
|
||||
@@ -810,6 +814,61 @@ public class BubbleDataTest extends ShellTestCase {
|
||||
assertEquals(1, mBubbleData.getOverflowBubbles().size());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void test_onMaxBubblesChanged_notExpanded() {
|
||||
mBubbleData.setListener(mListener);
|
||||
mPositioner.setMaxBubbles(5);
|
||||
sendUpdatedEntryAtTime(mEntryA1, 1000);
|
||||
sendUpdatedEntryAtTime(mEntryA2, 2000);
|
||||
sendUpdatedEntryAtTime(mEntryA3, 3000);
|
||||
sendUpdatedEntryAtTime(mEntryB1, 4000);
|
||||
sendUpdatedEntryAtTime(mEntryB2, 5000);
|
||||
mBubbleData.setExpanded(false);
|
||||
reset(mListener);
|
||||
|
||||
mPositioner.setMaxBubbles(3);
|
||||
mBubbleData.onMaxBubblesChanged();
|
||||
verifyUpdateReceived();
|
||||
|
||||
BubbleData.Update update = mUpdateCaptor.getValue();
|
||||
assertThat(update.removedBubbles.get(0)).isEqualTo(
|
||||
Pair.create(mBubbleA1, Bubbles.DISMISS_AGED));
|
||||
assertThat(update.removedBubbles.get(1)).isEqualTo(
|
||||
Pair.create(mBubbleA2, Bubbles.DISMISS_AGED));
|
||||
|
||||
assertNotNull(mBubbleData.getOverflowBubbleWithKey(mBubbleA1.getKey()));
|
||||
assertNotNull(mBubbleData.getOverflowBubbleWithKey(mBubbleA2.getKey()));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void test_onMaxBubblesChanged_expanded() {
|
||||
mBubbleData.setListener(mListener);
|
||||
mPositioner.setMaxBubbles(5);
|
||||
sendUpdatedEntryAtTime(mEntryA1, 1000);
|
||||
sendUpdatedEntryAtTime(mEntryA2, 2000);
|
||||
sendUpdatedEntryAtTime(mEntryA3, 3000);
|
||||
sendUpdatedEntryAtTime(mEntryB1, 4000);
|
||||
sendUpdatedEntryAtTime(mEntryB2, 5000);
|
||||
mBubbleData.setExpanded(true);
|
||||
reset(mListener);
|
||||
|
||||
mPositioner.setMaxBubbles(3);
|
||||
mBubbleData.onMaxBubblesChanged();
|
||||
verify(mListener, never()).applyUpdate(any());
|
||||
|
||||
mBubbleData.setExpanded(false);
|
||||
verifyUpdateReceived();
|
||||
|
||||
BubbleData.Update update = mUpdateCaptor.getValue();
|
||||
assertThat(update.removedBubbles.get(0)).isEqualTo(
|
||||
Pair.create(mBubbleA1, Bubbles.DISMISS_AGED));
|
||||
assertThat(update.removedBubbles.get(1)).isEqualTo(
|
||||
Pair.create(mBubbleA2, Bubbles.DISMISS_AGED));
|
||||
|
||||
assertNotNull(mBubbleData.getOverflowBubbleWithKey(mBubbleA1.getKey()));
|
||||
assertNotNull(mBubbleData.getOverflowBubbleWithKey(mBubbleA2.getKey()));
|
||||
}
|
||||
|
||||
private void verifyUpdateReceived() {
|
||||
verify(mListener).applyUpdate(mUpdateCaptor.capture());
|
||||
reset(mListener);
|
||||
|
||||
@@ -22,7 +22,10 @@ import android.graphics.Insets;
|
||||
import android.graphics.Rect;
|
||||
import android.view.WindowManager;
|
||||
|
||||
import com.android.wm.shell.R;
|
||||
|
||||
public class TestableBubblePositioner extends BubblePositioner {
|
||||
private int mMaxBubbles;
|
||||
|
||||
public TestableBubblePositioner(Context context,
|
||||
WindowManager windowManager) {
|
||||
@@ -31,5 +34,15 @@ public class TestableBubblePositioner extends BubblePositioner {
|
||||
updateInternal(Configuration.ORIENTATION_PORTRAIT,
|
||||
Insets.of(0, 0, 0, 0),
|
||||
new Rect(0, 0, 500, 1000));
|
||||
mMaxBubbles = context.getResources().getInteger(R.integer.bubbles_max_rendered);
|
||||
}
|
||||
|
||||
public void setMaxBubbles(int max) {
|
||||
mMaxBubbles = max;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getMaxBubbles() {
|
||||
return mMaxBubbles;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user