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         * <p>Fixes the child's width to
303         * {@link android.view.ViewGroup.LayoutParams#WRAP_CONTENT} and the child's
304         * height to  {@link android.view.ViewGroup.LayoutParams#WRAP_CONTENT}
305         * when not specified in the XML file.</p>
306         *
307         * @param a the styled attributes set
308         * @param widthAttr the width attribute to fetch
309         * @param heightAttr the height attribute to fetch
310         */
311        @Override
312        protected void setBaseAttributes(TypedArray a,
313                int widthAttr, int heightAttr) {
314
315            if (a.hasValue(widthAttr)) {
316                width = a.getLayoutDimension(widthAttr, "layout_width");
317            } else {
318                width = WRAP_CONTENT;
319            }
320
321            if (a.hasValue(heightAttr)) {
322                height = a.getLayoutDimension(heightAttr, "layout_height");
323            } else {
324                height = WRAP_CONTENT;
325            }
326        }
327    }
328
329    /**
330     * <p>Interface definition for a callback to be invoked when the checked
331     * radio button changed in this group.</p>
332     */
333    public interface OnCheckedChangeListener {
334        /**
335         * <p>Called when the checked radio button has changed. When the
336         * selection is cleared, checkedId is -1.</p>
337         *
338         * @param group the group in which the checked radio button has changed
339         * @param checkedId the unique identifier of the newly checked radio button
340         */
341        public void onCheckedChanged(RadioGroup group, int checkedId);
342    }
343
344    private class CheckedStateTracker implements CompoundButton.OnCheckedChangeListener {
345        public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
346            // prevents from infinite recursion
347            if (mProtectFromCheckedChange) {
348                return;
349            }
350
351            mProtectFromCheckedChange = true;
352            if (mCheckedId != -1) {
353                setCheckedStateForView(mCheckedId, false);
354            }
355            mProtectFromCheckedChange = false;
356
357            int id = buttonView.getId();
358            setCheckedId(id);
359        }
360    }
361
362    /**
363     * <p>A pass-through listener acts upon the events and dispatches them
364     * to another listener. This allows the table layout to set its own internal
365     * hierarchy change listener without preventing the user to setup his.</p>
366     */
367    private class PassThroughHierarchyChangeListener implements
368            ViewGroup.OnHierarchyChangeListener {
369        private ViewGroup.OnHierarchyChangeListener mOnHierarchyChangeListener;
370
371        /**
372         * {@inheritDoc}
373         */
374        public void onChildViewAdded(View parent, View child) {
375            if (parent == RadioGroup.this && child instanceof RadioButton) {
376                int id = child.getId();
377                // generates an id if it's missing
378                if (id == View.NO_ID) {
379                    id = View.generateViewId();
380                    child.setId(id);
381                }
382                ((RadioButton) child).setOnCheckedChangeWidgetListener(
383                        mChildOnCheckedChangeListener);
384            }
385
386            if (mOnHierarchyChangeListener != null) {
387                mOnHierarchyChangeListener.onChildViewAdded(parent, child);
388            }
389        }
390
391        /**
392         * {@inheritDoc}
393         */
394        public void onChildViewRemoved(View parent, View child) {
395            if (parent == RadioGroup.this && child instanceof RadioButton) {
396                ((RadioButton) child).setOnCheckedChangeWidgetListener(null);
397            }
398
399            if (mOnHierarchyChangeListener != null) {
400                mOnHierarchyChangeListener.onChildViewRemoved(parent, child);
401            }
402        }
403    }
404}
405