Skip to content

Commit

Permalink
[ButtonToggleGroup] Added APIs to customize inside spacing and corner…
Browse files Browse the repository at this point in the history
… size between buttons.

PiperOrigin-RevId: 628469557
  • Loading branch information
pekingme authored and leticiarossi committed Apr 29, 2024
1 parent a7a234b commit fb4761c
Show file tree
Hide file tree
Showing 10 changed files with 213 additions and 58 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@

import static androidx.test.espresso.Espresso.onView;
import static androidx.test.espresso.action.ViewActions.click;
import static androidx.test.espresso.action.ViewActions.scrollTo;
import static androidx.test.espresso.assertion.ViewAssertions.matches;
import static androidx.test.espresso.matcher.ViewMatchers.isDisplayed;
import static androidx.test.espresso.matcher.ViewMatchers.isNotChecked;
Expand Down Expand Up @@ -70,7 +71,7 @@ public void testSelectionRequiredToggle() {

@Test
public void testSelectionRequiredToggle_afterClicking() {
onView(withId(io.material.catalog.button.R.id.switch_toggle)).perform(click());
onView(withId(io.material.catalog.button.R.id.switch_toggle)).perform(scrollTo()).perform(click());

onView(withId(io.material.catalog.button.R.id.icon_only_group))
.check(matches(checkSelectionRequired(true)));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,8 @@
import androidx.annotation.Nullable;
import com.google.android.material.button.MaterialButton;
import com.google.android.material.button.MaterialButtonToggleGroup;
import com.google.android.material.button.MaterialButtonToggleGroup.OnButtonCheckedListener;
import com.google.android.material.materialswitch.MaterialSwitch;
import com.google.android.material.slider.Slider;
import com.google.android.material.snackbar.Snackbar;
import io.material.catalog.feature.DemoFragment;
import io.material.catalog.feature.DemoUtils;
Expand Down Expand Up @@ -92,15 +92,28 @@ public View onCreateDemoView(

for (MaterialButtonToggleGroup toggleGroup : toggleGroups) {
toggleGroup.addOnButtonCheckedListener(
new OnButtonCheckedListener() {
@Override
public void onButtonChecked(
MaterialButtonToggleGroup group, int checkedId, boolean isChecked) {
String message = "button" + (isChecked ? " checked" : " unchecked");
Snackbar.make(view, message, Snackbar.LENGTH_SHORT).show();
}
(group, checkedId, isChecked) -> {
String message = "button" + (isChecked ? " checked" : " unchecked");
Snackbar.make(view, message, Snackbar.LENGTH_SHORT).show();
});
}

Slider insideCornerSizeSlider = view.findViewById(R.id.insideCornerSizeSlider);
insideCornerSizeSlider.addOnChangeListener(
(slider, value, fromUser) -> {
for (MaterialButtonToggleGroup toggleGroup : toggleGroups) {
toggleGroup.setInsideCornerSizeInFraction(value / 100f);
}
});

Slider spacingSlider = view.findViewById(R.id.spacingSlider);
spacingSlider.addOnChangeListener(
(slider, value, fromUser) -> {
float pixelsInDp = view.getResources().getDisplayMetrics().density;
for (MaterialButtonToggleGroup toggleGroup : toggleGroups) {
toggleGroup.setSpacing((int) (value * pixelsInDp));
}
});
return view;
}

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

private static void adjustParams(LayoutParams layoutParams, int orientation) {
layoutParams.width = orientation == VERTICAL
? LayoutParams.MATCH_PARENT
: LayoutParams.WRAP_CONTENT;
layoutParams.width =
orientation == VERTICAL ? LayoutParams.MATCH_PARENT : LayoutParams.WRAP_CONTENT;
}

@LayoutRes
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,30 @@
app:iconPadding="0dp"/>
</com.google.android.material.button.MaterialButtonToggleGroup>

<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/cat_inside_corner_size_label"/>

<com.google.android.material.slider.Slider
android:id="@+id/insideCornerSizeSlider"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:valueFrom="0"
android:valueTo="50"/>

<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/cat_spacing_label"/>

<com.google.android.material.slider.Slider
android:id="@+id/spacingSlider"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:valueFrom="0"
android:valueTo="20"/>

<com.google.android.material.materialswitch.MaterialSwitch
android:id="@+id/switch_toggle"
android:paddingTop="16dp"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,10 @@
<string name="cat_single_select">Single-select</string>
<string name="cat_multi_select">Multi-select</string>
<string name="cat_icon_only">Icon only</string>
<string description="A label for a spacing slider [CHAR LIMIT=NONE]"
name="cat_spacing_label">Spacing (dp)</string>
<string description="A label for an inside corner size slider [CHAR LIMIT=NONE]"
name="cat_inside_corner_size_label">Inside corners size (0 – 50%)</string>
<string name="cat_button_label_private">Private</string>
<string name="cat_button_label_team">Team</string>
<string name="cat_button_label_everyone">Everyone</string>
Expand Down
12 changes: 7 additions & 5 deletions docs/components/Button.md
Original file line number Diff line number Diff line change
Expand Up @@ -746,11 +746,13 @@ A toggle button has a shared stroked container, icons and/or text labels.

#### Selection attributes

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

#### Styles

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
import com.google.android.material.R;

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

import android.content.Context;
import android.content.res.TypedArray;
Expand All @@ -36,6 +37,7 @@
import androidx.annotation.IdRes;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.Px;
import androidx.annotation.VisibleForTesting;
import androidx.core.view.AccessibilityDelegateCompat;
import androidx.core.view.MarginLayoutParamsCompat;
Expand All @@ -48,6 +50,7 @@
import com.google.android.material.internal.ViewUtils;
import com.google.android.material.shape.AbsoluteCornerSize;
import com.google.android.material.shape.CornerSize;
import com.google.android.material.shape.RelativeCornerSize;
import com.google.android.material.shape.ShapeAppearanceModel;
import java.util.ArrayList;
import java.util.Collections;
Expand Down Expand Up @@ -107,9 +110,9 @@
*
* <p>Any {@link MaterialButton}s added to this view group are automatically marked as {@code
* checkable}, and by default multiple buttons within the same group can be checked. To enforce that
* only one button can be checked at a time, set the {@code
* app:singleSelection} attribute to {@code true} on the MaterialButtonToggleGroup or call {@link
* #setSingleSelection(boolean) setSingleSelection(true)}.
* only one button can be checked at a time, set the {@code app:singleSelection} attribute to {@code
* true} on the MaterialButtonToggleGroup or call {@link #setSingleSelection(boolean)
* setSingleSelection(true)}.
*
* <p>MaterialButtonToggleGroup is a {@link LinearLayout}. Using {@code
* android:layout_width="MATCH_PARENT"} and removing {@code android:insetBottom} {@code
Expand Down Expand Up @@ -179,8 +182,10 @@ public int compare(MaterialButton v1, MaterialButton v2) {
private boolean singleSelection;
private boolean selectionRequired;

@IdRes
private final int defaultCheckId;
@NonNull private CornerSize insideCornerSize;
@Px private int spacing;

@IdRes private final int defaultCheckId;
private Set<Integer> checkedIds = new HashSet<>();

public MaterialButtonToggleGroup(@NonNull Context context) {
Expand All @@ -203,10 +208,16 @@ public MaterialButtonToggleGroup(
setSingleSelection(
attributes.getBoolean(R.styleable.MaterialButtonToggleGroup_singleSelection, false));
defaultCheckId =
attributes.getResourceId(
R.styleable.MaterialButtonToggleGroup_checkedButton, View.NO_ID);
attributes.getResourceId(R.styleable.MaterialButtonToggleGroup_checkedButton, View.NO_ID);
selectionRequired =
attributes.getBoolean(R.styleable.MaterialButtonToggleGroup_selectionRequired, false);
insideCornerSize =
ShapeAppearanceModel.getCornerSize(
attributes,
R.styleable.MaterialButtonToggleGroup_insideCornerSize,
new AbsoluteCornerSize(0));
spacing =
attributes.getDimensionPixelSize(R.styleable.MaterialButtonToggleGroup_android_spacing, 0);
setChildrenDrawingOrderEnabled(true);
setEnabled(attributes.getBoolean(R.styleable.MaterialButtonToggleGroup_android_enabled, true));
attributes.recycle();
Expand Down Expand Up @@ -313,7 +324,7 @@ public void onInitializeAccessibilityNodeInfo(@NonNull AccessibilityNodeInfo inf
/* rowCount= */ 1,
/* columnCount= */ getVisibleButtonCount(),
/* hierarchical= */ false,
/* selectionMode = */ isSingleSelection()
/* selectionMode= */ isSingleSelection()
? CollectionInfoCompat.SELECTION_MODE_SINGLE
: CollectionInfoCompat.SELECTION_MODE_MULTIPLE));
}
Expand Down Expand Up @@ -496,6 +507,29 @@ public void setSingleSelection(@BoolRes int id) {
setSingleSelection(getResources().getBoolean(id));
}

@Px
public int getSpacing() {
return spacing;
}

public void setSpacing(@Px int spacing) {
this.spacing = spacing;
invalidate();
requestLayout();
}

public void setInsideCornerSizeInPx(@Px int px) {
insideCornerSize = new AbsoluteCornerSize(px);
updateChildShapes();
invalidate();
}

public void setInsideCornerSizeInFraction(float fraction) {
insideCornerSize = new RelativeCornerSize(fraction);
updateChildShapes();
invalidate();
}

private void setCheckedStateForView(@IdRes int viewId, boolean checked) {
View checkedView = findViewById(viewId);
if (checkedView instanceof MaterialButton) {
Expand Down Expand Up @@ -528,18 +562,20 @@ private void adjustChildMarginsAndUpdateLayout() {
MaterialButton previousButton = getChildButton(i - 1);

// Calculates the margin adjustment to be the smaller of the two adjacent stroke widths
int smallestStrokeWidth =
Math.min(currentButton.getStrokeWidth(), previousButton.getStrokeWidth());
int smallestStrokeWidth = 0;
if (spacing <= 0) {
smallestStrokeWidth = min(currentButton.getStrokeWidth(), previousButton.getStrokeWidth());
}

LayoutParams params = buildLayoutParams(currentButton);
if (getOrientation() == HORIZONTAL) {
MarginLayoutParamsCompat.setMarginEnd(params, 0);
MarginLayoutParamsCompat.setMarginStart(params, -smallestStrokeWidth);
MarginLayoutParamsCompat.setMarginStart(params, spacing - smallestStrokeWidth);
params.topMargin = 0;
} else {
params.bottomMargin = 0;
params.topMargin = -smallestStrokeWidth;
MarginLayoutParamsCompat.setMarginStart(params, 0);
params.topMargin = spacing - smallestStrokeWidth;
MarginLayoutParamsCompat.setMarginEnd(params, 0);
}

currentButton.setLayoutParams(params);
Expand Down Expand Up @@ -648,6 +684,7 @@ private int getIndexWithinVisibleButtons(@Nullable View child) {
private CornerData getNewCornerData(
int index, int firstVisibleChildIndex, int lastVisibleChildIndex) {
CornerData cornerData = originalCornerData.get(index);
CornerData insideCornerData = new CornerData(insideCornerSize);

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

boolean isHorizontal = getOrientation() == HORIZONTAL;
if (index == firstVisibleChildIndex) {
return isHorizontal ? CornerData.start(cornerData, this) : CornerData.top(cornerData);
return isHorizontal
? CornerData.start(cornerData, insideCornerData, this)
: CornerData.top(cornerData, insideCornerData);
}

if (index == lastVisibleChildIndex) {
return isHorizontal ? CornerData.end(cornerData, this) : CornerData.bottom(cornerData);
return isHorizontal
? CornerData.end(cornerData, insideCornerData, this)
: CornerData.bottom(cornerData, insideCornerData);
}

return null;
return insideCornerData;
}

private static void updateBuilderWithCornerData(
Expand Down Expand Up @@ -782,11 +823,11 @@ private void updateChildOrder() {
}

void onButtonCheckedStateChanged(@NonNull MaterialButton button, boolean isChecked) {
// Checked state change is triggered by the button group, do not update checked ids again.
if (skipCheckedStateTracker) {
return;
}
checkInternal(button.getId(), isChecked);
// Checked state change is triggered by the button group, do not update checked ids again.
if (skipCheckedStateTracker) {
return;
}
checkInternal(button.getId(), isChecked);
}

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

private static class CornerData {

private static final CornerSize noCorner = new AbsoluteCornerSize(0);
@NonNull CornerSize topLeft;
@NonNull CornerSize topRight;
@NonNull CornerSize bottomRight;
@NonNull CornerSize bottomLeft;

CornerSize topLeft;
CornerSize topRight;
CornerSize bottomRight;
CornerSize bottomLeft;
CornerData(@NonNull CornerSize cornerSize) {
this(cornerSize, cornerSize, cornerSize, cornerSize);
}

CornerData(
CornerSize topLeft, CornerSize bottomLeft, CornerSize topRight, CornerSize bottomRight) {
@NonNull CornerSize topLeft,
@NonNull CornerSize bottomLeft,
@NonNull CornerSize topRight,
@NonNull CornerSize bottomRight) {
this.topLeft = topLeft;
this.topRight = topRight;
this.bottomRight = bottomRight;
this.bottomLeft = bottomLeft;
}

/** Keep the start side of the corner original data */
public static CornerData start(CornerData orig, View view) {
return ViewUtils.isLayoutRtl(view) ? right(orig) : left(orig);
public static CornerData start(
@NonNull CornerData orig, @NonNull CornerData other, @NonNull View view) {
return ViewUtils.isLayoutRtl(view) ? right(orig, other) : left(orig, other);
}

/** Keep the end side of the corner original data */
public static CornerData end(CornerData orig, View view) {
return ViewUtils.isLayoutRtl(view) ? left(orig) : right(orig);
public static CornerData end(
@NonNull CornerData orig, @NonNull CornerData other, @NonNull View view) {
return ViewUtils.isLayoutRtl(view) ? left(orig, other) : right(orig, other);
}

/** Keep the left side of the corner original data */
public static CornerData left(CornerData orig) {
return new CornerData(orig.topLeft, orig.bottomLeft, noCorner, noCorner);
public static CornerData left(@NonNull CornerData orig, @NonNull CornerData other) {
return new CornerData(orig.topLeft, orig.bottomLeft, other.topRight, other.bottomRight);
}

/** Keep the right side of the corner original data */
public static CornerData right(CornerData orig) {
return new CornerData(noCorner, noCorner, orig.topRight, orig.bottomRight);
public static CornerData right(@NonNull CornerData orig, @NonNull CornerData other) {
return new CornerData(other.topLeft, other.bottomLeft, orig.topRight, orig.bottomRight);
}

/** Keep the top side of the corner original data */
public static CornerData top(CornerData orig) {
return new CornerData(orig.topLeft, noCorner, orig.topRight, noCorner);
public static CornerData top(@NonNull CornerData orig, @NonNull CornerData other) {
return new CornerData(orig.topLeft, other.bottomLeft, orig.topRight, other.bottomRight);
}

/** Keep the bottom side of the corner original data */
public static CornerData bottom(CornerData orig) {
return new CornerData(noCorner, orig.bottomLeft, noCorner, orig.bottomRight);
public static CornerData bottom(@NonNull CornerData orig, @NonNull CornerData other) {
return new CornerData(other.topLeft, orig.bottomLeft, other.topRight, orig.bottomRight);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
<public name="iconTint" type="attr"/>
<public name="iconTintMode" type="attr"/>
<public name="toggleCheckedStateOnClick" type="attr"/>
<public name="insideCornerSize" type="attr"/>
<public name="materialButtonStyle" type="attr"/>
<public name="materialIconButtonStyle" type="attr"/>
<public name="materialIconButtonFilledStyle" type="attr"/>
Expand Down
Loading

0 comments on commit fb4761c

Please sign in to comment.