/* * Copyright (C) 2024 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.google.android.material.button; import com.google.android.material.R; import static androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP; import static com.google.android.material.theme.overlay.MaterialThemeOverlay.wrap; import static java.lang.Math.max; import static java.lang.Math.min; import android.content.Context; import android.content.res.TypedArray; import android.graphics.Canvas; import android.graphics.drawable.Drawable; import android.graphics.drawable.InsetDrawable; import android.os.Build.VERSION; import android.os.Build.VERSION_CODES; import androidx.appcompat.widget.PopupMenu; import android.text.TextUtils; import android.util.AttributeSet; import android.util.Log; import android.view.Gravity; import android.view.LayoutInflater; import android.view.Menu; import android.view.MenuItem; import android.view.View; import android.view.ViewGroup; import android.widget.Button; import android.widget.LinearLayout; import androidx.annotation.DrawableRes; import androidx.annotation.IntDef; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.Px; import androidx.annotation.RestrictTo; import androidx.annotation.RestrictTo.Scope; import androidx.annotation.VisibleForTesting; import com.google.android.material.button.MaterialButton.OnPressedChangeListener; import com.google.android.material.button.MaterialButton.WidthChangeDirection; import com.google.android.material.internal.ThemeEnforcement; import com.google.android.material.internal.ViewUtils; import com.google.android.material.resources.MaterialAttributes; import com.google.android.material.shape.AbsoluteCornerSize; import com.google.android.material.shape.CornerSize; import com.google.android.material.shape.ShapeAppearance; import com.google.android.material.shape.ShapeAppearanceModel; import com.google.android.material.shape.StateListCornerSize; import com.google.android.material.shape.StateListShapeAppearanceModel; import com.google.android.material.shape.StateListSizeChange; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.SortedMap; import java.util.TreeMap; /** * A common container for a set of related {@link MaterialButton}s. The {@link MaterialButton}s in * this group will be shown on a single line. * *

This layout currently only supports child views of type {@link MaterialButton}. Buttons can be * added to this view group via XML, as follows: * *

 * <com.google.android.material.button.MaterialButtonGroup
 *     xmlns:android="http://schemas.android.com/apk/res/android"
 *     android:id="@+id/toggle_button_group"
 *     android:layout_width="wrap_content"
 *     android:layout_height="wrap_content">
 *
 *     <com.google.android.material.button.MaterialButton
 *         android:layout_width="wrap_content"
 *         android:layout_height="wrap_content"
 *         android:text="@string/button_label_private"/>
 *     <com.google.android.material.button.MaterialButton
 *         android:layout_width="wrap_content"
 *         android:layout_height="wrap_content"
 *         android:text="@string/button_label_team"/>
 *     <com.google.android.material.button.MaterialButton
 *         android:layout_width="wrap_content"
 *         android:layout_height="wrap_content"
 *         android:text="@string/button_label_everyone"/>
 *     <com.google.android.material.button.MaterialButton
 *         android:layout_width="wrap_content"
 *         android:layout_height="wrap_content"
 *         android:text="@string/button_label_custom"/>
 *
 * </com.google.android.material.button.MaterialButtonGroup>
 * 
* *

Buttons can also be added to this view group programmatically via the {@link #addView(View)} * methods. * *

MaterialButtonGroup is a {@link LinearLayout}. Using {@code * android:layout_width="MATCH_PARENT"} and removing {@code android:insetBottom} {@code * android:insetTop} on the children is recommended if using {@code VERTICAL}. * *

For more information, see the component * developer guidance and design * guidelines. */ public class MaterialButtonGroup extends LinearLayout { private static final String LOG_TAG = "MButtonGroup"; private static final int DEF_STYLE_RES = R.style.Widget_Material3_MaterialButtonGroup; public static final Object OVERFLOW_BUTTON_TAG = new Object(); /** * A value for {@code overflowMode}. It indicates that there's no handling to the buttons that * don't fit to the group size. */ public static final int OVERFLOW_MODE_NONE = 0; /** * A value for {@code overflowMode}. It indicates that the buttons that don't fit to the group * size will be hidden in the group and contained in a popup menu. */ public static final int OVERFLOW_MODE_MENU = 1; /** * A value for {@code overflowMode}. It indicates that the buttons that don't fit to the group * size will be displayed in another row under it. */ public static final int OVERFLOW_MODE_WRAP = 2; /** * The interface for the overflow mode attribute. * * @hide */ @RestrictTo(LIBRARY_GROUP) @Retention(RetentionPolicy.SOURCE) @IntDef({OVERFLOW_MODE_NONE, OVERFLOW_MODE_MENU, OVERFLOW_MODE_WRAP}) public @interface OverflowMode {} private int overflowMode = OVERFLOW_MODE_NONE; private final List originalChildShapeAppearanceModels = new ArrayList<>(); private final PressedStateTracker pressedStateTracker = new PressedStateTracker(); private final Comparator childOrderComparator = (v1, v2) -> { int checked = Boolean.valueOf(v1.isChecked()).compareTo(v2.isChecked()); if (checked != 0) { return checked; } int stateful = Boolean.valueOf(v1.isPressed()).compareTo(v2.isPressed()); if (stateful != 0) { return stateful; } // don't return 0s return Integer.compare(indexOfChild(v1), indexOfChild(v2)); }; private Integer[] childOrder; @Nullable StateListCornerSize innerCornerSize; @Nullable private StateListShapeAppearanceModel groupStateListShapeAppearance; @Px private int spacing; @Nullable private StateListSizeChange buttonSizeChange; private boolean childShapesDirty = true; // Variables for overflow menu mode. private final int overflowMenuItemIconPadding; private boolean buttonOverflowInitialized; private MaterialButton overflowButton; private PopupMenu popupMenu; private final Map popupMenuItemToButtonMapping = new HashMap<>(); private final Map buttonToMenuItemMapping = new HashMap<>(); private final List