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