Skip to content

Commit 7c66a2e

Browse files
pekingmedrchen
authored andcommitted
[ButtonGroup] Added the "wrap" overflow mode.
If the "wrap" overflow mode is in use, the button group will wrap the buttons that don't fit in one row to more rows below. PiperOrigin-RevId: 800551513
1 parent 8dd5a81 commit 7c66a2e

File tree

8 files changed

+357
-93
lines changed

8 files changed

+357
-93
lines changed

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

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,6 @@
2020
import static androidx.test.espresso.action.ViewActions.click;
2121
import static androidx.test.espresso.action.ViewActions.scrollTo;
2222
import static androidx.test.espresso.assertion.ViewAssertions.matches;
23-
import static androidx.test.espresso.matcher.ViewMatchers.isDisplayed;
2423
import static androidx.test.espresso.matcher.ViewMatchers.isNotChecked;
2524
import static androidx.test.espresso.matcher.ViewMatchers.withId;
2625

@@ -61,9 +60,7 @@ public void setUpAndLaunchFragment() {
6160

6261
@Test
6362
public void testSelectionRequiredToggle() {
64-
onView(withId(io.material.catalog.button.R.id.switch_toggle))
65-
.check(matches(isDisplayed()))
66-
.check(matches(isNotChecked()));
63+
onView(withId(io.material.catalog.button.R.id.switch_toggle)).check(matches(isNotChecked()));
6764

6865
onView(withId(io.material.catalog.button.R.id.icon_only_group))
6966
.check(matches(checkSelectionRequired(false)));

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

Lines changed: 47 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -18,14 +18,14 @@
1818
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
1919
xmlns:app="http://schemas.android.com/apk/res-auto"
2020
android:layout_width="match_parent"
21-
android:layout_height="match_parent"
22-
android:layout_gravity="center_vertical">
21+
android:layout_height="match_parent">
2322

2423
<LinearLayout
2524
android:layout_width="match_parent"
2625
android:layout_height="wrap_content"
2726
android:paddingTop="8dp"
28-
android:gravity="center_horizontal"
27+
android:paddingLeft="8dp"
28+
android:paddingRight="8dp"
2929
android:orientation="vertical">
3030
<TextView
3131
android:layout_width="wrap_content"
@@ -36,33 +36,38 @@
3636
android:textAppearance="?attr/textAppearanceSubtitle1"/>
3737

3838
<com.google.android.material.button.MaterialButtonToggleGroup
39-
android:layout_width="wrap_content"
39+
android:layout_width="match_parent"
4040
android:layout_height="wrap_content"
41-
app:singleSelection="true">
41+
app:singleSelection="true"
42+
app:overflowMode="wrap">
4243

4344
<Button
44-
style="?attr/materialButtonOutlinedStyle"
45-
android:layout_width="wrap_content"
46-
android:layout_height="wrap_content"
47-
android:text="@string/cat_button_label_private" />
45+
style="?attr/materialButtonOutlinedStyle"
46+
android:layout_width="wrap_content"
47+
android:layout_height="wrap_content"
48+
android:layout_gravity="start|top"
49+
android:text="@string/cat_button_label_private" />
4850

4951
<Button
50-
style="?attr/materialButtonOutlinedStyle"
51-
android:layout_width="wrap_content"
52-
android:layout_height="wrap_content"
53-
android:text="@string/cat_button_label_team" />
52+
style="?attr/materialButtonOutlinedStyle"
53+
android:layout_width="wrap_content"
54+
android:layout_height="wrap_content"
55+
android:layout_gravity="start|top"
56+
android:text="@string/cat_button_label_team" />
5457

5558
<Button
56-
style="?attr/materialButtonOutlinedStyle"
57-
android:layout_width="wrap_content"
58-
android:layout_height="wrap_content"
59-
android:text="@string/cat_button_label_everyone" />
59+
style="?attr/materialButtonOutlinedStyle"
60+
android:layout_width="wrap_content"
61+
android:layout_height="wrap_content"
62+
android:layout_gravity="start|top"
63+
android:text="@string/cat_button_label_everyone" />
6064

6165
<Button
62-
style="?attr/materialButtonOutlinedStyle"
63-
android:layout_width="wrap_content"
64-
android:layout_height="wrap_content"
65-
android:text="@string/cat_button_label_custom" />
66+
style="?attr/materialButtonOutlinedStyle"
67+
android:layout_width="wrap_content"
68+
android:layout_height="wrap_content"
69+
android:layout_gravity="start|top"
70+
android:text="@string/cat_button_label_custom" />
6671
</com.google.android.material.button.MaterialButtonToggleGroup>
6772

6873
<TextView
@@ -74,26 +79,27 @@
7479
android:textAppearance="?attr/textAppearanceSubtitle1"/>
7580

7681
<com.google.android.material.button.MaterialButtonToggleGroup
77-
android:layout_width="wrap_content"
78-
android:layout_height="wrap_content">
82+
android:layout_width="match_parent"
83+
android:layout_height="wrap_content"
84+
app:overflowMode="wrap">
7985
<Button
80-
style="?attr/materialButtonOutlinedStyle"
81-
android:layout_width="0dp"
82-
android:layout_height="wrap_content"
83-
android:layout_weight="1"
84-
android:text="@string/cat_button_label_cheese" />
86+
style="?attr/materialButtonOutlinedStyle"
87+
android:layout_width="wrap_content"
88+
android:layout_height="wrap_content"
89+
android:layout_gravity="start|top"
90+
android:text="@string/cat_button_label_cheese" />
8591
<Button
86-
style="?attr/materialButtonOutlinedStyle"
87-
android:layout_width="0dp"
88-
android:layout_height="wrap_content"
89-
android:layout_weight="1"
90-
android:text="@string/cat_button_label_tomatoes" />
92+
style="?attr/materialButtonOutlinedStyle"
93+
android:layout_width="wrap_content"
94+
android:layout_height="wrap_content"
95+
android:layout_gravity="start|top"
96+
android:text="@string/cat_button_label_tomatoes" />
9197
<Button
92-
style="?attr/materialButtonOutlinedStyle"
93-
android:layout_width="0dp"
94-
android:layout_height="wrap_content"
95-
android:layout_weight="1"
96-
android:text="@string/cat_button_label_mushrooms" />
98+
style="?attr/materialButtonOutlinedStyle"
99+
android:layout_width="wrap_content"
100+
android:layout_height="wrap_content"
101+
android:layout_gravity="start|top"
102+
android:text="@string/cat_button_label_mushrooms" />
97103
</com.google.android.material.button.MaterialButtonToggleGroup>
98104

99105
<TextView
@@ -106,8 +112,9 @@
106112

107113
<com.google.android.material.button.MaterialButtonToggleGroup
108114
android:id="@+id/icon_only_group"
109-
android:layout_width="wrap_content"
110-
android:layout_height="wrap_content">
115+
android:layout_width="match_parent"
116+
android:layout_height="wrap_content"
117+
app:overflowMode="wrap">
111118
<Button
112119
style="?attr/materialButtonOutlinedStyle"
113120
android:layout_width="wrap_content"

docs/components/ButtonGroup.md

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -371,6 +371,8 @@ clipped or wrapped.
371371

372372
#### Overflow mode - menu
373373

374+
![Button group overflow menu mode](assets/buttons/buttongroup-overflow-menu.png)
375+
374376
Setting `overflowMode=menu` dynamically hides child buttons that don't fit in
375377
the current screen width in a popup menu. An icon button will be added
376378
automatically at the end of the button group for toggling the popup menu, once
@@ -390,6 +392,40 @@ selection.
390392
or as a menu item in the overflow menu. Visibility of all child buttons will be
391393
managed by the button group when setting `overflowMode=menu`.
392394

395+
#### Overflow mode - wrap
396+
397+
![Button group overflow wrap mode](assets/buttons/buttongroup-overflow-wrap.png)
398+
399+
Setting `overflowMode=wrap` will cause the buttons to wrap to the next line when
400+
they don't fit on the current line. This can be useful when you want to display
401+
all buttons without hiding them in a menu, but you have a limited amount of
402+
horizontal space. The buttons will maintain their shape and size as defined in
403+
the layout.
404+
405+
This mode is useful in cases:
406+
407+
* **Responsive layouts:** When you want buttons to adapt to different screen
408+
sizes by wrapping to the next line instead of being hidden.
409+
* **Content-heavy interfaces:** When you have many buttons and want to ensure
410+
they are all visible, even on smaller screens.
411+
* **Accessibility:** When you need to display all buttons but there's not
412+
enough space.
413+
414+
##### Configuration
415+
416+
Do not use the `wrap` overflow mode with `layout_width="wrap_content"`,
417+
`orientation="vertical"`, or `layout_weight` in child buttons, due to undefined
418+
expected behaviors. The `layout_width` can be set to a fixed dp size or
419+
`match_parent`. The height of the group is determined by the number of wrapped
420+
rows and spacing. So the value of `layout_height` will be ignored.
421+
422+
##### Wrapping gravity
423+
424+
In order to make the child buttons wrapped to the correct position, they must
425+
have `layout_gravity` set. When `start|top` is used, the `android:gravity` in
426+
`MaterialButtonGroup` can be ignored. If `end|top` is used, the
427+
`android:gravity` in `MaterialButtonGroup` must be set to `end` as well.
428+
393429
### Making buttons accessible
394430

395431
Buttons support content labeling for accessibility and are readable by most
31.1 KB
Loading

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

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -214,6 +214,13 @@ interface OnPressedChangeListener {
214214
@Retention(RetentionPolicy.SOURCE)
215215
public @interface IconGravity {}
216216

217+
enum WidthChangeDirection {
218+
NONE,
219+
START,
220+
END,
221+
BOTH
222+
}
223+
217224
private static final String LOG_TAG = "MaterialButton";
218225

219226
private static final int DEF_STYLE_RES = R.style.Widget_MaterialComponents_Button;
@@ -260,6 +267,7 @@ interface OnPressedChangeListener {
260267
@Px int allowedWidthDecrease = UNSET;
261268
@Nullable StateListSizeChange sizeChange;
262269
@Px int widthChangeMax;
270+
private WidthChangeDirection widthChangeDirection = WidthChangeDirection.BOTH;
263271
private float displayedWidthIncrease;
264272
private float displayedWidthDecrease;
265273
@Nullable private SpringAnimation widthIncreaseSpringAnimation;
@@ -1527,7 +1535,7 @@ private void maybeAnimateSize(boolean skipAnimation) {
15271535
// Animate width.
15281536
int widthChange =
15291537
min(
1530-
widthChangeMax,
1538+
calculateEffectiveWidthChangeMax(),
15311539
sizeChange
15321540
.getSizeChangeForState(getDrawableState())
15331541
.widthChange
@@ -1539,6 +1547,19 @@ private void maybeAnimateSize(boolean skipAnimation) {
15391547
}
15401548
}
15411549

1550+
/** Returns the effective width change max based on the width change direction. */
1551+
private int calculateEffectiveWidthChangeMax() {
1552+
switch (widthChangeDirection) {
1553+
case BOTH:
1554+
return this.widthChangeMax;
1555+
case START:
1556+
case END:
1557+
return this.widthChangeMax / 2;
1558+
case NONE:
1559+
}
1560+
return 0;
1561+
}
1562+
15421563
private boolean isInHorizontalButtonGroup() {
15431564
return getParent() instanceof MaterialButtonGroup
15441565
&& ((MaterialButtonGroup) getParent()).getOrientation() == LinearLayout.HORIZONTAL;
@@ -1558,6 +1579,13 @@ void setWidthChangeMax(@Px int widthChangeMax) {
15581579
}
15591580
}
15601581

1582+
void setWidthChangeDirection(@NonNull WidthChangeDirection widthChangeDirection) {
1583+
if (this.widthChangeDirection != widthChangeDirection) {
1584+
this.widthChangeDirection = widthChangeDirection;
1585+
maybeAnimateSize(/* skipAnimation= */ true);
1586+
}
1587+
}
1588+
15611589
@Px
15621590
int getAllowedWidthDecrease() {
15631591
return allowedWidthDecrease;

0 commit comments

Comments
 (0)