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