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のソースをもらってきます。

http://source.android.com

そして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

Leave a Reply


Featuring Recent Posts WordPress Widget development by YD