Skip to content

Commit abe77f1

Browse files
Chaoyang0201dsn5ft
authored andcommitted
[BadgeDrawable] add horizontal and vertical offset to control position
Resolves #638 Resolves #630 Co-authored-by:wcshi <[email protected]> GIT_ORIGIN_REV_ID=511c955a11e05fdc0848c74938c4c194da22826e PiperOrigin-RevId: 275522709
1 parent 444a16b commit abe77f1

File tree

5 files changed

+133
-15
lines changed

5 files changed

+133
-15
lines changed

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

Lines changed: 60 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
import android.os.Parcelable;
3636
import androidx.annotation.AttrRes;
3737
import androidx.annotation.ColorInt;
38+
import androidx.annotation.Dimension;
3839
import androidx.annotation.IntDef;
3940
import androidx.annotation.NonNull;
4041
import androidx.annotation.Nullable;
@@ -187,6 +188,12 @@ public static final class SavedState implements Parcelable {
187188
@PluralsRes private int contentDescriptionQuantityStrings;
188189
@BadgeGravity private int badgeGravity;
189190

191+
@Dimension(unit = Dimension.PX)
192+
private int horizontalOffset;
193+
194+
@Dimension(unit = Dimension.PX)
195+
private int verticalOffset;
196+
190197
public SavedState(@NonNull Context context) {
191198
// If the badge text color attribute was not explicitly set, use the text color specified in
192199
// the TextAppearance.
@@ -207,6 +214,8 @@ protected SavedState(@NonNull Parcel in) {
207214
contentDescriptionNumberless = in.readString();
208215
contentDescriptionQuantityStrings = in.readInt();
209216
badgeGravity = in.readInt();
217+
horizontalOffset = in.readInt();
218+
verticalOffset = in.readInt();
210219
}
211220

212221
public static final Creator<SavedState> CREATOR =
@@ -239,6 +248,8 @@ public void writeToParcel(@NonNull Parcel dest, int flags) {
239248
dest.writeString(contentDescriptionNumberless.toString());
240249
dest.writeInt(contentDescriptionQuantityStrings);
241250
dest.writeInt(badgeGravity);
251+
dest.writeInt(horizontalOffset);
252+
dest.writeInt(verticalOffset);
242253
}
243254
}
244255

@@ -321,6 +332,9 @@ private void restoreFromSavedState(@NonNull SavedState savedState) {
321332
setBadgeTextColor(savedState.badgeTextColor);
322333

323334
setBadgeGravity(savedState.badgeGravity);
335+
336+
setHorizontalOffset(savedState.horizontalOffset);
337+
setVerticalOffset(savedState.verticalOffset);
324338
}
325339

326340
private void loadDefaultStateFromAttributes(
@@ -348,6 +362,10 @@ private void loadDefaultStateFromAttributes(
348362
}
349363

350364
setBadgeGravity(a.getInt(R.styleable.Badge_badgeGravity, TOP_END));
365+
366+
setHorizontalOffset(a.getDimensionPixelOffset(R.styleable.Badge_horizontalOffset, 0));
367+
setVerticalOffset(a.getDimensionPixelOffset(R.styleable.Badge_verticalOffset, 0));
368+
351369
a.recycle();
352370
}
353371

@@ -631,6 +649,42 @@ public CharSequence getContentDescription() {
631649
}
632650
}
633651

