AndroidのRadioGroupを改造し、子孫要素を全部グループ化できるようにした
AndroidのRadioGroupは、子要素のRadioButtonだけをグループ化し、排他的にチェックできるようにします。孫要素・ひ孫要素がRadioButtonでも、それは無視されます。
たとえば、以下のような階層構造を作った場合、青のRadioButtonだけがグループの要素として扱われます。
(RadioGroupを使ったビューの例)
孫要素となっているRadioButton(白いやつ)を選択しても、他のRadioButtonのチェックが外れたりしません。これだと「ちょっと凝った作りのレイアウトを作って選択してもらいたい」というときに、めちゃくちゃ不便です。不便すぎてだんだん腹が立ってきたのでちょっとハック。
DeepRadioGroupというのを作りました。githubに置いてあるので適当に使ってください。
DeepRadioGroup.java (github.com)
このDeepRadioGroupってのは、はRadioGroupのちょっとした改造品で、子孫要素にRadioButtonがあれば、それらすべてをまとめてグループにしちゃうわけです。たとえば、次のような階層構造を作った場合、青のRadioButtonがみんなグループの要素として扱われます。
(DeepRadioGroupを使ったビューの例)
- LinearLayoutがベースになってますので、LinearLayoutと同様にレイアウトできます
- RadioGroupで使えるメソッド(check(), getCheckedRadioButtonId() などなど)はちゃんと動きます
- 動的に要素を追加した場合もちゃんと機能します
- android:orientation属性をつけてください
いつものRadioGroupと同じ気分で使ってください。動的に要素を追加した場合も、そこに入っているラジオボタンがちゃんとグループに加わるので安心。あと、使うときはandroid:orientation属性をつけてね。
(利用例1)
<org.maripo.android.widget.DeepRadioGroup android:id="@+id/radio_group" android:orientation="vertical"
android:layout_width="fill_parent" android:layout_height="wrap_content">
<RelativeLayout android:layout_height="fill_parent"
android:layout_width="fill_parent">
<RadioButton android:id="@+id/radio_button1"
android:layout_height="wrap_content" android:text="選択肢1"
android:layout_width="wrap_content"></RadioButton>
<Button android:id="@+id/button1"
android:layout_alignParentRight="true" android:gravity="center"
android:layout_width="wrap_content"
android:layout_height="wrap_content" android:text="詳細"/>
</RelativeLayout>
<RelativeLayout android:layout_height="fill_parent"
android:layout_width="fill_parent">
<RadioButton android:id="@+id/radio_button2"
android:layout_height="wrap_content" android:text="選択肢2"
android:layout_width="wrap_content"></RadioButton>
<Button android:id="@+id/button2"
android:layout_alignParentRight="true" android:gravity="center"
android:layout_width="wrap_content"
android:layout_height="wrap_content" android:text="詳細"/>
</RelativeLayout>
<RelativeLayout android:layout_height="fill_parent"
android:layout_width="fill_parent">
<RadioButton android:id="@+id/radio_button3"
android:layout_height="wrap_content" android:text="選択肢3"
android:layout_width="wrap_content"></RadioButton>
<Button android:id="@+id/button3"
android:layout_alignParentRight="true" android:gravity="center"
android:layout_width="wrap_content"
android:layout_height="wrap_content" android:text="詳細"/>
</RelativeLayout>
</org.maripo.android.widget.DeepRadioGroup>
(利用例2)
<org.maripo.android.widget.DeepRadioGroup
android:id="@+id/radio_group" android:orientation="vertical"
android:layout_width="fill_parent" android:layout_height="wrap_content">
<TableLayout android:layout_height="fill_parent" android:background="#ddd"
android:layout_width="fill_parent">
<TableRow android:layout_width="fill_parent"
android:layout_height="wrap_content">
<RadioButton android:id="@+id/radio_button1"
android:layout_height="wrap_content" android:text="選択肢1"
android:layout_width="wrap_content"></RadioButton>
<RadioButton android:id="@+id/radio_button2"
android:layout_height="wrap_content" android:text="選択肢2"
android:layout_width="wrap_content"></RadioButton>
</TableRow>
<TableRow android:layout_width="fill_parent"
android:layout_height="wrap_content">
<RadioButton android:id="@+id/radio_button3"
android:layout_height="wrap_content" android:text="選択肢3"
android:layout_width="wrap_content"></RadioButton>
<RadioButton android:id="@+id/radio_button4"
android:layout_height="wrap_content" android:text="選択肢4"
android:layout_width="wrap_content"></RadioButton>
</TableRow>
</TableLayout>
<RadioButton android:id="@+id/radio_button5"
android:layout_height="wrap_content" android:text="どうでもいいです"
android:layout_width="wrap_content"></RadioButton>
</org.maripo.android.widget.DeepRadioGroup>
《解説》
以下、実際どうなっているのかを少し解説します。まずはAndroid SDKのソースをもらってきます。
そしてRadioGroup.javaを開いて読んでみます。使いたいメンバがことごとくprivateだったので継承して作るのはちと面倒。コピペして改造しました。
RadioGroupは、「子要素が加わったとき、それがRadioButtonだったらゴニョゴニョする」ということをやってます。これを改造して「子要素が加わった場合、その要素自身もしくは子孫要素がRadioButtonだったらゴニョゴニョする」という挙動にすればオッケーというわけです。
まずは再帰的に木構造を掘っていってRadioGuttonを見つけ次第コールバックするというクラスをてきとうに作りました。
private class TreeScanner {
OnRadioButtonFoundListener listener;
public TreeScanner(OnRadioButtonFoundListener listener) {
this.listener = listener;
}
// Scan Recursively
public void scan(View child) {
if (child instanceof RadioButton) {
listener.onRadioButtonFound((RadioButton)child);
}
else if (child instanceof ViewGroup)
{
ViewGroup viewGroup = (ViewGroup) child;
for (int i=0, l=viewGroup.getChildCount(); i<l; i++) {
scan(viewGroup.getChildAt(i));
}
}
}
}
private interface OnRadioButtonFoundListener {
public void onRadioButtonFound (RadioButton radioButton);
}
これを使って、「子要素が加わったとき、それがRadioButtonだったらゴニョゴニョ」している所を置き換えてやりましょう。
まずは addViewメソッド。
(Before)
@Override
public void addView(View child, int index, ViewGroup.LayoutParams params) {
if (child instanceof RadioButton) {
final RadioButton button = (RadioButton) child;
if (button.isChecked()) {
mProtectFromCheckedChange = true;
if (mCheckedId != -1) {
setCheckedStateForView(mCheckedId, false);
}
mProtectFromCheckedChange = false;
setCheckedId(button.getId());
}
}
super.addView(child, index, params);
}
(After)
@Override
public void addView(View child, int index, ViewGroup.LayoutParams params) {
new TreeScanner(new OnRadioButtonFoundListener() {
@Override
public void onRadioButtonFound(RadioButton radioButton) {
if (radioButton.isChecked()) {
mProtectFromCheckedChange = true;
if (mCheckedId != -1) {
setCheckedStateForView(mCheckedId, false);
}
mProtectFromCheckedChange = false;
setCheckedId(radioButton.getId());
}
}
}).scan(child);
super.addView(child, index, params);
}
次は、内部クラスPassThroughHierarchyChangeListenerを改造。
(Before)
private class PassThroughHierarchyChangeListener implements
ViewGroup.OnHierarchyChangeListener {
private ViewGroup.OnHierarchyChangeListener mOnHierarchyChangeListener;
/**
* {@inheritDoc}
*/
public void onChildViewAdded(View parent, View child) {
if (parent == RadioGroup.this && child instanceof RadioButton) {
int id = child.getId();
// generates an id if it's missing
if (id == View.NO_ID) {
id = child.hashCode();
child.setId(id);
}
((RadioButton) child).setOnCheckedChangeWidgetListener(
mChildOnCheckedChangeListener);
}
if (mOnHierarchyChangeListener != null) {
mOnHierarchyChangeListener.onChildViewAdded(parent, child);
}
}
/**
* {@inheritDoc}
*/
public void onChildViewRemoved(View parent, View child) {
if (parent == RadioGroup.this && child instanceof RadioButton) {
((RadioButton) child).setOnCheckedChangeWidgetListener(null);
}
if (mOnHierarchyChangeListener != null) {
mOnHierarchyChangeListener.onChildViewRemoved(parent, child);
}
}
}
(After)
private class PassThroughHierarchyChangeListener implements
ViewGroup.OnHierarchyChangeListener {
private ViewGroup.OnHierarchyChangeListener mOnHierarchyChangeListener;
public void onChildViewAdded(final View parent, View child) {
if (parent == DeepRadioGroup.this) {
new TreeScanner(new OnRadioButtonFoundListener() {
@Override
public void onRadioButtonFound(RadioButton radioButton) {
int id = radioButton.getId();
if (id == View.NO_ID) {
id = radioButton.hashCode();
radioButton.setId(id);
}
radioButton.setOnCheckedChangeListener(mChildOnCheckedChangeListener);
}
}).scan(child);
}
if (mOnHierarchyChangeListener != null) {
mOnHierarchyChangeListener.onChildViewAdded(parent, child);
}
}
public void onChildViewRemoved(View parent, View child) {
if (parent == DeepRadioGroup.this) {
new TreeScanner (new OnRadioButtonFoundListener() {
@Override
public void onRadioButtonFound(RadioButton radioButton) {
radioButton.setOnCheckedChangeListener(null);
}
}).scan(child);
}
}
}
これでできあがり。現物はgithubからどうぞ。
src/maripo/android/widget/DeepRadioGroup.java at master from maripo/MaripoWidgets – GitHub