複数行のRadioButtonが標準で用意されてなかったので、作ってみたところ、意外と詰まったのでまとめてみました。
イメージは下の通りです。少しレイアウトが弄ってありますが、詳しい説明は省かせていただきます。
忙しい方は3. RadioGroupをカスタマイズするをご覧ください。
1. TableLayoutをカスタマイズする
おすすめ度 ★★★★☆
TableLayoutをRadioGroup代わりに利用する方法です。調べると大体がこの方法に行き着くと思います。
参考
import android.content.Context;
import android.os.Parcel;
import android.os.Parcelable;
import android.support.annotation.IdRes;
import android.util.AttributeSet;
import android.view.View;
import android.widget.RadioButton;
import android.widget.TableLayout;
import android.widget.TableRow;
public class RadioGridGroup extends TableLayout implements View.OnClickListener {
private static final String TAG = "ToggleButtonGroupTableLayout";
private int checkedButtonID = -1;
/**
* @param context
*/
public RadioGridGroup(Context context) {
super(context);
// TODO Auto-generated constructor stub
}
/**
* @param context
* @param attrs
*/
public RadioGridGroup(Context context, AttributeSet attrs) {
super(context, attrs);
// TODO Auto-generated constructor stub
}
@Override
public void onClick(View v) {
if (v instanceof RadioButton) {
int id = v.getId();
check(id);
}
}
private void setCheckedStateForView(int viewId, boolean checked) {
View checkedView = findViewById(viewId);
if (checkedView != null && checkedView instanceof RadioButton) {
((RadioButton) checkedView).setChecked(checked);
}
}
/* (non-Javadoc)
* @see android.widget.TableLayout#addView(android.view.View, int, android.view.ViewGroup.LayoutParams)
*/
@Override
public void addView(View child, int index,
android.view.ViewGroup.LayoutParams params) {
super.addView(child, index, params);
setChildrenOnClickListener((TableRow) child);
}
/* (non-Javadoc)
* @see android.widget.TableLayout#addView(android.view.View, android.view.ViewGroup.LayoutParams)
*/
@Override
public void addView(View child, android.view.ViewGroup.LayoutParams params) {
super.addView(child, params);
setChildrenOnClickListener((TableRow) child);
}
private void setChildrenOnClickListener(TableRow tr) {
final int c = tr.getChildCount();
for (int i = 0; i < c; i++) {
final View v = tr.getChildAt(i);
if (v instanceof RadioButton) {
v.setOnClickListener(this);
}
}
}
/**
* @return the checked button Id
*/
public int getCheckedRadioButtonId() {
return checkedButtonID;
}
/**
* Check the id
*
* @param id
*/
public void check(@IdRes int id) {
// don't even bother
if (id != -1 && (id == checkedButtonID)) {
return;
}
if (checkedButtonID != -1) {
setCheckedStateForView(checkedButtonID, false);
}
if (id != -1) {
setCheckedStateForView(id, true);
}
setCheckedId(id);
}
/**
* set the checked button Id
*
* @param id
*/
private void setCheckedId(int id) {
this.checkedButtonID = id;
}
public void clearCheck() {
check(-1);
}
@Override
protected void onRestoreInstanceState(Parcelable state) {
if (!(state instanceof SavedState)) {
super.onRestoreInstanceState(state);
return;
}
SavedState ss = (SavedState) state;
super.onRestoreInstanceState(ss.getSuperState());
this.checkedButtonID = ss.buttonId;
setCheckedStateForView(checkedButtonID, true);
}
@Override
protected Parcelable onSaveInstanceState() {
Parcelable superState = super.onSaveInstanceState();
SavedState savedState = new SavedState(superState);
savedState.buttonId = checkedButtonID;
return savedState;
}
static class SavedState extends BaseSavedState {
int buttonId;
/**
* Constructor used when reading from a parcel. Reads the state of the superclass.
*
* @param source
*/
public SavedState(Parcel source) {
super(source);
buttonId = source.readInt();
}
/**
* Constructor called by derived classes when creating their SavedState objects
*
* @param superState The state of the superclass of this view
*/
public SavedState(Parcelable superState) {
super(superState);
}
@Override
public void writeToParcel(Parcel out, int flags) {
super.writeToParcel(out, flags);
out.writeInt(buttonId);
}
public static final Parcelable.Creator<SavedState> CREATOR =
new Parcelable.Creator<SavedState>() {
public SavedState createFromParcel(Parcel in) {
return new SavedState(in);
}
public SavedState[] newArray(int size) {
return new SavedState[size];
}
};
}
}
3×2のレイアウトです。
<com.test.customviews.RadioGridGroup
の部分は自分の環境に合ったものに変更しておいて下さい。
<com.test.customviews.RadioGridGroup
android:id="@+id/group"
android:layout_width="wrap_content"
android:layout_height="wrap_content">
<TableRow>
<RadioButton
android:id="@+id/rad1"
android:checked="true"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="text1" />
<RadioButton
android:id="@+id/rad2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="text2" />
</TableRow>
<TableRow>
<RadioButton
android:id="@+id/rad3"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="text3" />
<RadioButton
android:id="@+id/rad4"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="text4" />
</TableRow>
<TableRow>
<RadioButton
android:id="@+id/rad5"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="text5" />
<RadioButton
android:id="@+id/rad6"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="text6" />
</TableRow>
</com.test.customviews.RadioGridGroup>
XMLでandroid:checked="true"
を指定すると、RadioButtonのチェックが外れなくなりバグるためコード上で指定する必要があります。
val group = findViewById<GridRadioGroup>(R.id.radioSubject)
group.check(R.id.group)
メリット
- TableLayoutなので比較的自由にレイアウトをカスタマイズすることができる
デメリット
- RadioButtonのチェック状態が変更された時に呼ばれるリスナー
setOnCheckedChangeListener
が無い
→ 独自リスナーを実装することで解決
public class RadioGridGroup extends TableLayout implements View.OnClickListener {
private static final String TAG = "ToggleButtonGroupTableLayout";
private int checkedButtonID = -1;
private OnCheckedChangeListener mOnCheckedChangeListener;
// ・・・ 略
private void setCheckedId(int id) {
if (mOnCheckedChangeListener != null) {
mOnCheckedChangeListener.onCheckedChanged(this, id);
}
this.checkedButtonID = id;
}
// ・・・ 略
public void setOnCheckedChangeListener(OnCheckedChangeListener listener) {
mOnCheckedChangeListener = listener;
}
public interface OnCheckedChangeListener {
void onCheckedChanged(GridRadioGroup gridRadioGroup, int checkedId);
}
}
group.setOnCheckedChangeListener { _, checkedId ->
Log.d("GridRadioGroup", "${checkedId}が選択されました")
}
- RadioButtonに
setOnClickListener
を呼んだときにバグる
→ 当てはまる人はごく少数だと思われますが、私の場合このバグが致命的だったため別の方法を探すことにしました
基本的なデメリットはRadioGroupが継承されてないことのみだと思います。
2. RadioGroupを3つ並べる
おすすめ度 ★★☆☆☆
RadioGroupを3つ並べてコードで操作する方法です。クラスの拡張等が必要ありません。
参考
<RadioGroup
android:layout_width="wrap_content"
android:layout_height="wrap_content">
<RadioGroup
android:id="@+id/group1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal">
<RadioButton
android:id="@+id/rad1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="text1" />
<RadioButton
android:id="@+id/rad2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="text2" />
</RadioGroup>
<RadioGroup
android:id="@+id/group2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal">
<RadioButton
android:id="@+id/rad3"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="text3" />
<RadioButton
android:id="@+id/rad4"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="text4" />
</RadioGroup>
<RadioGroup
android:id="@+id/group3"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal">
<RadioButton
android:id="@+id/rad5"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="text5" />
<RadioButton
android:id="@+id/rad6"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="text6" />
</RadioGroup>
</RadioGroup>
コード側です。仕組みは簡単で、一行目のRadioGroupが選択されたら二行目・三行目のチェックを解除。二行目のRadioGroupが選択されたら一行目・三行目のチェックを解除、といった仕組みです。
val group1 = root.findViewById<RadioGroup>(R.id.group1)
val group2 = root.findViewById<RadioGroup>(R.id.group2)
val group3 = root.findViewById<RadioGroup>(R.id.group3)
listener = RadioGroup.OnCheckedChangeListener { group, checkedId ->
if (checkedId != -1) when (group.id) {
R.id.group1 -> {
group2.clearCheck()
group3.clearCheck()
}
R.id.group2 -> {
group1.clearCheck()
group3.clearCheck()
}
R.id.group3 -> {
group1.clearCheck()
group2.clearCheck()
}
}
}
group1.setOnCheckedChangeListener(listener)
group2.setOnCheckedChangeListener(listener)
group3.setOnCheckedChangeListener(listener)
一見動きそうに見えますが、実はこの状態では動きません。clearCheck()
が呼ばれた時にもOnCheckedChangeListener
が呼ばれるからです。
そのため、対処方法としてはclearCheck()
を呼ぶ度にリスナーの登録を解除する必要があります。
class MainActivity : AppCompatActivity() {
private lateinit var listener: RadioGroup.OnCheckedChangeListener
override fun onCreate(savedInstanceState: Bundle?) {
// ・・・ 略
val group1 = findViewById<RadioGroup>(R.id.group1)
val group2 = findViewById<RadioGroup>(R.id.group2)
val group3 = findViewById<RadioGroup>(R.id.group3)
listener = RadioGroup.OnCheckedChangeListener { group, checkedId ->
if (checkedId != -1) when (group.id) {
R.id.group1 -> {
checkAnswer(group2)
checkAnswer(group3)
}
R.id.group2 -> {
checkAnswer(group1)
checkAnswer(group3)
}
R.id.group3 -> {
checkAnswer(group1)
checkAnswer(group2)
}
}
}
group1.setOnCheckedChangeListener(listener)
group2.setOnCheckedChangeListener(listener)
group3.setOnCheckedChangeListener(listener)
// ・・・ 略
}
private fun checkAnswer(radioGroup: RadioGroup) {
radioGroup.setOnCheckedChangeListener(null)
radioGroup.clearCheck()
radioGroup.setOnCheckedChangeListener(listener)
}
}
メリット
- RadioGroupのメソッドが使える
デメリット
- コードが多い & 見た目が良くない
- listenerをグローバル変数に置く必要がある
RadioGroupが使えるため独自実装等が必要ありませんが、保守性があまり良くないです。
3. RadioGroupをカスタマイズする
おすすめ度 ★★★★★
見つけるのに結構時間がかかりました。仕組みは 2. と同じです。
RadioGroup代わりに利用できるようカスタマイズされていたので有り難く使わせていただきます。
import android.content.Context;
import android.os.Build.VERSION;
import android.util.AttributeSet;
import android.view.View;
import android.view.ViewGroup;
import android.widget.CompoundButton;
import android.widget.RadioButton;
import android.widget.RadioGroup;
import java.util.ArrayList;
/**
* 可以多行布局的RadioGroup,但是会用掉子RadioButton的OnCheckedChangeListener
* A RadioGroup allow multiple rows layout, will use the RadioButton's OnCheckedChangeListener
*/
public class MultiRowsRadioGroup extends RadioGroup {
public MultiRowsRadioGroup(Context context, AttributeSet attrs) {
super(context, attrs);
init();
}
public MultiRowsRadioGroup(Context context) {
super(context);
init();
}
private void init() {
setOnHierarchyChangeListener(new OnHierarchyChangeListener() {
public void onChildViewRemoved(View parent, View child) {
if (parent == MultiRowsRadioGroup.this && child instanceof ViewGroup) {
for (RadioButton radioButton : getRadioButtonFromGroup((ViewGroup) child)) {
radioButton.setOnCheckedChangeListener(null);
}
}
}
@Override
public void onChildViewAdded(View parent, View child) {
if (parent == MultiRowsRadioGroup.this && child instanceof ViewGroup) {
for (final RadioButton radioButton : getRadioButtonFromGroup((ViewGroup) child)) {
int id = radioButton.getId();
// generates an id if it's missing
if (id == View.NO_ID) {
if (VERSION.SDK_INT >= 17) id = View.generateViewId();
else id = radioButton.hashCode();
radioButton.setId(id);
}
if (radioButton.isChecked()) {
check(id);
}
radioButton.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
if (isChecked) {
radioButton.setOnCheckedChangeListener(null);
check(buttonView.getId());
radioButton.setOnCheckedChangeListener(this);
}
}
});
}
}
}
});
}
private boolean checking = false;
@Override
public void check(int id) {
if (checking) return;
checking = true;
super.check(id);
checking = false;
}
private ArrayList<RadioButton> getRadioButtonFromGroup(ViewGroup group) {
if (group == null) return new ArrayList<>();
ArrayList<RadioButton> list = new ArrayList<>();
getRadioButtonFromGroup(group, list);
return list;
}
private void getRadioButtonFromGroup(ViewGroup group, ArrayList<RadioButton> list) {
for (int i = 0, count = group.getChildCount(); i < count; i++) {
View child = group.getChildAt(i);
if (child instanceof RadioButton) {
list.add((RadioButton) child);
} else if (child instanceof ViewGroup) {
getRadioButtonFromGroup((ViewGroup) child, list);
}
}
}
}
<com.test.customviews.RadioGridGroup
の部分は自分の環境に合ったものに変更しておいて下さい。
<com.test.customviews.MultiRowsRadioGroup
android:layout_width="wrap_content"
android:layout_height="wrap_content">
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal">
<RadioButton
android:id="@+id/rad1"
android:checked="true"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="text1" />
<RadioButton
android:id="@+id/rad2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="text2" />
</LinearLayout>
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal">
<RadioButton
android:id="@+id/rad3"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="text3" />
<RadioButton
android:id="@+id/rad4"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="text4" />
</LinearLayout>
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal">
<RadioButton
android:id="@+id/rad5"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="text5" />
<RadioButton
android:id="@+id/rad6"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="text6" />
</LinearLayout>
</com.test.customviews.MultiRowsRadioGroup>
メリット
- RadioGroupのメソッドが使える
- XMLだけで完結している
デメリット
- 多分無し
おわりに
自由にレイアウトをカスタマイズしたい方以外は 3. がおすすめです。