Skip to content

Commit

Permalink
[Badge] Adjust badges to fit within the bounds of the first ancestor …
Browse files Browse the repository at this point in the history
…view that clips its children to avoid getting cut off

PiperOrigin-RevId: 629810011
  • Loading branch information
imhappi authored and leticiarossi committed May 2, 2024
1 parent c4cf6b2 commit 3880efe
Show file tree
Hide file tree
Showing 5 changed files with 104 additions and 69 deletions.
163 changes: 100 additions & 63 deletions lib/java/com/google/android/material/badge/BadgeDrawable.java
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
import android.view.Gravity;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewParent;
import android.widget.FrameLayout;
import android.widget.FrameLayout.LayoutParams;
import androidx.annotation.AttrRes;
Expand Down Expand Up @@ -1323,46 +1324,64 @@ private void calculateCenterAndBounds(@NonNull Rect anchorRect, @NonNull View an

if (state.isAutoAdjustedToGrandparentBounds()) {
autoAdjustWithinGrandparentBounds(anchorView);
} else {
autoAdjustWithinViewBounds(anchorView, null);
}
}

/** 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;
/**
* Adjust the badge placement so it is within the specified ancestor view. If {@code ancestorView}
* is null, it will default to adjusting to the first ancestor of {@code anchorView} that clips
* its children.
*/
private void autoAdjustWithinViewBounds(@NonNull View anchorView, @Nullable View ancestorView) {
// The top of the badge may be cut off by the anchor view's ancestor view if clipChildren is
// false (eg. in the case of the bottom navigation bar). If that is the case, we should adjust
// the position of the badge.

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

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

float topCutOff = getTopCutOff(anchorParent, anchorYOffset);
float leftCutOff = getLeftCutOff(anchorParent, anchorXOffset);
float bottomCutOff = getBottomCutOff(anchorParent, anchorYOffset);
float rightCutOff = getRightCutoff(anchorParent, anchorXOffset);
ViewParent currentViewParent = anchorParent;
while (currentViewParent instanceof View && currentViewParent != ancestorView) {
ViewParent viewGrandparent = currentViewParent.getParent();
if (!(viewGrandparent instanceof ViewGroup)
|| ((ViewGroup) viewGrandparent).getClipChildren()) {
break;
}
View currentViewGroup = (View) currentViewParent;
totalAnchorYOffset += currentViewGroup.getY();
totalAnchorXOffset += currentViewGroup.getX();
currentViewParent = currentViewParent.getParent();
}

// If currentViewParent is not a View, all ancestor Views did not clip their children
if (!(currentViewParent instanceof View)) {
return;
}

float topCutOff = getTopCutOff(totalAnchorYOffset);
float leftCutOff = getLeftCutOff(totalAnchorXOffset);
float bottomCutOff =
getBottomCutOff(((View) currentViewParent).getHeight(), totalAnchorYOffset);
float rightCutOff = getRightCutoff(((View) currentViewParent).getWidth(), totalAnchorXOffset);

// If there's any part of the badge that is cut off, we move the badge accordingly.
if (topCutOff < 0) {
Expand All @@ -1379,50 +1398,68 @@ private void autoAdjustWithinGrandparentBounds(@NonNull View anchorView) {
}
}

/* 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.
/** Adjust the badge placement so it is within its anchor's grandparent view. */
private void autoAdjustWithinGrandparentBounds(@NonNull View anchorView) {
// If there is a custom badge parent, we should use its coordinates instead of the anchor
// view's parent.
ViewParent customAnchor = getCustomBadgeParent();
ViewParent anchorParent = null;
if (customAnchor == null) {
anchorParent = anchorView.getParent();
} else if (isAnchorViewWrappedInCompatParent()) {
anchorParent = customAnchor.getParent();
} else {
anchorParent = customAnchor;
}
if (anchorParent instanceof View && anchorParent.getParent() instanceof View) {
autoAdjustWithinViewBounds(anchorView, (View) anchorParent.getParent());
}
}

/**
* Returns where the badge is relative to the top bound of the anchor's ancestor view. If the
* value is negative, it is beyond the bounds of the anchor's ancestor view.
*
* @param totalAnchorYOffset the total X offset of the anchor in relation to the ancestor view it
* is adjusting its bounds to
*/
private float getTopCutOff(View anchorParent, float anchorViewOffset) {
return badgeCenterY - halfBadgeHeight + anchorParent.getY() + anchorViewOffset;
private float getTopCutOff(float totalAnchorYOffset) {
return badgeCenterY - halfBadgeHeight + totalAnchorYOffset;
}

/* 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.
/**
* Returns where the badge is relative to the left bound of the anchor's ancestor view. If the
* value is negative, it is beyond the bounds of the anchor's ancestor view.
*
* @param totalAnchorXOffset the total X offset of the anchor in relation to the ancestor view it
* is adjusting its bounds to
*/
private float getLeftCutOff(View anchorParent, float anchorViewOffset) {
return badgeCenterX - halfBadgeWidth + anchorParent.getX() + anchorViewOffset;
private float getLeftCutOff(float totalAnchorXOffset) {
return badgeCenterX - halfBadgeWidth + totalAnchorXOffset;
}

/* 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.
/**
* Returns where the badge is relative to the bottom bound of the anchor's ancestor view. If the
* value is positive, it is beyond the bounds of the anchor's ancestor view.
*
* @param ancestorHeight the height of the ancestor view
* @param totalAnchorYOffset the total Y offset of the anchor in relation to the ancestor view it
* is adjusting its bounds to
*/
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;
private float getBottomCutOff(float ancestorHeight, float totalAnchorYOffset) {
return badgeCenterY + halfBadgeHeight - ancestorHeight + totalAnchorYOffset;
}

/* 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.
/**
* Returns where the badge is relative to the right bound of the anchor's ancestor view. If the
* value is positive, it is beyond the bounds of the anchor's ancestor view.
*
* @param ancestorWidth the width of the ancestor view
* @param totalAnchorXOffset the total X offset of the anchor in relation to the ancestor view it
* is adjusting its bounds to
*/
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 float getRightCutoff(float ancestorWidth, float totalAnchorXOffset) {
return badgeCenterX + halfBadgeWidth - ancestorWidth + totalAnchorXOffset;
}

private void drawBadgeContent(Canvas canvas) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -72,8 +72,6 @@
<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>
<style name="Widget.Material3.Badge.AdjustToBounds" parent="Widget.Material3.Badge"/>

</resources>
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@
<item name="badgeStyle">@style/Widget.Material3.BottomNavigation.Badge</item>
</style>

<style name="Widget.Material3.BottomNavigation.Badge" parent="Widget.Material3.Badge.AdjustToBounds">
<style name="Widget.Material3.BottomNavigation.Badge" parent="Widget.Material3.Badge">
<item name="verticalOffsetWithText">@dimen/m3_nav_badge_with_text_vertical_offset</item>
</style>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@
<item name="badgeStyle">@style/Widget.Material3.NavigationRailView.Badge</item>
</style>

<style name="Widget.Material3.NavigationRailView.Badge" parent="Widget.Material3.Badge.AdjustToBounds">
<style name="Widget.Material3.NavigationRailView.Badge" parent="Widget.Material3.Badge">
<item name="largeFontVerticalOffsetAdjustment">@dimen/m3_large_text_vertical_offset_adjustment</item>
<item name="verticalOffsetWithText">@dimen/m3_nav_badge_with_text_vertical_offset</item>
</style>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@
</style>

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

<style name="Widget.Material3.TabLayout" parent="Base.Widget.Material3.TabLayout"/>
Expand Down

0 comments on commit 3880efe

Please sign in to comment.