• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 android.annotation.IdRes;
20 import android.content.Context;
21 import android.content.res.TypedArray;
22 import android.util.AttributeSet;
23 import android.util.Log;
24 import android.view.View;
25 import android.view.ViewGroup;
26 import android.view.ViewStructure;
27 import android.view.autofill.AutofillManager;
28 import android.view.autofill.AutofillValue;
29 
30 import com.android.internal.R;
31 
32 
33 /**
34  * <p>This class is used to create a multiple-exclusion scope for a set of radio
35  * buttons. Checking one radio button that belongs to a radio group unchecks
36  * any previously checked radio button within the same group.</p>
37  *
38  * <p>Intially, all of the radio buttons are unchecked. While it is not possible
39  * to uncheck a particular radio button, the radio group can be cleared to
40  * remove the checked state.</p>
41  *
42  * <p>The selection is identified by the unique id of the radio button as defined
43  * in the XML layout file.</p>
44  *
45  * <p><strong>XML Attributes</strong></p>
46  * <p>See {@link android.R.styleable#RadioGroup RadioGroup Attributes},
47  * {@link android.R.styleable#LinearLayout LinearLayout Attributes},
48  * {@link android.R.styleable#ViewGroup ViewGroup Attributes},
49  * {@link android.R.styleable#View View Attributes}</p>
50  * <p>Also see
51  * {@link android.widget.LinearLayout.LayoutParams LinearLayout.LayoutParams}
52  * for layout attributes.</p>
53  *
54  * @see RadioButton
55  *
56  */
57 public class RadioGroup extends LinearLayout {
58     private static final String LOG_TAG = RadioGroup.class.getSimpleName();
59 
60     // holds the checked id; the selection is empty by default
61     private int mCheckedId = -1;
62     // tracks children radio buttons checked state
63     private CompoundButton.OnCheckedChangeListener mChildOnCheckedChangeListener;
64     // when true, mOnCheckedChangeListener discards events
65     private boolean mProtectFromCheckedChange = false;
66     private OnCheckedChangeListener mOnCheckedChangeListener;
67     private PassThroughHierarchyChangeListener mPassThroughListener;
68 
69     // Indicates whether the child was set from resources or dynamically, so it can be used
70     // to sanitize autofill requests.
71     private int mInitialCheckedId = View.NO_ID;
72 
73     /**
74      * {@inheritDoc}
75      */
RadioGroup(Context context)76     public RadioGroup(Context context) {
77         super(context);
78         setOrientation(VERTICAL);
79         init();
80     }
81 
82     /**
83      * {@inheritDoc}
84      */
RadioGroup(Context context, AttributeSet attrs)85     public RadioGroup(Context context, AttributeSet attrs) {
86         super(context, attrs);
87 
88         // RadioGroup is important by default, unless app developer overrode attribute.
89         if (getImportantForAutofill() == IMPORTANT_FOR_AUTOFILL_AUTO) {
90             setImportantForAutofill(IMPORTANT_FOR_AUTOFILL_YES);
91         }
92 
93         // retrieve selected radio button as requested by the user in the
94         // XML layout file
95         TypedArray attributes = context.obtainStyledAttributes(
96                 attrs, com.android.internal.R.styleable.RadioGroup, com.android.internal.R.attr.radioButtonStyle, 0);
97 
98         int value = attributes.getResourceId(R.styleable.RadioGroup_checkedButton, View.NO_ID);
99         if (value != View.NO_ID) {
100             mCheckedId = value;
101             mInitialCheckedId = value;
102         }
103         final int index = attributes.getInt(com.android.internal.R.styleable.RadioGroup_orientation, VERTICAL);
104         setOrientation(index);
105 
106         attributes.recycle();
107         init();
108     }
109 
init()110     private void init() {
111         mChildOnCheckedChangeListener = new CheckedStateTracker();
112         mPassThroughListener = new PassThroughHierarchyChangeListener();
113         super.setOnHierarchyChangeListener(mPassThroughListener);
114     }
115 
116     /**
117      * {@inheritDoc}
118      */
119     @Override
setOnHierarchyChangeListener(OnHierarchyChangeListener listener)120     public void setOnHierarchyChangeListener(OnHierarchyChangeListener listener) {
121         // the user listener is delegated to our pass-through listener
122         mPassThroughListener.mOnHierarchyChangeListener = listener;
123     }
124 
125     /**
126      * {@inheritDoc}
127      */
128     @Override
onFinishInflate()129     protected void onFinishInflate() {
130         super.onFinishInflate();
131 
132         // checks the appropriate radio button as requested in the XML file
133         if (mCheckedId != -1) {
134             mProtectFromCheckedChange = true;
135             setCheckedStateForView(mCheckedId, true);
136             mProtectFromCheckedChange = false;
137             setCheckedId(mCheckedId);
138         }
139     }
140 
141     @Override
addView(View child, int index, ViewGroup.LayoutParams params)142     public void addView(View child, int index, ViewGroup.LayoutParams params) {
143         if (child instanceof RadioButton) {
144             final RadioButton button = (RadioButton) child;
145             if (button.isChecked()) {
146                 mProtectFromCheckedChange = true;
147                 if (mCheckedId != -1) {
148                     setCheckedStateForView(mCheckedId, false);
149                 }
150                 mProtectFromCheckedChange = false;
151                 setCheckedId(button.getId());
152             }
153         }
154 
155         super.addView(child, index, params);
156     }
157 
158     /**
159      * <p>Sets the selection to the radio button whose identifier is passed in
160      * parameter. Using -1 as the selection identifier clears the selection;
161      * such an operation is equivalent to invoking {@link #clearCheck()}.</p>
162      *
163      * @param id the unique id of the radio button to select in this group
164      *
165      * @see #getCheckedRadioButtonId()
166      * @see #clearCheck()
167      */
check(@dRes int id)168     public void check(@IdRes int id) {
169         // don't even bother
170         if (id != -1 && (id == mCheckedId)) {
171             return;
172         }
173 
174         if (mCheckedId != -1) {
175             setCheckedStateForView(mCheckedId, false);
176         }
177 
178         if (id != -1) {
179             setCheckedStateForView(id, true);
180         }
181 
182         setCheckedId(id);
183     }
184 
setCheckedId(@dRes int id)185     private void setCheckedId(@IdRes int id) {
186         mCheckedId = id;
187         if (mOnCheckedChangeListener != null) {
188             mOnCheckedChangeListener.onCheckedChanged(this, mCheckedId);
189         }
190         final AutofillManager afm = mContext.getSystemService(AutofillManager.class);
191         if (afm != null) {
192             afm.notifyValueChanged(this);
193         }
194     }
195 
setCheckedStateForView(int viewId, boolean checked)196     private void setCheckedStateForView(int viewId, boolean checked) {
197         View checkedView = findViewById(viewId);
198         if (checkedView != null && checkedView instanceof RadioButton) {
199             ((RadioButton) checkedView).setChecked(checked);
200         }
201     }
202 
203     /**
204      * <p>Returns the identifier of the selected radio button in this group.
205      * Upon empty selection, the returned value is -1.</p>
206      *
207      * @return the unique id of the selected radio button in this group
208      *
209      * @see #check(int)
210      * @see #clearCheck()
211      *
212      * @attr ref android.R.styleable#RadioGroup_checkedButton
213      */
214     @IdRes
getCheckedRadioButtonId()215     public int getCheckedRadioButtonId() {
216         return mCheckedId;
217     }
218 
219     /**
220      * <p>Clears the selection. When the selection is cleared, no radio button
221      * in this group is selected and {@link #getCheckedRadioButtonId()} returns
222      * null.</p>
223      *
224      * @see #check(int)
225      * @see #getCheckedRadioButtonId()
226      */
clearCheck()227     public void clearCheck() {
228         check(-1);
229     }
230 
231     /**
232      * <p>Register a callback to be invoked when the checked radio button
233      * changes in this group.</p>
234      *
235      * @param listener the callback to call on checked state change
236      */
setOnCheckedChangeListener(OnCheckedChangeListener listener)237     public void setOnCheckedChangeListener(OnCheckedChangeListener listener) {
238         mOnCheckedChangeListener = listener;
239     }
240 
241     /**
242      * {@inheritDoc}
243      */
244     @Override
generateLayoutParams(AttributeSet attrs)245     public LayoutParams generateLayoutParams(AttributeSet attrs) {
246         return new RadioGroup.LayoutParams(getContext(), attrs);
247     }
248 
249     /**
250      * {@inheritDoc}
251      */
252     @Override
checkLayoutParams(ViewGroup.LayoutParams p)253     protected boolean checkLayoutParams(ViewGroup.LayoutParams p) {
254         return p instanceof RadioGroup.LayoutParams;
255     }
256 
257     @Override
generateDefaultLayoutParams()258     protected LinearLayout.LayoutParams generateDefaultLayoutParams() {
259         return new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
260     }
261 
262     @Override
getAccessibilityClassName()263     public CharSequence getAccessibilityClassName() {
264         return RadioGroup.class.getName();
265     }
266 
267     /**
268      * <p>This set of layout parameters defaults the width and the height of
269      * the children to {@link #WRAP_CONTENT} when they are not specified in the
270      * XML file. Otherwise, this class ussed the value read from the XML file.</p>
271      *
272      * <p>See
273      * {@link android.R.styleable#LinearLayout_Layout LinearLayout Attributes}
274      * for a list of all child view attributes that this class supports.</p>
275      *
276      */
277     public static class LayoutParams extends LinearLayout.LayoutParams {
278         /**
279          * {@inheritDoc}
280          */
LayoutParams(Context c, AttributeSet attrs)281         public LayoutParams(Context c, AttributeSet attrs) {
282             super(c, attrs);
283         }
284 
285         /**
286          * {@inheritDoc}
287          */
LayoutParams(int w, int h)288         public LayoutParams(int w, int h) {
289             super(w, h);
290         }
291 
292         /**
293          * {@inheritDoc}
294          */
LayoutParams(int w, int h, float initWeight)295         public LayoutParams(int w, int h, float initWeight) {
296             super(w, h, initWeight);
297         }
298 
299         /**
300          * {@inheritDoc}
301          */
LayoutParams(ViewGroup.LayoutParams p)302         public LayoutParams(ViewGroup.LayoutParams p) {
303             super(p);
304         }
305 
306         /**
307          * {@inheritDoc}
308          */
LayoutParams(MarginLayoutParams source)309         public LayoutParams(MarginLayoutParams source) {
310             super(source);
311         }
312 
313         /**
314          * <p>Fixes the child's width to
315          * {@link android.view.ViewGroup.LayoutParams#WRAP_CONTENT} and the child's
316          * height to  {@link android.view.ViewGroup.LayoutParams#WRAP_CONTENT}
317          * when not specified in the XML file.</p>
318          *
319          * @param a the styled attributes set
320          * @param widthAttr the width attribute to fetch
321          * @param heightAttr the height attribute to fetch
322          */
323         @Override
setBaseAttributes(TypedArray a, int widthAttr, int heightAttr)324         protected void setBaseAttributes(TypedArray a,
325                 int widthAttr, int heightAttr) {
326 
327             if (a.hasValue(widthAttr)) {
328                 width = a.getLayoutDimension(widthAttr, "layout_width");
329             } else {
330                 width = WRAP_CONTENT;
331             }
332 
333             if (a.hasValue(heightAttr)) {
334                 height = a.getLayoutDimension(heightAttr, "layout_height");
335             } else {
336                 height = WRAP_CONTENT;
337             }
338         }
339     }
340 
341     /**
342      * <p>Interface definition for a callback to be invoked when the checked
343      * radio button changed in this group.</p>
344      */
345     public interface OnCheckedChangeListener {
346         /**
347          * <p>Called when the checked radio button has changed. When the
348          * selection is cleared, checkedId is -1.</p>
349          *
350          * @param group the group in which the checked radio button has changed
351          * @param checkedId the unique identifier of the newly checked radio button
352          */
onCheckedChanged(RadioGroup group, @IdRes int checkedId)353         public void onCheckedChanged(RadioGroup group, @IdRes int checkedId);
354     }
355 
356     private class CheckedStateTracker implements CompoundButton.OnCheckedChangeListener {
357         @Override
onCheckedChanged(CompoundButton buttonView, boolean isChecked)358         public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
359             // prevents from infinite recursion
360             if (mProtectFromCheckedChange) {
361                 return;
362             }
363 
364             mProtectFromCheckedChange = true;
365             if (mCheckedId != -1) {
366                 setCheckedStateForView(mCheckedId, false);
367             }
368             mProtectFromCheckedChange = false;
369 
370             int id = buttonView.getId();
371             setCheckedId(id);
372         }
373     }
374 
375     /**
376      * <p>A pass-through listener acts upon the events and dispatches them
377      * to another listener. This allows the table layout to set its own internal
378      * hierarchy change listener without preventing the user to setup his.</p>
379      */
380     private class PassThroughHierarchyChangeListener implements
381             ViewGroup.OnHierarchyChangeListener {
382         private ViewGroup.OnHierarchyChangeListener mOnHierarchyChangeListener;
383 
384         /**
385          * {@inheritDoc}
386          */
387         @Override
onChildViewAdded(View parent, View child)388         public void onChildViewAdded(View parent, View child) {
389             if (parent == RadioGroup.this && child instanceof RadioButton) {
390                 int id = child.getId();
391                 // generates an id if it's missing
392                 if (id == View.NO_ID) {
393                     id = View.generateViewId();
394                     child.setId(id);
395                 }
396                 ((RadioButton) child).setOnCheckedChangeWidgetListener(
397                         mChildOnCheckedChangeListener);
398             }
399 
400             if (mOnHierarchyChangeListener != null) {
401                 mOnHierarchyChangeListener.onChildViewAdded(parent, child);
402             }
403         }
404 
405         /**
406          * {@inheritDoc}
407          */
408         @Override
onChildViewRemoved(View parent, View child)409         public void onChildViewRemoved(View parent, View child) {
410             if (parent == RadioGroup.this && child instanceof RadioButton) {
411                 ((RadioButton) child).setOnCheckedChangeWidgetListener(null);
412             }
413 
414             if (mOnHierarchyChangeListener != null) {
415                 mOnHierarchyChangeListener.onChildViewRemoved(parent, child);
416             }
417         }
418     }
419 
420     @Override
onProvideAutofillStructure(ViewStructure structure, int flags)421     public void onProvideAutofillStructure(ViewStructure structure, int flags) {
422         super.onProvideAutofillStructure(structure, flags);
423         structure.setDataIsSensitive(mCheckedId != mInitialCheckedId);
424     }
425 
426     @Override
autofill(AutofillValue value)427     public void autofill(AutofillValue value) {
428         if (!isEnabled()) return;
429 
430         if (!value.isList()) {
431             Log.w(LOG_TAG, value + " could not be autofilled into " + this);
432             return;
433         }
434 
435         final int index = value.getListValue();
436         final View child = getChildAt(index);
437         if (child == null) {
438             Log.w(VIEW_LOG_TAG, "RadioGroup.autoFill(): no child with index " + index);
439             return;
440         }
441 
442         check(child.getId());
443     }
444 
445     @Override
getAutofillType()446     public @AutofillType int getAutofillType() {
447         return isEnabled() ? AUTOFILL_TYPE_LIST : AUTOFILL_TYPE_NONE;
448     }
449 
450     @Override
getAutofillValue()451     public AutofillValue getAutofillValue() {
452         if (!isEnabled()) return null;
453 
454         final int count = getChildCount();
455         for (int i = 0; i < count; i++) {
456             final View child = getChildAt(i);
457             if (child.getId() == mCheckedId) {
458                 return AutofillValue.forList(i);
459             }
460         }
461         return null;
462     }
463 }
464