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