From b706506c3d7e12df64375e116d587c2e9b96059b Mon Sep 17 00:00:00 2001 From: rightnao Date: Mon, 10 Apr 2023 11:42:06 -0700 Subject: [PATCH] [Badge] Add attribute to automatically adjust badge so that it is within the anchor view's grandparent view's bounds PiperOrigin-RevId: 523171594 --- docs/components/BadgeDrawable.md | 2 +- .../android/material/badge/BadgeDrawable.java | 126 ++++++++++++++++++ .../android/material/badge/BadgeState.java | 18 +++ .../badge/res-public/values/public.xml | 1 + .../material/badge/res/values/attrs.xml | 3 + .../material/badge/res/values/styles.xml | 5 + .../bottomnavigation/res/values/styles.xml | 5 + .../material/tabs/res/values/styles.xml | 5 +- 8 files changed, 163 insertions(+), 2 deletions(-) diff --git a/docs/components/BadgeDrawable.md b/docs/components/BadgeDrawable.md index 1bad7fd25da..b694124b8ee 100644 --- a/docs/components/BadgeDrawable.md +++ b/docs/components/BadgeDrawable.md @@ -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 diff --git a/lib/java/com/google/android/material/badge/BadgeDrawable.java b/lib/java/com/google/android/material/badge/BadgeDrawable.java index 3b4204491b1..90497f59699 100644 --- a/lib/java/com/google/android/material/badge/BadgeDrawable.java +++ b/lib/java/com/google/android/material/badge/BadgeDrawable.java @@ -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() { @@ -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. * @@ -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) { diff --git a/lib/java/com/google/android/material/badge/BadgeState.java b/lib/java/com/google/android/material/badge/BadgeState.java index c0e17c7257e..8783cbad388 100644 --- a/lib/java/com/google/android/material/badge/BadgeState.java +++ b/lib/java/com/google/android/material/badge/BadgeState.java @@ -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) { @@ -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(); @@ -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) { @@ -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 CREATOR = @@ -719,6 +736,7 @@ public void writeToParcel(@NonNull Parcel dest, int flags) { dest.writeSerializable(additionalVerticalOffset); dest.writeSerializable(isVisible); dest.writeSerializable(numberLocale); + dest.writeSerializable(autoAdjustToWithinGrandparentBounds); } } } diff --git a/lib/java/com/google/android/material/badge/res-public/values/public.xml b/lib/java/com/google/android/material/badge/res-public/values/public.xml index d31a0bbad06..c536a7d82fe 100644 --- a/lib/java/com/google/android/material/badge/res-public/values/public.xml +++ b/lib/java/com/google/android/material/badge/res-public/values/public.xml @@ -35,6 +35,7 @@ + diff --git a/lib/java/com/google/android/material/badge/res/values/attrs.xml b/lib/java/com/google/android/material/badge/res/values/attrs.xml index c72655acb80..7fbf079eb4b 100644 --- a/lib/java/com/google/android/material/badge/res/values/attrs.xml +++ b/lib/java/com/google/android/material/badge/res/values/attrs.xml @@ -91,6 +91,9 @@ badge has text. If this is not defined, it will default to verticalOffset's value. --> + + 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 b9f60b72881..63c5b38536f 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 @@ -29,6 +29,7 @@ @style/TextAppearance.MaterialComponents.Badge @style/ShapeAppearance.MaterialComponents.Badge @style/ShapeAppearance.MaterialComponents.Badge + false + + diff --git a/lib/java/com/google/android/material/bottomnavigation/res/values/styles.xml b/lib/java/com/google/android/material/bottomnavigation/res/values/styles.xml index 2c012996948..29239856a42 100644 --- a/lib/java/com/google/android/material/bottomnavigation/res/values/styles.xml +++ b/lib/java/com/google/android/material/bottomnavigation/res/values/styles.xml @@ -78,12 +78,17 @@ @dimen/m3_bottom_nav_item_padding_top @dimen/m3_bottom_nav_item_padding_bottom @dimen/m3_bottom_nav_min_height + @style/ThemeOverlay.Material3.BottomNavigationView + + -