Skip to content

Commit b706506

Browse files
imhappidrchen
authored andcommitted
[Badge] Add attribute to automatically adjust badge so that it is within the anchor view's grandparent view's bounds
PiperOrigin-RevId: 523171594
1 parent a4c65d8 commit b706506

File tree

8 files changed

+163
-2
lines changed

8 files changed

+163
-2
lines changed

docs/components/BadgeDrawable.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,7 @@ center, use `setHorizontalOffset(int)` or `setVerticalOffset(int)`
104104
| Offset Alignment | `app:offsetAlignmentMode` |
105105
| Horizontal Padding | `app:badgeWidePadding` |
106106
| Vertical Padding | `app:badgeVerticalPadding` |
107-
107+
| Auto Adjust | `app:autoAdjustToWithinGrandparentBounds` |
108108
**Note:** If both `app:badgeText` and `app:number` are specified, the badge label will be `app:badgeText`.
109109

110110
### Talkback Support

lib/java/com/google/android/material/badge/BadgeDrawable.java

Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -364,6 +364,12 @@ public void updateBadgeCoordinates(
364364
invalidateSelf();
365365
}
366366

367+
private boolean isAnchorViewWrappedInCompatParent() {
368+
View customBadgeAnchorParent = getCustomBadgeParent();
369+
return customBadgeAnchorParent != null
370+
&& customBadgeAnchorParent.getId() == R.id.mtrl_anchor_parent;
371+
}
372+
367373
/** Returns a {@link FrameLayout} that will set this {@code BadgeDrawable} as its foreground. */
368374
@Nullable
369375
public FrameLayout getCustomBadgeParent() {
@@ -982,6 +988,23 @@ int getAdditionalVerticalOffset() {
982988
return state.getAdditionalVerticalOffset();
983989
}
984990

991+
/**
992+
* Sets whether or not to auto adjust the badge placement to within the badge anchor's
993+
* grandparent view.
994+
*
995+
* @param autoAdjustToWithinGrandparentBounds whether or not to auto adjust to within
996+
* the anchor's grandparent view.
997+
*/
998+
public void setAutoAdjustToWithinGrandparentBounds(boolean autoAdjustToWithinGrandparentBounds) {
999+
if (state.isAutoAdjustedToGrandparentBounds() == autoAdjustToWithinGrandparentBounds) {
1000+
return;
1001+
}
1002+
state.setAutoAdjustToGrandparentBounds(autoAdjustToWithinGrandparentBounds);
1003+
if (anchorViewRef != null && anchorViewRef.get() != null) {
1004+
autoAdjustWithinGrandparentBounds(anchorViewRef.get());
1005+
}
1006+
}
1007+
9851008
/**
9861009
* Sets this badge's text appearance resource.
9871010
*
@@ -1200,6 +1223,109 @@ private void calculateCenterAndBounds(@NonNull Rect anchorRect, @NonNull View an
12001223
: anchorRect.left - halfBadgeWidth + totalHorizontalOffset;
12011224
break;
12021225
}
1226+
1227+
if (state.isAutoAdjustedToGrandparentBounds()) {
1228+
autoAdjustWithinGrandparentBounds(anchorView);
1229+
}
1230+
}
1231+
1232+
/** Adjust the badge placement so it is within its anchor's grandparent view. */
1233+
private void autoAdjustWithinGrandparentBounds(@NonNull View anchorView) {
1234+
// The top of the badge may be cut off by the anchor view's parent's parent
1235+
// (eg. in the case of the bottom navigation bar). If that is the case,
1236+
// we should adjust the position of the badge.
1237+
1238+
float anchorYOffset;
1239+
float anchorXOffset;
1240+
View anchorParent;
1241+
// If there is a custom badge parent, we should use its coordinates instead of the anchor
1242+
// view's parent.
1243+
View customAnchorParent = getCustomBadgeParent();
1244+
if (customAnchorParent == null) {
1245+
if (!(anchorView.getParent() instanceof View)) {
1246+
return;
1247+
}
1248+
anchorYOffset = anchorView.getY();
1249+
anchorXOffset = anchorView.getX();
1250+
1251+
anchorParent = (View) anchorView.getParent();
1252+
} else if (isAnchorViewWrappedInCompatParent()) {
1253+
if (!(customAnchorParent.getParent() instanceof View)) {
1254+
return;
1255+
}
1256+
anchorYOffset = customAnchorParent.getY();
1257+
anchorXOffset = customAnchorParent.getX();
1258+
anchorParent = (View) customAnchorParent.getParent();
1259+
} else {
1260+
anchorYOffset = 0;
1261+
anchorXOffset = 0;
1262+
anchorParent = customAnchorParent;
1263+
}
1264+
1265+
float topCutOff = getTopCutOff(anchorParent, anchorYOffset);
1266+
float leftCutOff = getLeftCutOff(anchorParent, anchorXOffset);
1267+
float bottomCutOff = getBottomCutOff(anchorParent, anchorYOffset);
1268+
float rightCutOff = getRightCutoff(anchorParent, anchorXOffset);
1269+
1270+
// If there's any part of the badge that is cut off, we move the badge accordingly.
1271+
if (topCutOff < 0) {
1272+
badgeCenterY += Math.abs(topCutOff);
1273+
}
1274+
if (leftCutOff < 0) {
1275+
badgeCenterX += Math.abs(leftCutOff);
1276+
}
1277+
if (bottomCutOff > 0) {
1278+
badgeCenterY -= Math.abs(bottomCutOff);
1279+
}
1280+
if (rightCutOff > 0) {
1281+
badgeCenterX -= Math.abs(rightCutOff);
1282+
}
1283+
}
1284+
1285+
/* Returns where the badge is relative to the top bound of the anchor's grandparent view.
1286+
* If the value is negative, it is beyond the bounds of the anchor's grandparent view.
1287+
*/
1288+
private float getTopCutOff(View anchorParent, float anchorViewOffset) {
1289+
return badgeCenterY - halfBadgeHeight + anchorParent.getY() + anchorViewOffset;
1290+
}
1291+
1292+
/* Returns where the badge is relative to the left bound of the anchor's grandparent view.
1293+
* If the value is negative, it is beyond the bounds of the anchor's grandparent view.
1294+
*/
1295+
private float getLeftCutOff(View anchorParent, float anchorViewOffset) {
1296+
return badgeCenterX - halfBadgeWidth + anchorParent.getX() + anchorViewOffset;
1297+
}
1298+
1299+
/* Returns where the badge is relative to the bottom bound of the anchor's grandparent view.
1300+
* If the value is positive, it is beyond the bounds of the anchor's grandparent view.
1301+
*/
1302+
private float getBottomCutOff(View anchorParent, float anchorViewOffset) {
1303+
float bottomCutOff = 0f;
1304+
if (anchorParent.getParent() instanceof View) {
1305+
View anchorGrandparent = (View) anchorParent.getParent();
1306+
bottomCutOff =
1307+
badgeCenterY
1308+
+ halfBadgeHeight
1309+
- (anchorGrandparent.getHeight() - anchorParent.getY())
1310+
+ anchorViewOffset;
1311+
}
1312+
return bottomCutOff;
1313+
}
1314+
1315+
/* Returns where the badge is relative to the right bound of the anchor's grandparent view.
1316+
* If the value is positive, it is beyond the bounds of the anchor's grandparent view.
1317+
*/
1318+
private float getRightCutoff(View anchorParent, float anchorViewOffset) {
1319+
float rightCutOff = 0f;
1320+
if (anchorParent.getParent() instanceof View) {
1321+
View anchorGrandparent = (View) anchorParent.getParent();
1322+
rightCutOff =
1323+
badgeCenterX
1324+
+ halfBadgeWidth
1325+
- (anchorGrandparent.getWidth() - anchorParent.getX())
1326+
+ anchorViewOffset;
1327+
}
1328+
return rightCutOff;
12031329
}
12041330

12051331
private void drawText(Canvas canvas) {

lib/java/com/google/android/material/badge/BadgeState.java

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -273,6 +273,11 @@ public final class BadgeState {
273273
currentState.additionalVerticalOffset =
274274
storedState.additionalVerticalOffset == null ? 0 : storedState.additionalVerticalOffset;
275275

276+
currentState.autoAdjustToWithinGrandparentBounds =
277+
storedState.autoAdjustToWithinGrandparentBounds == null
278+
? a.getBoolean(R.styleable.Badge_autoAdjustToWithinGrandparentBounds, false)
279+
: storedState.autoAdjustToWithinGrandparentBounds;
280+
276281
a.recycle();
277282

278283
if (storedState.numberLocale == null) {
@@ -574,6 +579,15 @@ void setNumberLocale(Locale locale) {
574579
currentState.numberLocale = locale;
575580
}
576581

582+
boolean isAutoAdjustedToGrandparentBounds() {
583+
return currentState.autoAdjustToWithinGrandparentBounds;
584+
}
585+
586+
void setAutoAdjustToGrandparentBounds(boolean autoAdjustToGrandparentBounds) {
587+
overridingState.autoAdjustToWithinGrandparentBounds = autoAdjustToGrandparentBounds;
588+
currentState.autoAdjustToWithinGrandparentBounds = autoAdjustToGrandparentBounds;
589+
}
590+
577591
private static int readColorFromAttributes(
578592
Context context, @NonNull TypedArray a, @StyleableRes int index) {
579593
return MaterialResources.getColorStateList(context, a, index).getDefaultColor();
@@ -638,6 +652,8 @@ public static final class State implements Parcelable {
638652
@Dimension(unit = Dimension.PX)
639653
private Integer additionalVerticalOffset;
640654

655+
private Boolean autoAdjustToWithinGrandparentBounds;
656+
641657
public State() {}
642658

643659
State(@NonNull Parcel in) {
@@ -667,6 +683,7 @@ public State() {}
667683
additionalVerticalOffset = (Integer) in.readSerializable();
668684
isVisible = (Boolean) in.readSerializable();
669685
numberLocale = (Locale) in.readSerializable();
686+
autoAdjustToWithinGrandparentBounds = (Boolean) in.readSerializable();
670687
}
671688

672689
public static final Creator<State> CREATOR =
@@ -719,6 +736,7 @@ public void writeToParcel(@NonNull Parcel dest, int flags) {
719736
dest.writeSerializable(additionalVerticalOffset);
720737
dest.writeSerializable(isVisible);
721738
dest.writeSerializable(numberLocale);
739+
dest.writeSerializable(autoAdjustToWithinGrandparentBounds);
722740
}
723741
}
724742
}

lib/java/com/google/android/material/badge/res-public/values/public.xml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
<public name="badgeShapeAppearance" type="attr"/>
3636
<public name="badgeWithTextShapeAppearance" type="attr"/>
3737
<public name="badgeShapeAppearanceOverlay" type="attr"/>
38+
<public name="autoAdjustToWithinGrandparentBounds" type="attr"/>
3839
<public name="badgeWithTextShapeAppearanceOverlay" type="attr"/>
3940
<public name="Widget.MaterialComponents.Badge" type="style"/>
4041
<public name="Widget.Material3.Badge" type="style"/>

lib/java/com/google/android/material/badge/res/values/attrs.xml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,9 @@
9191
badge has text. If this is not defined, it will default to
9292
verticalOffset's value. -->
9393
<attr name="verticalOffsetWithText" format="dimension"/>
94+
<!-- Automatically move the badge so it is within the anchor view's
95+
grandparent's bounds. -->
96+
<attr name="autoAdjustToWithinGrandparentBounds" format="boolean"/>
9497
</declare-styleable>
9598

9699
</resources>

lib/java/com/google/android/material/badge/res/values/styles.xml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
<item name="badgeTextAppearance">@style/TextAppearance.MaterialComponents.Badge</item>
3030
<item name="badgeShapeAppearance">@style/ShapeAppearance.MaterialComponents.Badge</item>
3131
<item name="badgeWithTextShapeAppearance">@style/ShapeAppearance.MaterialComponents.Badge</item>
32+
<item name="autoAdjustToWithinGrandparentBounds">false</item>
3233
</style>
3334

3435
<style name="Base.TextAppearance.MaterialComponents.Badge" parent="TextAppearance.AppCompat">
@@ -71,4 +72,8 @@
7172
<item name="badgeVerticalPadding">@dimen/m3_badge_with_text_vertical_padding</item>
7273
</style>
7374

75+
<style name="Widget.Material3.Badge.AdjustToBounds" parent="Widget.Material3.Badge">
76+
<item name="autoAdjustToWithinGrandparentBounds">true</item>
77+
</style>
78+
7479
</resources>

lib/java/com/google/android/material/bottomnavigation/res/values/styles.xml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,12 +78,17 @@
7878
<item name="itemPaddingTop">@dimen/m3_bottom_nav_item_padding_top</item>
7979
<item name="itemPaddingBottom">@dimen/m3_bottom_nav_item_padding_bottom</item>
8080
<item name="android:minHeight">@dimen/m3_bottom_nav_min_height</item>
81+
<item name="materialThemeOverlay">@style/ThemeOverlay.Material3.BottomNavigationView</item>
8182
</style>
8283

8384
<style name="Widget.Material3.BottomNavigationView" parent="Base.Widget.Material3.BottomNavigationView">
8485
<item name="compatShadowEnabled">false</item>
8586
</style>
8687

88+
<style name="ThemeOverlay.Material3.BottomNavigationView" parent="">
89+
<item name="badgeStyle">@style/Widget.Material3.Badge.AdjustToBounds</item>
90+
</style>
91+
8792
<style name="Widget.Material3.BottomNavigationView.ActiveIndicator" parent="">
8893
<item name="android:width">@dimen/m3_bottom_nav_item_active_indicator_width</item>
8994
<item name="android:height">@dimen/m3_bottom_nav_item_active_indicator_height</item>

lib/java/com/google/android/material/tabs/res/values/styles.xml

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -81,9 +81,12 @@
8181
<item name="tabRippleColor">@color/m3_tabs_ripple_color</item>
8282
<item name="tabIndicatorFullWidth">false</item>
8383
<item name="tabIndicatorAnimationDuration">?attr/motionDurationLong2</item>
84+
<item name="materialThemeOverlay">@style/ThemeOverlay.Material3.TabLayout</item>
8485
</style>
8586

86-
<style name="Widget.Material3.TabLayout" parent="Base.Widget.Material3.TabLayout"/>
87+
<style name="ThemeOverlay.Material3.TabLayout" parent="">
88+
<item name="badgeStyle">@style/Widget.Material3.Badge.AdjustToBounds</item>
89+
</style>
8790

8891
<!-- Styles for M3 Tabs used on an elevatable surface. -->
8992
<style name="Base.Widget.Material3.TabLayout.OnSurface" parent="Widget.Material3.TabLayout">

0 commit comments

Comments
 (0)