RadioGroup.java revision a9108a217e039492855fbeacda2ab6c4f4a3f70a
1/*
2 * Copyright (C) 2006 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package android.widget;
18
19import com.android.internal.R;
20
21import android.content.Context;
22import android.content.res.TypedArray;
23import android.util.AttributeSet;
24import android.view.View;
25import android.view.ViewGroup;
26import android.view.accessibility.AccessibilityEvent;
27import android.view.accessibility.AccessibilityNodeInfo;
28
29
30/**
31 * <p>This class is used to create a multiple-exclusion scope for a set of radio
32 * buttons. Checking one radio button that belongs to a radio group unchecks
33 * any previously checked radio button within the same group.</p>
34 *
35 * <p>Intially, all of the radio buttons are unchecked. While it is not possible
36 * to uncheck a particular radio button, the radio group can be cleared to
37 * remove the checked state.</p>
38 *
39 * <p>The selection is identified by the unique id of the radio button as defined
40 * in the XML layout file.</p>
41 *
42 * <p><strong>XML Attributes</strong></p>
43 * <p>See {@link android.R.styleable#RadioGroup RadioGroup Attributes},
44 * {@link android.R.styleable#LinearLayout LinearLayout Attributes},
45 * {@link android.R.styleable#ViewGroup ViewGroup Attributes},
46 * {@link android.R.styleable#View View Attributes}</p>
47 * <p>Also see
48 * {@link android.widget.LinearLayout.LayoutParams LinearLayout.LayoutParams}
49 * for layout attributes.</p>
50 *
51 * @see RadioButton
52 *
53 */
54public class RadioGroup extends LinearLayout {
55    // holds the checked id; the selection is empty by default
56    private int mCheckedId = -1;
57    // tracks children radio buttons checked state
58    private CompoundButton.OnCheckedChangeListener mChildOnCheckedChangeListener;
59    // when true, mOnCheckedChangeListener discards events
60    private boolean mProtectFromCheckedChange = false;
61    private OnCheckedChangeListener mOnCheckedChangeListener;
62    private PassThroughHierarchyChangeListener mPassThroughListener;
63
64    /**
65     * {@inheritDoc}
66     */
67    public RadioGroup(Context context) {
68        super(context);
69        setOrientation(VERTICAL);
70        init();
71    }
72
73    /**
74     * {@inheritDoc}
75     */
76    public RadioGroup(Context context, AttributeSet attrs) {
77        super(context, attrs);
78
79        // retrieve selected radio button as requested by the user in the
80        // XML layout file
81        TypedArray attributes = context.obtainStyledAttributes(
82                attrs, com.android.internal.R.styleable.RadioGroup, com.android.internal.R.attr.radioButtonStyle, 0);
83
84        int value = attributes.getResourceId(R.styleable.RadioGroup_checkedButton, View.NO_ID);
85        if (value != View.NO_ID) {
86            mCheckedId = value;
87        }
88
89        final int index = attributes.getInt(com.android.internal.R.styleable.RadioGroup_orientation, VERTICAL);
90        setOrientation(index);
91
92        attributes.recycle();
93        init();
94    }
95
96    private void init() {
97        mChildOnCheckedChangeListener = new CheckedStateTracker();
98        mPassThroughListener = new PassThroughHierarchyChangeListener();
99        super.setOnHierarchyChangeListener(mPassThroughListener);
100    }
101
102    /**
103     * {@inheritDoc}
104     */
105    @Override
106    public void setOnHierarchyChangeListener(OnHierarchyChangeListener listener) {
107        // the user listener is delegated to our pass-through listener
108        mPassThroughListener.mOnHierarchyChangeListener = listener;
109    }
110
111    /**
112     * {@inheritDoc}
113     */
114    @Override
115    protected void onFinishInflate() {
116        super.onFinishInflate();
117
118        // checks the appropriate radio button as requested in the XML file
119        if (mCheckedId != -1) {
120            mProtectFromCheckedChange = true;
121            setCheckedStateForView(mCheckedId, true);
122            mProtectFromCheckedChange = false;
123            setCheckedId(mCheckedId);
124        }
125    }
126
127    @Override
128    public void addView(View child, int index, ViewGroup.LayoutParams params) {
129        if (child instanceof RadioButton) {
130            final RadioButton button = (RadioButton) child;
131            if (button.isChecked()) {
132                mProtectFromCheckedChange = true;
133                if (mCheckedId != -1) {
134                    setCheckedStateForView(mCheckedId, false);
135                }
136                mProtectFromCheckedChange = false;
137                setCheckedId(button.getId());
138            }
139        }
140
141        super.addView(child, index, params);
142    }
143
144    /**
145     * <p>Sets the selection to the radio button whose identifier is passed in
146     * parameter. Using -1 as the selection identifier clears the selection;
147     * such an operation is equivalent to invoking {@link #clearCheck()}.</p>
148     *
149     * @param id the unique id of the radio button to select in this group
150     *
151     * @see #getCheckedRadioButtonId()
152     * @see #clearCheck()
153     */
154    public void check(int id) {
155        // don't even bother
156        if (id != -1 && (id == mCheckedId)) {
157            return;
158        }
159
160        if (mCheckedId != -1) {
161            setCheckedStateForView(mCheckedId, false);
162        }
163
164        if (id != -1) {
165            setCheckedStateForView(id, true);
166        }
167
168        setCheckedId(id);
169    }
170
171    private void setCheckedId(int id) {
172        mCheckedId = id;
173        if (mOnCheckedChangeListener != null) {
174            mOnCheckedChangeListener.onCheckedChanged(this, mCheckedId);
175        }
176    }
177
178    private void setCheckedStateForView(int viewId, boolean checked) {
179        View checkedView = findViewById(viewId);
180        if (checkedView != null && checkedView instanceof RadioButton) {
181            ((RadioButton) checkedView).setChecked(checked);
182        }
183    }
184
185    /**
186     * <p>Returns the identifier of the selected radio button in this group.
187     * Upon empty selection, the returned value is -1.</p>
188     *
189     * @return the unique id of the selected radio button in this group
190     *
191     * @see #check(int)
192     * @see #clearCheck()
193     *
194     * @attr ref android.R.styleable#RadioGroup_checkedButton
195     */
196    public int getCheckedRadioButtonId() {
197        return mCheckedId;
198    }
199
200    /**
201     * <p>Clears the selection. When the selection is cleared, no radio button
202     * in this group is selected and {@link #getCheckedRadioButtonId()} returns
203     * null.</p>
204     *
205     * @see #check(int)
206     * @see #getCheckedRadioButtonId()
207     */
208    public void clearCheck() {
209        check(-1);
210    }
211
212    /**
213     * <p>Register a callback to be invoked when the checked radio button
214     * changes in this group.</p>
215     *
216     * @param listener the callback to call on checked state change
217     */
218    public void setOnCheckedChangeListener(OnCheckedChangeListener listener) {
219        mOnCheckedChangeListener = listener;
220    }
221
222    /**
223     * {@inheritDoc}
224     */
225    @Override
226    public LayoutParams generateLayoutParams(AttributeSet attrs) {
227        return new RadioGroup.LayoutParams(getContext(), attrs);
228    }
229
230    /**
231     * {@inheritDoc}
232     */
233    @Override
234    protected boolean checkLayoutParams(ViewGroup.LayoutParams p) {
235        return p instanceof RadioGroup.LayoutParams;
236    }
237
238    @Override
239    protected LinearLayout.LayoutParams generateDefaultLayoutParams() {
240        return new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
241    }
242
243    @Override
244    public void onInitializeAccessibilityEvent(AccessibilityEvent event) {
245        super.onInitializeAccessibilityEvent(event);
246        event.setClassName(RadioGroup.class.getName());
247    }
248
249    @Override
250    public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) {
251        super.onInitializeAccessibilityNodeInfo(info);
252        info.setClassName(RadioGroup.class.getName());
253    }
254
255    /**
256     * <p>This set of layout parameters defaults the width and the height of
257     * the children to {@link #WRAP_CONTENT} when they are not specified in the
258     * XML file. Otherwise, this class ussed the value read from the XML file.</p>
259     *
260     * <p>See
261     * {@link android.R.styleable#LinearLayout_Layout LinearLayout Attributes}
262     * for a list of all child view attributes that this class supports.</p>
263     *
264     */
265    public static class LayoutParams extends LinearLayout.LayoutParams {
266        /**
267         * {@inheritDoc}
268         */
269        public LayoutParams(Context c, AttributeSet attrs) {
270            super(c, attrs);
271        }
272
273        /**
274         * {@inheritDoc}
275         */
276        public LayoutParams(int w, int h) {
277            super(w, h);
278        }
279
280        /**
281         * {@inheritDoc}
282         */
283        public LayoutParams(int w, int h, float initWeight) {
284            super(w, h, initWeight);
285        }
286
287        /**
288         * {@inheritDoc}
289         */
290        public LayoutParams(ViewGroup.LayoutParams p) {
291            super(p);
292        }
293
294        /**
295         * {@inheritDoc}
296         */
297        public LayoutParams(MarginLayoutParams source) {
298            super(source);
299        }
300    }
301
302    /**
303     * <p>Interface definition for a callback to be invoked when the checked
304     * radio button changed in this group.</p>
305     */
306    public interface OnCheckedChangeListener {
307        /**
308         * <p>Called when the checked radio button has changed. When the
309         * selection is cleared, checkedId is -1.</p>
310         *
311         * @param group the group in which the checked radio button has changed
312         * @param checkedId the unique identifier of the newly checked radio button
313         */
314        public void onCheckedChanged(RadioGroup group, int checkedId);
315    }
316
317    private class CheckedStateTracker implements CompoundButton.OnCheckedChangeListener {
318        public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
319            // prevents from infinite recursion
320            if (mProtectFromCheckedChange) {
321                return;
322            }
323
324            mProtectFromCheckedChange = true;
325            if (mCheckedId != -1) {
326                setCheckedStateForView(mCheckedId, false);
327            }
328            mProtectFromCheckedChange = false;
329
330            int id = buttonView.getId();
331            setCheckedId(id);
332        }
333    }
334
335    /**
336     * <p>A pass-through listener acts upon the events and dispatches them
337     * to another listener. This allows the table layout to set its own internal
338     * hierarchy change listener without preventing the user to setup his.</p>
339     */
340    private class PassThroughHierarchyChangeListener implements
341            ViewGroup.OnHierarchyChangeListener {
342        private ViewGroup.OnHierarchyChangeListener mOnHierarchyChangeListener;
343
344        /**
345         * {@inheritDoc}
346         */
347        public void onChildViewAdded(View parent, View child) {
348            if (parent == RadioGroup.this && child instanceof RadioButton) {
349                int id = child.getId();
350                // generates an id if it's missing
351                if (id == View.NO_ID) {
352                    id = View.generateViewId();
353                    child.setId(id);
354                }
355                ((RadioButton) child).setOnCheckedChangeWidgetListener(
356                        mChildOnCheckedChangeListener);
357            }
358
359            if (mOnHierarchyChangeListener != null) {
360                mOnHierarchyChangeListener.onChildViewAdded(parent, child);
361            }
362        }
363
364        /**
365         * {@inheritDoc}
366         */
367        public void onChildViewRemoved(View parent, View child) {
368            if (parent == RadioGroup.this && child instanceof RadioButton) {
369                ((RadioButton) child).setOnCheckedChangeWidgetListener(null);
370            }
371
372            if (mOnHierarchyChangeListener != null) {
373                mOnHierarchyChangeListener.onChildViewRemoved(parent, child);
374            }
375        }
376    }
377}
378