Skip to content

Commit

Permalink
[Badge] Add attribute to automatically adjust badge so that it is wit…
Browse files Browse the repository at this point in the history
…hin the anchor view's grandparent view's bounds

PiperOrigin-RevId: 523171594
  • Loading branch information
imhappi authored and drchen committed Apr 12, 2023
1 parent a4c65d8 commit b706506
Show file tree
Hide file tree
Showing 8 changed files with 163 additions and 2 deletions.
2 changes: 1 addition & 1 deletion docs/components/BadgeDrawable.md
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ center, use `setHorizontalOffset(int)` or `setVerticalOffset(int)`
| Offset Alignment | `app:offsetAlignmentMode` |
| Horizontal Padding | `app:badgeWidePadding` |
| Vertical Padding | `app:badgeVerticalPadding` |

| Auto Adjust | `app:autoAdjustToWithinGrandparentBounds` |
**Note:** If both `app:badgeText` and `app:number` are specified, the badge label will be `app:badgeText`.

### Talkback Support
Expand Down
126 changes: 126 additions & 0 deletions lib/java/com/google/android/material/badge/BadgeDrawable.java
Original file line number Diff line number Diff line change
Expand Up @@ -364,6 +364,12 @@ public void updateBadgeCoordinates(
invalidateSelf();
}

private boolean isAnchorViewWrappedInCompatParent() {
View customBadgeAnchorParent = getCustomBadgeParent();
return customBadgeAnchorParent != null
&& customBadgeAnchorParent.getId() == R.id.mtrl_anchor_parent;
}

