From 3880efefef6ee7f57ddb11712b9a4b97825f9032 Mon Sep 17 00:00:00 2001 From: rightnao Date: Wed, 1 May 2024 19:57:20 +0000 Subject: [PATCH] [Badge] Adjust badges to fit within the bounds of the first ancestor view that clips its children to avoid getting cut off PiperOrigin-RevId: 629810011 --- .../android/material/badge/BadgeDrawable.java | 163 +++++++++++------- .../material/badge/res/values/styles.xml | 4 +- .../bottomnavigation/res/values/styles.xml | 2 +- .../navigationrail/res/values/styles.xml | 2 +- .../material/tabs/res/values/styles.xml | 2 +- 5 files changed, 104 insertions(+), 69 deletions(-) diff --git a/lib/java/com/google/android/material/badge/BadgeDrawable.java b/lib/java/com/google/android/material/badge/BadgeDrawable.java index a71173b346d..f1700e13275 100644 --- a/lib/java/com/google/android/material/badge/BadgeDrawable.java +++ b/lib/java/com/google/android/material/badge/BadgeDrawable.java @@ -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; @@ -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) { @@ -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) { diff --git a/lib/java/com/google/android/material/badge/res/values/styles.xml b/lib/java/com/google/android/material/badge/res/values/styles.xml index e25d80575ae..510ad0bfad9 100644 --- a/lib/java/com/google/android/material/badge/res/values/styles.xml +++ b/lib/java/com/google/android/material/badge/res/values/styles.xml @@ -72,8 +72,6 @@ @dimen/m3_badge_with_text_vertical_padding - + - diff --git a/lib/java/com/google/android/material/navigationrail/res/values/styles.xml b/lib/java/com/google/android/material/navigationrail/res/values/styles.xml index dcb6f690d3f..1b6a429baeb 100644 --- a/lib/java/com/google/android/material/navigationrail/res/values/styles.xml +++ b/lib/java/com/google/android/material/navigationrail/res/values/styles.xml @@ -96,7 +96,7 @@ @style/Widget.Material3.NavigationRailView.Badge - diff --git a/lib/java/com/google/android/material/tabs/res/values/styles.xml b/lib/java/com/google/android/material/tabs/res/values/styles.xml index 914aadd9f5f..bf9e6963707 100644 --- a/lib/java/com/google/android/material/tabs/res/values/styles.xml +++ b/lib/java/com/google/android/material/tabs/res/values/styles.xml @@ -85,7 +85,7 @@