LoginSignup
6

More than 3 years have passed since last update.

MaterialButtonToggleGroupを使ってみる

Last updated at Posted at 2020-01-19

まえおき

とあるアプリ開発で「定休日」の入力フォームを作っていたときのこと。
Rectangle.png
こういうフォームを作る必要があった。

一昔前だったら「そんなiOSっぽいフォームはAndroidの世界にはありませんよー?」だったんだけど、
今はもうMaterial Designのガイドにも存在している。
貼り付けた画像_2020_01_19_17_29.png

調べてみると、Android用のコンポーネントもあるようだ。
https://material.io/develop/android/components/material-button-toggle-group/

そんなわけで、昔からあるCheckBox、ToggleButtonではなく、新しそうなMaterialButtonToggleGroupを使ってみた。
image.png

画面横幅にあわせてボタンを配置する

何も考えずにリファレンスのとおりにボタンを配置してみる

MaterialButtonToggleGroupに愚直にボタンを置くだけだと・・・

res/layout/activity.xml
    <com.google.android.material.button.MaterialButtonToggleGroup
        android:layout_width="match_parent"
        android:layout_height="wrap_content">

        <com.google.android.material.button.MaterialButton
            style="?attr/materialButtonOutlinedStyle"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="日" />

        <com.google.android.material.button.MaterialButton
            style="?attr/materialButtonOutlinedStyle"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="月" />

        <com.google.android.material.button.MaterialButton
            style="?attr/materialButtonOutlinedStyle"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="火" />

  (以下略)

image.png

こんな感じで、画面をはみ出してしまう。
これは、MaterialButtonはButton継承の部品なので、minWidth, minHeightが設定されているためだ。

じゃあ minWidth=0 を指定すると・・・?

res/layout/activity.xml
    <com.google.android.material.button.MaterialButtonToggleGroup
        android:layout_width="match_parent"
        android:layout_height="wrap_content">

        <com.google.android.material.button.MaterialButton
            style="?attr/materialButtonOutlinedStyle"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:minWidth="0dp"
            android:text="日" />

        <com.google.android.material.button.MaterialButton
            style="?attr/materialButtonOutlinedStyle"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:minWidth="0dp"
            android:text="月" />

        <com.google.android.material.button.MaterialButton
            style="?attr/materialButtonOutlinedStyle"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:minWidth="0dp"
            android:text="火" />

image.png

wrap_parentの挙動そのものになる。でも画面幅ぴったりにはならない。

layout_weightを指定すれば良い!

画面幅にあわせるといえば、layout_weightだ。
ただ、このプロパティはLinearLayoutが親じゃないと使えない。

そこでもう一度リファレンスを見てみよう。

貼り付けた画像_2020_01_19_21_10.png

なんとMaterialButtonToggleGroupはLinearLayout継承のコンポーネントではないか!

res/layout/activity.xml
    <com.google.android.material.button.MaterialButtonToggleGroup
        android:layout_width="match_parent"
        android:layout_height="wrap_content">

        <com.google.android.material.button.MaterialButton
            style="?attr/materialButtonOutlinedStyle"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_weight="1"
            android:minWidth="0dp"
            android:text="日" />

        <com.google.android.material.button.MaterialButton
            style="?attr/materialButtonOutlinedStyle"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_weight="1"
            android:minWidth="0dp"
            android:text="月" />

        <com.google.android.material.button.MaterialButton
            style="?attr/materialButtonOutlinedStyle"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_weight="1"
            android:minWidth="0dp"
            android:text="火" />

image.png

layout_weight=1を各ボタンに付けることで、期待通りになった!

ViewModelを使って2-wayデータバインディングする

画面回転するとフォームの入力値がリセットされるのでは困る。
よほど凝ったフォームじゃない限りは、フォームの入力値はViewModelに持たせるのが定石である。

MainActivityViewModel.kt

class MainActivityViewModel : ViewModel() {
    val title = MutableLiveData<String>()
    val closedOnSun = MutableLiveData<Boolean>()
    val closedOnMon = MutableLiveData<Boolean>()
    val closedOnTue = MutableLiveData<Boolean>()
    val closedOnWed = MutableLiveData<Boolean>()
    val closedOnThu = MutableLiveData<Boolean>()
    val closedOnFri = MutableLiveData<Boolean>()
    val closedOnSat = MutableLiveData<Boolean>()
}

超適当だけど、とりあえずこんな感じで各ボタンのチェック状態を覚えておくLiveDataをもったViewModelを作り、

