Skip to content

Commit fb4761c

Browse files
pekingmeleticiarossi
authored andcommitted
[ButtonToggleGroup] Added APIs to customize inside spacing and corner size between buttons.
PiperOrigin-RevId: 628469557
1 parent a7a234b commit fb4761c

File tree

10 files changed

+213
-58
lines changed

10 files changed

+213
-58
lines changed

catalog/androidTest/javatests/io/material/catalog/button/ButtonToggleGroupDemoFragmentTest.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818

1919
import static androidx.test.espresso.Espresso.onView;
2020
import static androidx.test.espresso.action.ViewActions.click;
21+
import static androidx.test.espresso.action.ViewActions.scrollTo;
2122
import static androidx.test.espresso.assertion.ViewAssertions.matches;
2223
import static androidx.test.espresso.matcher.ViewMatchers.isDisplayed;
2324
import static androidx.test.espresso.matcher.ViewMatchers.isNotChecked;
@@ -70,7 +71,7 @@ public void testSelectionRequiredToggle() {
7071

7172
@Test
7273
public void testSelectionRequiredToggle_afterClicking() {
73-
onView(withId(io.material.catalog.button.R.id.switch_toggle)).perform(click());
74+
onView(withId(io.material.catalog.button.R.id.switch_toggle)).perform(scrollTo()).perform(click());
7475

7576
onView(withId(io.material.catalog.button.R.id.icon_only_group))
7677
.check(matches(checkSelectionRequired(true)));

catalog/java/io/material/catalog/button/ButtonToggleGroupDemoFragment.java

Lines changed: 23 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -30,8 +30,8 @@
3030
import androidx.annotation.Nullable;
3131
import com.google.android.material.button.MaterialButton;
3232
import com.google.android.material.button.MaterialButtonToggleGroup;
33-
import com.google.android.material.button.MaterialButtonToggleGroup.OnButtonCheckedListener;
3433
import com.google.android.material.materialswitch.MaterialSwitch;
34+
import com.google.android.material.slider.Slider;
3535
import com.google.android.material.snackbar.Snackbar;
3636
import io.material.catalog.feature.DemoFragment;
3737
import io.material.catalog.feature.DemoUtils;
@@ -92,15 +92,28 @@ public View onCreateDemoView(
9292

9393
for (MaterialButtonToggleGroup toggleGroup : toggleGroups) {
9494
toggleGroup.addOnButtonCheckedListener(
95-
new OnButtonCheckedListener() {
96-
@Override
97-
public void onButtonChecked(
98-
MaterialButtonToggleGroup group, int checkedId, boolean isChecked) {
99-
String message = "button" + (isChecked ? " checked" : " unchecked");
100-
Snackbar.make(view, message, Snackbar.LENGTH_SHORT).show();
101-
}
95+
(group, checkedId, isChecked) -> {
96+
String message = "button" + (isChecked ? " checked" : " unchecked");
97+
Snackbar.make(view, message, Snackbar.LENGTH_SHORT).show();
10298
});
10399
}
100+
101+
Slider insideCornerSizeSlider = view.findViewById(R.id.insideCornerSizeSlider);
102+
insideCornerSizeSlider.addOnChangeListener(
103+
(slider, value, fromUser) -> {
104+
for (MaterialButtonToggleGroup toggleGroup : toggleGroups) {
105+
toggleGroup.setInsideCornerSizeInFraction(value / 100f);
106+
}
107+
});
108+
109+
Slider spacingSlider = view.findViewById(R.id.spacingSlider);
110+
spacingSlider.addOnChangeListener(
111+
(slider, value, fromUser) -> {
112+
float pixelsInDp = view.getResources().getDisplayMetrics().density;
113+
for (MaterialButtonToggleGroup toggleGroup : toggleGroups) {
114+
toggleGroup.setSpacing((int) (value * pixelsInDp));
115+
}
116+
});
104117
return view;
105118
}
106119

@@ -109,9 +122,8 @@ private int getInsetForOrientation(int orientation) {
109122
}
110123

111124
private static void adjustParams(LayoutParams layoutParams, int orientation) {
112-
layoutParams.width = orientation == VERTICAL
113-
? LayoutParams.MATCH_PARENT
114-
: LayoutParams.WRAP_CONTENT;
125+
layoutParams.width =
126+
orientation == VERTICAL ? LayoutParams.MATCH_PARENT : LayoutParams.WRAP_CONTENT;
115127
}
116128

117129
@LayoutRes

catalog/java/io/material/catalog/button/res/layout/cat_buttons_toggle_group_fragment.xml

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,30 @@
147147
app:iconPadding="0dp"/>
148148
</com.google.android.material.button.MaterialButtonToggleGroup>
149149

150+
<TextView
151+
android:layout_width="wrap_content"
152+
android:layout_height="wrap_content"
153+
android:text="@string/cat_inside_corner_size_label"/>
154+
155+
<com.google.android.material.slider.Slider
156+
android:id="@+id/insideCornerSizeSlider"
157+
android:layout_width="match_parent"
158+
android:layout_height="wrap_content"
159+
android:valueFrom="0"
160+
android:valueTo="50"/>
161+
162+
<TextView
163+
android:layout_width="wrap_content"
164+
android:layout_height="wrap_content"
165+
android:text="@string/cat_spacing_label"/>
166+
167+
<com.google.android.material.slider.Slider
168+
android:id="@+id/spacingSlider"
169+
android:layout_width="match_parent"
170+
android:layout_height="wrap_content"
171+
android:valueFrom="0"
172+
android:valueTo="20"/>
173+
150174
<com.google.android.material.materialswitch.MaterialSwitch
151175
android:id="@+id/switch_toggle"
152176
android:paddingTop="16dp"

catalog/java/io/material/catalog/button/res/values/strings.xml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,10 @@
5858
<string name="cat_single_select">Single-select</string>
5959
<string name="cat_multi_select">Multi-select</string>
6060
<string name="cat_icon_only">Icon only</string>
61+
<string description="A label for a spacing slider [CHAR LIMIT=NONE]"
62+
name="cat_spacing_label">Spacing (dp)</string>
63+
<string description="A label for an inside corner size slider [CHAR LIMIT=NONE]"
64+
name="cat_inside_corner_size_label">Inside corners size (0 – 50%)</string>
6165
<string name="cat_button_label_private">Private</string>
6266
<string name="cat_button_label_team">Team</string>
6367
<string name="cat_button_label_everyone">Everyone</string>

docs/components/Button.md

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -746,11 +746,13 @@ A toggle button has a shared stroked container, icons and/or text labels.
746746

747747
#### Selection attributes
748748

749-
Element | Attribute | Related method(s) | Default value
750-
----------------------------------- | ----------------------- | ------------------------------------------------ | -------------
751-
**Single selection** | `app:singleSelection` | `setSingleSelection`<br/>`isSingleSelection` | `false`
752-
**Selection required** | `app:selectionRequired` | `setSelectionRequired`<br/>`isSelectionRequired` | `false`
753-
**Enable the group and all children | `android:enabled` | `setEnabled`<br/>`isEnabled` | `true`
749+
Element | Attribute | Related method(s) | Default value
750+
------------------------------------- | ----------------------- | --------------------------------------------------------------------------------------- | -------------
751+
**Single selection** | `app:singleSelection` | `setSingleSelection`<br/>`isSingleSelection` | `false`
752+
**Selection required** | `app:selectionRequired` | `setSelectionRequired`<br/>`isSelectionRequired` | `false`
753+
**Enable the group and all children** | `android:enabled` | `setEnabled`<br/>`isEnabled` | `true`
754+
**Radius of inside corners** | `app:insideCornerSize` | `setInsideCornerSizeByPx`<br/>`setInsideCornerSizeByFraction`<br/>`getInsideCornerSize` | `0dp`
755+
**Spacing between buttons** | `android:spacing` | `setSpacing`<br/>`getSpacing` | `0dp`
754756

755757
#### Styles
756758

lib/java/com/google/android/material/button/MaterialButtonToggleGroup.java

Lines changed: 87 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
import com.google.android.material.R;
2020

2121
import static com.google.android.material.theme.overlay.MaterialThemeOverlay.wrap;
22+
import static java.lang.Math.min;
2223

2324
import android.content.Context;
2425
import android.content.res.TypedArray;
@@ -36,6 +37,7 @@
3637
import androidx.annotation.IdRes;
3738
import androidx.annotation.NonNull;
3839
import androidx.annotation.Nullable;
40+
import androidx.annotation.Px;
3941
import androidx.annotation.VisibleForTesting;
4042
import androidx.core.view.AccessibilityDelegateCompat;
4143
import androidx.core.view.MarginLayoutParamsCompat;
@@ -48,6 +50,7 @@
4850
import com.google.android.material.internal.ViewUtils;
4951
import com.google.android.material.shape.AbsoluteCornerSize;
5052
import com.google.android.material.shape.CornerSize;
53+
import com.google.android.material.shape.RelativeCornerSize;
5154
import com.google.android.material.shape.ShapeAppearanceModel;
5255
import java.util.ArrayList;
5356
import java.util.Collections;
@@ -107,9 +110,9 @@
107110
*
108111
* <p>Any {@link MaterialButton}s added to this view group are automatically marked as {@code
109112
* checkable}, and by default multiple buttons within the same group can be checked. To enforce that
110-
* only one button can be checked at a time, set the {@code
111-
* app:singleSelection} attribute to {@code true} on the MaterialButtonToggleGroup or call {@link
112-
* #setSingleSelection(boolean) setSingleSelection(true)}.
113+
* only one button can be checked at a time, set the {@code app:singleSelection} attribute to {@code
114+
* true} on the MaterialButtonToggleGroup or call {@link #setSingleSelection(boolean)
115+
* setSingleSelection(true)}.
113116
*
114117
* <p>MaterialButtonToggleGroup is a {@link LinearLayout}. Using {@code
115118
* android:layout_width="MATCH_PARENT"} and removing {@code android:insetBottom} {@code
@@ -179,8 +182,10 @@ public int compare(MaterialButton v1, MaterialButton v2) {
179182
private boolean singleSelection;
180183
private boolean selectionRequired;
181184

182-
@IdRes
183-
private final int defaultCheckId;
185+
@NonNull private CornerSize insideCornerSize;
186+
@Px private int spacing;
187+
188+
@IdRes private final int defaultCheckId;
184189
private Set<Integer> checkedIds = new HashSet<>();
185190

186191
public MaterialButtonToggleGroup(@NonNull Context context) {
@@ -203,10 +208,16 @@ public MaterialButtonToggleGroup(
203208
setSingleSelection(
204209
attributes.getBoolean(R.styleable.MaterialButtonToggleGroup_singleSelection, false));
205210
defaultCheckId =
206-
attributes.getResourceId(
207-
R.styleable.MaterialButtonToggleGroup_checkedButton, View.NO_ID);
211+
attributes.getResourceId(R.styleable.MaterialButtonToggleGroup_checkedButton, View.NO_ID);
208212
selectionRequired =
209213
attributes.getBoolean(R.styleable.MaterialButtonToggleGroup_selectionRequired, false);
214+
insideCornerSize =
215+
ShapeAppearanceModel.getCornerSize(
216+
attributes,
217+
R.styleable.MaterialButtonToggleGroup_insideCornerSize,
218+
new AbsoluteCornerSize(0));
219+
spacing =
220+
attributes.getDimensionPixelSize(R.styleable.MaterialButtonToggleGroup_android_spacing, 0);
210221
setChildrenDrawingOrderEnabled(true);
211222
setEnabled(attributes.getBoolean(R.styleable.MaterialButtonToggleGroup_android_enabled, true));
212223
attributes.recycle();
@@ -313,7 +324,7 @@ public void onInitializeAccessibilityNodeInfo(@NonNull AccessibilityNodeInfo inf
313324
/* rowCount= */ 1,
314325
/* columnCount= */ getVisibleButtonCount(),
315326
/* hierarchical= */ false,
316-
/* selectionMode = */ isSingleSelection()
327+
/* selectionMode= */ isSingleSelection()
317328
? CollectionInfoCompat.SELECTION_MODE_SINGLE
318329
: CollectionInfoCompat.SELECTION_MODE_MULTIPLE));
319330
}
@@ -496,6 +507,29 @@ public void setSingleSelection(@BoolRes int id) {
496507
setSingleSelection(getResources().getBoolean(id));
497508
}
498509

510+
@Px
511+
public int getSpacing() {
512+
return spacing;
513+
}
514+
515+
public void setSpacing(@Px int spacing) {
516+
this.spacing = spacing;
517+
invalidate();
518+
requestLayout();
519+
}
520+
521+
public void setInsideCornerSizeInPx(@Px int px) {
522+
insideCornerSize = new AbsoluteCornerSize(px);
523+
updateChildShapes();
524+
invalidate();
525+
}
526+
527+
public void setInsideCornerSizeInFraction(float fraction) {
528+
insideCornerSize = new RelativeCornerSize(fraction);
529+
updateChildShapes();
530+
invalidate();
531+
}
532+
499533
private void setCheckedStateForView(@IdRes int viewId, boolean checked) {
500534
View checkedView = findViewById(viewId);
501535
if (checkedView instanceof MaterialButton) {
@@ -528,18 +562,20 @@ private void adjustChildMarginsAndUpdateLayout() {
528562
MaterialButton previousButton = getChildButton(i - 1);
529563

530564
// Calculates the margin adjustment to be the smaller of the two adjacent stroke widths
531-
int smallestStrokeWidth =
532-
Math.min(currentButton.getStrokeWidth(), previousButton.getStrokeWidth());
565+
int smallestStrokeWidth = 0;
566+
if (spacing <= 0) {
567+
smallestStrokeWidth = min(currentButton.getStrokeWidth(), previousButton.getStrokeWidth());
568+
}
533569

534570
LayoutParams params = buildLayoutParams(currentButton);
535571
if (getOrientation() == HORIZONTAL) {
536572
MarginLayoutParamsCompat.setMarginEnd(params, 0);
537-
MarginLayoutParamsCompat.setMarginStart(params, -smallestStrokeWidth);
573+
MarginLayoutParamsCompat.setMarginStart(params, spacing - smallestStrokeWidth);
538574
params.topMargin = 0;
539575
} else {
540576
params.bottomMargin = 0;
541-
params.topMargin = -smallestStrokeWidth;
542-
MarginLayoutParamsCompat.setMarginStart(params, 0);
577+
params.topMargin = spacing - smallestStrokeWidth;
578+
MarginLayoutParamsCompat.setMarginEnd(params, 0);
543579
}
544580

545581
currentButton.setLayoutParams(params);
@@ -648,6 +684,7 @@ private int getIndexWithinVisibleButtons(@Nullable View child) {
648684
private CornerData getNewCornerData(
649685
int index, int firstVisibleChildIndex, int lastVisibleChildIndex) {
650686
CornerData cornerData = originalCornerData.get(index);
687+
CornerData insideCornerData = new CornerData(insideCornerSize);
651688

652689
// If only one (visible) child exists, use its original corners
653690
if (firstVisibleChildIndex == lastVisibleChildIndex) {
@@ -656,14 +693,18 @@ private CornerData getNewCornerData(
656693

657694
boolean isHorizontal = getOrientation() == HORIZONTAL;
658695
if (index == firstVisibleChildIndex) {
659-
return isHorizontal ? CornerData.start(cornerData, this) : CornerData.top(cornerData);
696+
return isHorizontal
697+
? CornerData.start(cornerData, insideCornerData, this)
698+
: CornerData.top(cornerData, insideCornerData);
660699
}
661700

662701
if (index == lastVisibleChildIndex) {
663-
return isHorizontal ? CornerData.end(cornerData, this) : CornerData.bottom(cornerData);
702+
return isHorizontal
703+
? CornerData.end(cornerData, insideCornerData, this)
704+
: CornerData.bottom(cornerData, insideCornerData);
664705
}
665706

666-
return null;
707+
return insideCornerData;
667708
}
668709

669710
private static void updateBuilderWithCornerData(
@@ -782,11 +823,11 @@ private void updateChildOrder() {
782823
}
783824

784825
void onButtonCheckedStateChanged(@NonNull MaterialButton button, boolean isChecked) {
785-
// Checked state change is triggered by the button group, do not update checked ids again.
786-
if (skipCheckedStateTracker) {
787-
return;
788-
}
789-
checkInternal(button.getId(), isChecked);
826+
// Checked state change is triggered by the button group, do not update checked ids again.
827+
if (skipCheckedStateTracker) {
828+
return;
829+
}
830+
checkInternal(button.getId(), isChecked);
790831
}
791832

792833
/**
@@ -814,49 +855,56 @@ public void onPressedChanged(@NonNull MaterialButton button, boolean isPressed)
814855

815856
private static class CornerData {
816857

817-
private static final CornerSize noCorner = new AbsoluteCornerSize(0);
858+
@NonNull CornerSize topLeft;
859+
@NonNull CornerSize topRight;
860+
@NonNull CornerSize bottomRight;
861+
@NonNull CornerSize bottomLeft;
818862

819-
CornerSize topLeft;
820-
CornerSize topRight;
821-
CornerSize bottomRight;
822-
CornerSize bottomLeft;
863+
CornerData(@NonNull CornerSize cornerSize) {
864+
this(cornerSize, cornerSize, cornerSize, cornerSize);
865+
}
823866

824867
CornerData(
825-
CornerSize topLeft, CornerSize bottomLeft, CornerSize topRight, CornerSize bottomRight) {
868+
@NonNull CornerSize topLeft,
869+
@NonNull CornerSize bottomLeft,
870+
@NonNull CornerSize topRight,
871+
@NonNull CornerSize bottomRight) {
826872
this.topLeft = topLeft;
827873
this.topRight = topRight;
828874
this.bottomRight = bottomRight;
829875
this.bottomLeft = bottomLeft;
830876
}
831877

832878
/** Keep the start side of the corner original data */
833-
public static CornerData start(CornerData orig, View view) {
834-
return ViewUtils.isLayoutRtl(view) ? right(orig) : left(orig);
879+
public static CornerData start(
880+
@NonNull CornerData orig, @NonNull CornerData other, @NonNull View view) {
881+
return ViewUtils.isLayoutRtl(view) ? right(orig, other) : left(orig, other);
835882
}
836883

837884
/** Keep the end side of the corner original data */
838-
public static CornerData end(CornerData orig, View view) {
839-
return ViewUtils.isLayoutRtl(view) ? left(orig) : right(orig);
885+
public static CornerData end(
886+
@NonNull CornerData orig, @NonNull CornerData other, @NonNull View view) {
887+
return ViewUtils.isLayoutRtl(view) ? left(orig, other) : right(orig, other);
840888
}
841889

842890
/** Keep the left side of the corner original data */
843-
public static CornerData left(CornerData orig) {
844-
return new CornerData(orig.topLeft, orig.bottomLeft, noCorner, noCorner);
891+
public static CornerData left(@NonNull CornerData orig, @NonNull CornerData other) {
892+
return new CornerData(orig.topLeft, orig.bottomLeft, other.topRight, other.bottomRight);
845893
}
846894

847895
/** Keep the right side of the corner original data */
848-
public static CornerData right(CornerData orig) {
849-
return new CornerData(noCorner, noCorner, orig.topRight, orig.bottomRight);
896+
public static CornerData right(@NonNull CornerData orig, @NonNull CornerData other) {
897+
return new CornerData(other.topLeft, other.bottomLeft, orig.topRight, orig.bottomRight);
850898
}
851899

852900
/** Keep the top side of the corner original data */
853-
public static CornerData top(CornerData orig) {
854-
return new CornerData(orig.topLeft, noCorner, orig.topRight, noCorner);
901+
public static CornerData top(@NonNull CornerData orig, @NonNull CornerData other) {
902+
return new CornerData(orig.topLeft, other.bottomLeft, orig.topRight, other.bottomRight);
855903
}
856904

857905
/** Keep the bottom side of the corner original data */
858-
public static CornerData bottom(CornerData orig) {
859-
return new CornerData(noCorner, orig.bottomLeft, noCorner, orig.bottomRight);
906+
public static CornerData bottom(@NonNull CornerData orig, @NonNull CornerData other) {
907+
return new CornerData(other.topLeft, orig.bottomLeft, other.topRight, orig.bottomRight);
860908
}
861909
}
862910
}

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
<public name="iconTint" type="attr"/>
2525
<public name="iconTintMode" type="attr"/>
2626
<public name="toggleCheckedStateOnClick" type="attr"/>
27+
<public name="insideCornerSize" type="attr"/>
2728
<public name="materialButtonStyle" type="attr"/>
2829
<public name="materialIconButtonStyle" type="attr"/>
2930
<public name="materialIconButtonFilledStyle" type="attr"/>

0 commit comments

Comments
 (0)