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 
17 package android.widget;
18 
19 import com.android.internal.R;
20 
21 import android.annotation.IdRes;
22 import android.content.Context;
23 import android.content.res.TypedArray;
24 import android.util.AttributeSet;
25 import android.view.View;
26 import 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  */
53 public 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      */
RadioGroup(Context context)66     public RadioGroup(Context context) {
67         super(context);
68         setOrientation(VERTICAL);
69         init();
70     }
71 
72     /**
73      * {@inheritDoc}
74      */
RadioGroup(Context context, AttributeSet attrs)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 
init()95     private void init() {
96         mChildOnCheckedChangeListener = new CheckedStateTracker();
97         mPassThroughListener = new PassThroughHierarchyChangeListener();
98         super.setOnHierarchyChangeListener(mPassThroughListener);
99     }
100 
101     /**
102      * {@inheritDoc}
103      */
104     @Override
setOnHierarchyChangeListener(OnHierarchyChangeListener listener)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
onFinishInflate()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
addView(View child, int index, ViewGroup.LayoutParams params)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      */
check(@dRes int id)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 
setCheckedId(@dRes int id)170     private void setCheckedId(@IdRes int id) {
171         mCheckedId = id;
172         if (mOnCheckedChangeListener != null) {
173             mOnCheckedChangeListener.onCheckedChanged(this, mCheckedId);
174         }
175     }
176 
setCheckedStateForView(int viewId, boolean checked)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
getCheckedRadioButtonId()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      */
clearCheck()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      */
setOnCheckedChangeListener(OnCheckedChangeListener listener)218     public void setOnCheckedChangeListener(OnCheckedChangeListener listener) {
219         mOnCheckedChangeListener = listener;
220     }
221 
222     /**
223      * {@inheritDoc}
224      */
225     @Override
generateLayoutParams(AttributeSet attrs)226     public LayoutParams generateLayoutParams(AttributeSet attrs) {
227         return new RadioGroup.LayoutParams(getContext(), attrs);
228     }
229 
230     /**
231      * {@inheritDoc}
232      */
233     @Override
checkLayoutParams(ViewGroup.LayoutParams p)234     protected boolean checkLayoutParams(ViewGroup.LayoutParams p) {
235         return p instanceof RadioGroup.LayoutParams;
236     }
237 
238     @Override
generateDefaultLayoutParams()239     protected LinearLayout.LayoutParams generateDefaultLayoutParams() {
240         return new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
241     }
242 
243     @Override
getAccessibilityClassName()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          */
LayoutParams(Context c, AttributeSet attrs)262         public LayoutParams(Context c, AttributeSet attrs) {
263             super(c, attrs);
264         }
265 
266         /**
267          * {@inheritDoc}
268          */
LayoutParams(int w, int h)269         public LayoutParams(int w, int h) {
270             super(w, h);
271         }
272 
273         /**
274          * {@inheritDoc}
275          */
LayoutParams(int w, int h, float initWeight)276         public LayoutParams(int w, int h, float initWeight) {
277             super(w, h, initWeight);
278         }
279 
280         /**
281          * {@inheritDoc}
282          */
LayoutParams(ViewGroup.LayoutParams p)283         public LayoutParams(ViewGroup.LayoutParams p) {
284             super(p);
285         }
286 
287         /**
288          * {@inheritDoc}
289          */
LayoutParams(MarginLayoutParams source)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
setBaseAttributes(TypedArray a, int widthAttr, int heightAttr)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          */
onCheckedChanged(RadioGroup group, @IdRes int checkedId)334         public void onCheckedChanged(RadioGroup group, @IdRes int checkedId);
335     }
336 
337     private class CheckedStateTracker implements CompoundButton.OnCheckedChangeListener {
onCheckedChanged(CompoundButton buttonView, boolean isChecked)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          */
onChildViewAdded(View parent, View child)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          */
onChildViewRemoved(View parent, View child)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