/** Returns a {@link FrameLayout} that will set this {@code BadgeDrawable} as its foreground. */
@Nullable
public FrameLayout getCustomBadgeParent() {
Expand Down Expand Up @@ -982,6 +988,23 @@ int getAdditionalVerticalOffset() {
return state.getAdditionalVerticalOffset();
}

/**
* Sets whether or not to auto adjust the badge placement to within the badge anchor's
* grandparent view.
*
* @param autoAdjustToWithinGrandparentBounds whether or not to auto adjust to within
* the anchor's grandparent view.
*/
public void setAutoAdjustToWithinGrandparentBounds(boolean autoAdjustToWithinGrandparentBounds) {
if (state.isAutoAdjustedToGrandparentBounds() == autoAdjustToWithinGrandparentBounds) {
return;
}
state.setAutoAdjustToGrandparentBounds(autoAdjustToWithinGrandparentBounds);
if (anchorViewRef != null && anchorViewRef.get() != null) {
autoAdjustWithinGrandparentBounds(anchorViewRef.get());
}
}

/**
* Sets this badge's text appearance resource.
*
Expand Down Expand Up @@ -1200,6 +1223,109 @@ private void calculateCenterAndBounds(@NonNull Rect anchorRect, @NonNull View an
: anchorRect.left - halfBadgeWidth + totalHorizontalOffset;
break;
}

if (state.isAutoAdjustedToGrandparentBounds()) {
autoAdjustWithinGrandparentBounds(anchorView);
}
}

/** Adjust the badge placement so it is within its anchor's grandparent view. */
private void autoAdjustWithinGrandparentBounds(@NonNull View anchorView) {
// The top of the badge may be cut off by the anchor view's parent's parent
// (eg. in the case of the bottom navigation bar). If that is the case,
// we should adjust the position of the badge.

float anchorYOffset;
float anchorXOffset;
View anchorParent;
// If there is a custom badge parent, we should use its coordinates instead of the anchor
// view's parent.
View customAnchorParent = getCustomBadgeParent();
if (customAnchorParent == null) {
if (!(anchorView.getParent() instanceof View)) {
return;
}
anchorYOffset = anchorView.getY();
anchorXOffset = anchorView.getX();

anchorParent = (View) anchorView.getParent();
} else if (isAnchorViewWrappedInCompatParent()) {
if (!(customAnchorParent.getParent() instanceof View)) {
return;
}
anchorYOffset = customAnchorParent.getY();
anchorXOffset = customAnchorParent.getX();
anchorParent = (View) customAnchorParent.getParent();
} else {
anchorYOffset = 0;
anchorXOffset = 0;
anchorParent = customAnchorParent;
}

float topCutOff = getTopCutOff(anchorParent, anchorYOffset);
float leftCutOff = getLeftCutOff(anchorParent, anchorXOffset);
float bottomCutOff = getBottomCutOff(anchorParent, anchorYOffset);
float rightCutOff = getRightCutoff(anchorParent, anchorXOffset);

// If there's any part of the badge that is cut off, we move the badge accordingly.
if (topCutOff < 0) {
badgeCenterY += Math.abs(topCutOff);
}
if (leftCutOff < 0) {
badgeCenterX += Math.abs(leftCutOff);
}
if (bottomCutOff > 0) {
badgeCenterY -= Math.abs(bottomCutOff);
}
if (rightCutOff > 0) {
badgeCenterX -= Math.abs(rightCutOff);
}
}

/* Returns where the badge is relative to the top bound of the anchor's grandparent view.
* If the value is negative, it is beyond the bounds of the anchor's grandparent view.
*/
private float getTopCutOff(View anchorParent, float anchorViewOffset) {
return badgeCenterY - halfBadgeHeight + anchorParent.getY() + anchorViewOffset;
}

/* Returns where the badge is relative to the left bound of the anchor's grandparent view.
* If the value is negative, it is beyond the bounds of the anchor's grandparent view.
*/
private float getLeftCutOff(View anchorParent, float anchorViewOffset) {
return badgeCenterX - halfBadgeWidth + anchorParent.getX() + anchorViewOffset;
}

/* Returns where the badge is relative to the bottom bound of the anchor's grandparent view.
* If the value is positive, it is beyond the bounds of the anchor's grandparent view.
*/
private float getBottomCutOff(View anchorParent, float anchorViewOffset) {
float bottomCutOff = 0f;
if (anchorParent.getParent() instanceof View) {
View anchorGrandparent = (View) anchorParent.getParent();
bottomCutOff =
badgeCenterY
+ halfBadgeHeight
- (anchorGrandparent.getHeight() - anchorParent.getY())
+ anchorViewOffset;
}
return bottomCutOff;
}

/* Returns where the badge is relative to the right bound of the anchor's grandparent view.
* If the value is positive, it is beyond the bounds of the anchor's grandparent view.
*/
private float getRightCutoff(View anchorParent, float anchorViewOffset) {
float rightCutOff = 0f;
if (anchorParent.getParent() instanceof View) {
View anchorGrandparent = (View) anchorParent.getParent();
rightCutOff =
badgeCenterX
+ halfBadgeWidth
- (anchorGrandparent.getWidth() - anchorParent.getX())
+ anchorViewOffset;
}
return rightCutOff;
}

private void drawText(Canvas canvas) {
Expand Down
18 changes: 18 additions & 0 deletions lib/java/com/google/android/material/badge/BadgeState.java
Original file line number Diff line number Diff line change
Expand Up @@ -273,6 +273,11 @@ public final class BadgeState {
currentState.additionalVerticalOffset =
storedState.additionalVerticalOffset == null ? 0 : storedState.additionalVerticalOffset;

currentState.autoAdjustToWithinGrandparentBounds =
storedState.autoAdjustToWithinGrandparentBounds == null
? a.getBoolean(R.styleable.Badge_autoAdjustToWithinGrandparentBounds, false)
: storedState.autoAdjustToWithinGrandparentBounds;

a.recycle();

if (storedState.numberLocale == null) {
Expand Down Expand Up @@ -574,6 +579,15 @@ void setNumberLocale(Locale locale) {
currentState.numberLocale = locale;
}

boolean isAutoAdjustedToGrandparentBounds() {
return currentState.autoAdjustToWithinGrandparentBounds;
}

void setAutoAdjustToGrandparentBounds(boolean autoAdjustToGrandparentBounds) {
overridingState.autoAdjustToWithinGrandparentBounds = autoAdjustToGrandparentBounds;
currentState.autoAdjustToWithinGrandparentBounds = autoAdjustToGrandparentBounds;
}

private static int readColorFromAttributes(
Context context, @NonNull TypedArray a, @StyleableRes int index) {
return MaterialResources.getColorStateList(context, a, index).getDefaultColor();
Expand Down Expand Up @@ -638,6 +652,8 @@ public static final class State implements Parcelable {
@Dimension(unit = Dimension.PX)
private Integer additionalVerticalOffset;

private Boolean autoAdjustToWithinGrandparentBounds;

public State() {}

State(@NonNull Parcel in) {
Expand Down Expand Up @@ -667,6 +683,7 @@ public State() {}
additionalVerticalOffset = (Integer) in.readSerializable();
isVisible = (Boolean) in.readSerializable();
numberLocale = (Locale) in.readSerializable();
autoAdjustToWithinGrandparentBounds = (Boolean) in.readSerializable();
}

public static final Creator<State> CREATOR =
Expand Down Expand Up @@ -719,6 +736,7 @@ public void writeToParcel(@NonNull Parcel dest, int flags) {
dest.writeSerializable(additionalVerticalOffset);
dest.writeSerializable(isVisible);
dest.writeSerializable(numberLocale);
dest.writeSerializable(autoAdjustToWithinGrandparentBounds);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
<public name="badgeShapeAppearance" type="attr"/>
<public name="badgeWithTextShapeAppearance" type="attr"/>
<public name="badgeShapeAppearanceOverlay" type="attr"/>
<public name="autoAdjustToWithinGrandparentBounds" type="attr"/>
<public name="badgeWithTextShapeAppearanceOverlay" type="attr"/>
<public name="Widget.MaterialComponents.Badge" type="style"/>
<public name="Widget.Material3.Badge" type="style"/>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,9 @@
badge has text. If this is not defined, it will default to
verticalOffset's value. -->
<attr name="verticalOffsetWithText" format="dimension"/>
<!-- Automatically move the badge so it is within the anchor view's
grandparent's bounds. -->
<attr name="autoAdjustToWithinGrandparentBounds" format="boolean"/>
</declare-styleable>

</resources>
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
<item name="badgeTextAppearance">@style/TextAppearance.MaterialComponents.Badge</item>
<item name="badgeShapeAppearance">@style/ShapeAppearance.MaterialComponents.Badge</item>
<item name="badgeWithTextShapeAppearance">@style/ShapeAppearance.MaterialComponents.Badge</item>
<item name="autoAdjustToWithinGrandparentBounds">false</item>
</style>

<style name="Base.TextAppearance.MaterialComponents.Badge" parent="TextAppearance.AppCompat">
Expand Down Expand Up @@ -71,4 +72,8 @@
<item name="badgeVerticalPadding">@dimen/m3_badge_with_text_vertical_padding</item>
</style>

<style name="Widget.Material3.Badge.AdjustToBounds" parent="Widget.Material3.Badge">
<item name="autoAdjustToWithinGrandparentBounds">true</item>
</style>

</resources>
Original file line number Diff line number Diff line change
Expand Up @@ -78,12 +78,17 @@
<item name="itemPaddingTop">@dimen/m3_bottom_nav_item_padding_top</item>
<item name="itemPaddingBottom">@dimen/m3_bottom_nav_item_padding_bottom</item>
<item name="android:minHeight">@dimen/m3_bottom_nav_min_height</item>
<item name="materialThemeOverlay">@style/ThemeOverlay.Material3.BottomNavigationView</item>
</style>

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

<style name="ThemeOverlay.Material3.BottomNavigationView" parent="">
<item name="badgeStyle">@style/Widget.Material3.Badge.AdjustToBounds</item>
</style>

<style name="Widget.Material3.BottomNavigationView.ActiveIndicator" parent="">
<item name="android:width">@dimen/m3_bottom_nav_item_active_indicator_width</item>
<item name="android:height">@dimen/m3_bottom_nav_item_active_indicator_height</item>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -81,9 +81,12 @@
<item name="tabRippleColor">@color/m3_tabs_ripple_color</item>
<item name="tabIndicatorFullWidth">false</item>
<item name="tabIndicatorAnimationDuration">?attr/motionDurationLong2</item>
<item name="materialThemeOverlay">@style/ThemeOverlay.Material3.TabLayout</item>
</style>

<style name="Widget.Material3.TabLayout" parent="Base.Widget.Material3.TabLayout"/>
<style name="ThemeOverlay.Material3.TabLayout" parent="">
<item name="badgeStyle">@style/Widget.Material3.Badge.AdjustToBounds</item>
</style>

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

0 comments on commit b706506

Please sign in to comment.