res/layout/activity.xml
<layout>
    <data>
        <variable
            name="viewModel"
            type="io.github.yusukeiwaki.materialbuttontogglegroupplayground.MainActivityViewModel" />
    </data>

  (中略)

        <com.google.android.material.button.MaterialButtonToggleGroup
            android:layout_width="match_parent"
            android:layout_height="wrap_content">

            <com.google.android.material.button.MaterialButton
                style="?attr/materialButtonOutlinedStyle"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_weight="1"
                android:checked="@={viewModel.closedOnSun}"
                android:minWidth="0dp"
                android:text="日" />

            <com.google.android.material.button.MaterialButton
                style="?attr/materialButtonOutlinedStyle"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_weight="1"
                android:checked="@={viewModel.closedOnMon}"
                android:minWidth="0dp"
                android:text="月" />

            <com.google.android.material.button.MaterialButton
                style="?attr/materialButtonOutlinedStyle"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_weight="1"
                android:checked="@={viewModel.closedOnTue}"
                android:minWidth="0dp"
                android:text="火" />

こんな感じで、 android:checked にそれを指定する。

CheckBoxやToggleButtonだとこれでうまくいくはずだ。
しかしながらMaterialButtonToggleGroupを使うとこれではうまくいかない。

image.png

レイアウトファイルのエラーを見てみると、

Cannot find a getter for <com.google.android.material.button.MaterialButton android:checked> that accepts parameter type 'java.lang.Boolean'

If a binding adapter provides the getter, check that the adapter is annotated correctly and that the parameter type matches.

ようするにbinding adapterが無いよって言われている。

MaterialButtonには setChecked(Bool) / isChecked: Bool の setter/getterは定義されてるんだけども、CompoundButton(CheckBoxやToggleButtonの基底クラス)を継承はしていない。
きっとcheckが変化したリスナーを自動では見つけられないのだろうと推測。

とりあえずbinding adapterを作る

CompoundButtonのbinding adapterをコピペすれば動くだろう、ということで
https://android.googlesource.com/platform/frameworks/data-binding/+/master/extensions/baseAdapters/src/main/java/android/databinding/adapters
ソースを読む。

CompoundButtonBindingAdapter.java
@BindingMethods({
        @BindingMethod(type = CompoundButton.class, attribute = "android:buttonTint", method = "setButtonTintList"),
        @BindingMethod(type = CompoundButton.class, attribute = "android:onCheckedChanged", method = "setOnCheckedChangeListener"),
})
@InverseBindingMethods({
        @InverseBindingMethod(type = CompoundButton.class, attribute = "android:checked"),
})
public class CompoundButtonBindingAdapter {
    @BindingAdapter("android:checked")
    public static void setChecked(CompoundButton view, boolean checked) {
        if (view.isChecked() != checked) {
            view.setChecked(checked);
        }
    }
    @BindingAdapter(value = {"android:onCheckedChanged", "android:checkedAttrChanged"},
            requireAll = false)
    public static void setListeners(CompoundButton view, final OnCheckedChangeListener listener,
            final InverseBindingListener attrChange) {
        if (attrChange == null) {
            view.setOnCheckedChangeListener(listener);
        } else {
            view.setOnCheckedChangeListener(new OnCheckedChangeListener() {
                @Override
                public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
                    if (listener != null) {
                        listener.onCheckedChanged(buttonView, isChecked);
                    }
                    attrChange.onChange();
                }
            });
        }
    }
}

なるほど。onCheckedChangedは今回使わないので、とりあえず適当にコピペすればいけるだろう。

MaterialButtonBindingAdapter.java
@InverseBindingMethods({
        @InverseBindingMethod(type = MaterialButton.class, attribute = "android:checked"),
})
public class MaterialButtonBindingAdapter {
    @BindingAdapter("android:checked")
    public static void setChecked(MaterialButton view, boolean checked) {
        if (view.isChecked() != checked) {
            view.setChecked(checked);
        }
    }

    @BindingAdapter(value = {"android:checkedAttrChanged"}, requireAll = false)
    public static void setListeners(MaterialButton view, final InverseBindingListener attrChange) {
        if (attrChange != null) {
            // TODO:
            //   丁寧に実装するには、TextViewBindingAdapterのようにListenerUtilというクラスを使って
            //   前回仕掛けたリスナーを明示的に解除する必要がある。
            //   参考: https://android.googlesource.com/platform/frameworks/data-binding/+/master/extensions/baseAdapters/src/main/java/android/databinding/adapters/TextViewBindingAdapter.java
            view.clearOnCheckedChangeListeners();

            view.addOnCheckedChangeListener(new MaterialButton.OnCheckedChangeListener() {
                @Override
                public void onCheckedChanged(MaterialButton buttonView, boolean isChecked) {
                    attrChange.onChange();
                }
            });
        }
    }
}

こんな感じでbinding adapterを作る。

これで、ビルドが通る。

めでたしめでたし。

まとめ

MaterialButtonToggleGroup を使うと、ちょっと今風なトグルが作れる。ただし

  • 場合によってはMaterialButtonに minWidth=0, layout_weight=1 指定が必要かもしれない
  • 2-wayデータバインディングを使うには、MaterialButtonのバインディングアダプタを自前で実装する必要がある

というハマりどころがあった。

 

お試しソースコードはまとめてここにおいてあります→ https://github.com/YusukeIwaki/MaterialButtonToggleGroupPlayground

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
6