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