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