652+
/**
653+
* Sets how much (in pixels) to horizontally move this badge towards the center of its anchor.
654+
*
655+
* @param px badge's horizontal offset
656+
*/
657+
public void setHorizontalOffset(int px) {
658+
savedState.horizontalOffset = px;
659+
updateCenterAndBounds();
660+
}
661+
662+
/**
663+
* Returns how much (in pixels) this badge is being horizontally offset towards the center of its
664+
* anchor.
665+
*/
666+
public int getHorizontalOffset() {
667+
return savedState.horizontalOffset;
668+
}
669+
670+
/**
671+
* Sets how much (in pixels) to vertically move this badge towards the center of its anchor.
672+
*
673+
* @param px badge's vertical offset
674+
*/
675+
public void setVerticalOffset(int px) {
676+
savedState.verticalOffset = px;
677+
updateCenterAndBounds();
678+
}
679+
680+
/**
681+
* Returns how much (in pixels) this badge is being vertically moved towards the center of its
682+
* anchor.
683+
*/
684+
public int getVerticalOffset() {
685+
return savedState.verticalOffset;
686+
}
687+
634688
private void setTextAppearanceResource(@StyleRes int id) {
635689
Context context = contextRef.get();
636690
if (context == null) {
@@ -687,12 +741,12 @@ private void calculateCenterAndBounds(
687741
switch (savedState.badgeGravity) {
688742
case BOTTOM_END:
689743
case BOTTOM_START:
690-
badgeCenterY = anchorRect.bottom;
744+
badgeCenterY = anchorRect.bottom - savedState.verticalOffset;
691745
break;
692746
case TOP_END:
693747
case TOP_START:
694748
default:
695-
badgeCenterY = anchorRect.top;
749+
badgeCenterY = anchorRect.top + savedState.verticalOffset;
696750
break;
697751
}
698752

@@ -720,16 +774,16 @@ private void calculateCenterAndBounds(
720774
case TOP_START:
721775
badgeCenterX =
722776
ViewCompat.getLayoutDirection(anchorView) == View.LAYOUT_DIRECTION_LTR
723-
? anchorRect.left - halfBadgeWidth + inset
724-
: anchorRect.right + halfBadgeWidth - inset;
777+
? anchorRect.left - halfBadgeWidth + inset + savedState.horizontalOffset
778+
: anchorRect.right + halfBadgeWidth - inset - savedState.horizontalOffset;
725779
break;
726780
case BOTTOM_END:
727781
case TOP_END:
728782
default:
729783
badgeCenterX =
730784
ViewCompat.getLayoutDirection(anchorView) == View.LAYOUT_DIRECTION_LTR
731-
? anchorRect.right + halfBadgeWidth - inset
732-
: anchorRect.left - halfBadgeWidth + inset;
785+
? anchorRect.right + halfBadgeWidth - inset - savedState.horizontalOffset
786+
: anchorRect.left - halfBadgeWidth + inset + savedState.horizontalOffset;
733787
break;
734788
}
735789
}

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,5 +21,7 @@
2121
<public name="maxCharacterCount" type="attr"/>
2222
<public name="number" type="attr"/>
2323
<public name="badgeGravity" type="attr"/>
24+
<public name="horizontalOffset" type="attr"/>
25+
<public name="verticalOffset" type="attr"/>
2426
<public name="Widget.MaterialComponents.Badge" type="style"/>
2527
</resources>

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,10 @@
3535
<!-- Gravity.BOTTOM | Gravity.START -->
3636
<enum name="BOTTOM_START" value ="8388691"/>
3737
</attr>
38+
39+
<!-- Offset moves the badge towards the center of its anchor. -->
40+
<attr name="horizontalOffset" format="dimension"/>
41+
<attr name="verticalOffset" format="dimension"/>
3842
</declare-styleable>
3943

4044
</resources>

lib/javatests/com/google/android/material/badge/BadgeDrawableTest.java

Lines changed: 45 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
import androidx.annotation.XmlRes;
2727
import androidx.core.content.res.ResourcesCompat;
2828
import android.util.AttributeSet;
29+
import android.util.TypedValue;
2930
import android.view.Gravity;
3031
import androidx.test.core.app.ApplicationProvider;
3132
import com.google.android.material.badge.BadgeDrawable.SavedState;
@@ -34,6 +35,7 @@
3435
import org.junit.Test;
3536
import org.junit.runner.RunWith;
3637
import org.robolectric.RobolectricTestRunner;
38+
import org.robolectric.annotation.Config;
3739
import org.robolectric.annotation.internal.DoNotInstrument;
3840

3941
/** Test for {@link BadgeDrawable} */
@@ -43,6 +45,9 @@ public class BadgeDrawableTest {
4345

4446
private static final int TEST_BADGE_NUMBER = 26;
4547

48+
private static final int TEST_BADGE_HORIZONTAL_OFFSET = 10;
49+
private static final int TEST_BADGE_VERTICAL_OFFSET = 5;
50+
4651
private final Context context = ApplicationProvider.getApplicationContext();
4752

4853
@Before
@@ -62,6 +67,9 @@ public void testSavedState() {
6267
badgeDrawable.setNumber(TEST_BADGE_NUMBER);
6368
badgeDrawable.setBadgeGravity(BadgeDrawable.TOP_START);
6469

70+
badgeDrawable.setHorizontalOffset(TEST_BADGE_HORIZONTAL_OFFSET);
71+
badgeDrawable.setVerticalOffset(TEST_BADGE_VERTICAL_OFFSET);
72+
6573
badgeDrawable.setBackgroundColor(testBackgroundColor);
6674
badgeDrawable.setBadgeTextColor(testBadgeTextColor);
6775

@@ -79,6 +87,9 @@ public void testSavedState() {
7987
assertThat(restoredBadgeDrawable.getAlpha()).isEqualTo(255);
8088
assertThat(restoredBadgeDrawable.getMaxCharacterCount()).isEqualTo(4);
8189
assertThat(restoredBadgeDrawable.getBadgeGravity()).isEqualTo(BadgeDrawable.TOP_START);
90+
// badge offsets
91+
assertThat(restoredBadgeDrawable.getHorizontalOffset()).isEqualTo(TEST_BADGE_HORIZONTAL_OFFSET);
92+
assertThat(restoredBadgeDrawable.getVerticalOffset()).isEqualTo(TEST_BADGE_VERTICAL_OFFSET);
8293
}
8394

8495
// Verify that the hardcoded badge gravity attribute values match their piped Gravity counter
@@ -106,16 +117,24 @@ public void testBadgeGravityAttributeValue_bottomStart() {
106117
R.xml.standalone_badge_gravity_bottom_start, Gravity.BOTTOM | Gravity.START);
107118
}
108119

120+
@Test
121+
@Config(qualifiers = "w360dp-h640dp-xhdpi")
122+
public void testHorizontalOffset() {
123+
BadgeDrawable badgeDrawable =
124+
BadgeDrawable.createFromResource(context, R.xml.standalone_badge_offset);
125+
assertThat(badgeDrawable.getHorizontalOffset()).isEqualTo(dpToPx(TEST_BADGE_HORIZONTAL_OFFSET));
126+
}
127+
128+
@Test
129+
@Config(qualifiers = "w360dp-h640dp-xhdpi")
130+
public void testVerticalOffset() {
131+
BadgeDrawable badgeDrawable =
132+
BadgeDrawable.createFromResource(context, R.xml.standalone_badge_offset);
133+
assertThat(badgeDrawable.getVerticalOffset()).isEqualTo(dpToPx(TEST_BADGE_VERTICAL_OFFSET));
134+
}
135+
109136
private void testBadgeGravityValueHelper(@XmlRes int xmlId, int expectedValue) {
110-
AttributeSet attrs = DrawableUtils.parseDrawableXml(context, xmlId, "badge");
111-
@StyleRes int style = attrs.getStyleAttribute();
112-
if (style == 0) {
113-
style = R.style.Widget_MaterialComponents_Badge;
114-
}
115-
TypedArray a =
116-
context
117-
.getTheme()
118-
.obtainStyledAttributes(attrs, R.styleable.Badge, R.attr.badgeStyle, style);
137+
TypedArray a = getTypedArray(xmlId);
119138

120139
int value = 0;
121140
if (a.hasValue(R.styleable.Badge_badgeGravity)) {
@@ -124,4 +143,21 @@ private void testBadgeGravityValueHelper(@XmlRes int xmlId, int expectedValue) {
124143
assertThat(value).isEqualTo(expectedValue);
125144
a.recycle();
126145
}
146+
147+
private TypedArray getTypedArray(@XmlRes int xmlId) {
148+
AttributeSet attrs = DrawableUtils.parseDrawableXml(context, xmlId, "badge");
149+
@StyleRes int style = attrs.getStyleAttribute();
150+
if (style == 0) {
151+
style = R.style.Widget_MaterialComponents_Badge;
152+
}
153+
return context
154+
.getTheme()
155+
.obtainStyledAttributes(attrs, R.styleable.Badge, R.attr.badgeStyle, style);
156+
}
157+
158+
private int dpToPx(float dp) {
159+
return (int)
160+
TypedValue.applyDimension(
161+
TypedValue.COMPLEX_UNIT_DIP, dp, context.getResources().getDisplayMetrics());
162+
}
127163
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
<?xml version="1.0" encoding="utf-8"?>
2+
<!--
3+
Copyright 2019 The Android Open Source Project
4+
5+
Licensed under the Apache License, Version 2.0 (the "License");
6+
you may not use this file except in compliance with the License.
7+
You may obtain a copy of the License at
8+
9+
https://www.apache.org/licenses/LICENSE-2.0
10+
11+
Unless required by applicable law or agreed to in writing, software
12+
distributed under the License is distributed on an "AS IS" BASIS,
13+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
See the License for the specific language governing permissions and
15+
limitations under the License.
16+
-->
17+
18+
<badge
19+
xmlns:app="http://schemas.android.com/apk/res-auto"
20+
app:maxCharacterCount="2"
21+
app:horizontalOffset="10dp"
22+
app:verticalOffset="5dp"/>

0 commit comments

Comments
 (0)