blob: 71ccb595278b511a471f8b10fbe871e35ec680de [file] [log] [blame]
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001/*
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
Tor Norbye7b9c9122013-05-30 16:48:33 -070019import android.annotation.IdRes;
Felipe Leme92736c12018-11-13 12:00:59 -080020import android.annotation.NonNull;
sallyyuen69a146c2019-11-25 17:05:47 -080021import android.annotation.Nullable;
Artur Satayeved5a6ae2019-12-10 17:47:54 +000022import android.compat.annotation.UnsupportedAppUsage;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080023import android.content.Context;
24import android.content.res.TypedArray;
25import android.util.AttributeSet;
Felipe Leme6d553872016-12-08 17:13:25 -080026import android.util.Log;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080027import android.view.View;
28import android.view.ViewGroup;
Felipe Lemec01a8732017-02-22 17:26:06 -080029import android.view.ViewStructure;
sallyyuen69a146c2019-11-25 17:05:47 -080030import android.view.accessibility.AccessibilityNodeInfo;
Felipe Leme640f30a2017-03-06 15:44:06 -080031import android.view.autofill.AutofillManager;
32import android.view.autofill.AutofillValue;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080033
Aurimas Liutikas99441c52016-10-11 16:48:32 -070034import com.android.internal.R;
35
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080036
37/**
38 * <p>This class is used to create a multiple-exclusion scope for a set of radio
39 * buttons. Checking one radio button that belongs to a radio group unchecks
40 * any previously checked radio button within the same group.</p>
41 *
42 * <p>Intially, all of the radio buttons are unchecked. While it is not possible
43 * to uncheck a particular radio button, the radio group can be cleared to
44 * remove the checked state.</p>
45 *
46 * <p>The selection is identified by the unique id of the radio button as defined
47 * in the XML layout file.</p>
48 *
49 * <p><strong>XML Attributes</strong></p>
Aurimas Liutikas99441c52016-10-11 16:48:32 -070050 * <p>See {@link android.R.styleable#RadioGroup RadioGroup Attributes},
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080051 * {@link android.R.styleable#LinearLayout LinearLayout Attributes},
52 * {@link android.R.styleable#ViewGroup ViewGroup Attributes},
53 * {@link android.R.styleable#View View Attributes}</p>
54 * <p>Also see
55 * {@link android.widget.LinearLayout.LayoutParams LinearLayout.LayoutParams}
56 * for layout attributes.</p>
Aurimas Liutikas99441c52016-10-11 16:48:32 -070057 *
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080058 * @see RadioButton
59 *
60 */
61public class RadioGroup extends LinearLayout {
Philip P. Moltmann96689032017-03-09 13:19:55 -080062 private static final String LOG_TAG = RadioGroup.class.getSimpleName();
Felipe Leme6d553872016-12-08 17:13:25 -080063
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080064 // holds the checked id; the selection is empty by default
65 private int mCheckedId = -1;
66 // tracks children radio buttons checked state
Mathew Inwood978c6e22018-08-21 15:58:55 +010067 @UnsupportedAppUsage
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080068 private CompoundButton.OnCheckedChangeListener mChildOnCheckedChangeListener;
69 // when true, mOnCheckedChangeListener discards events
70 private boolean mProtectFromCheckedChange = false;
Mathew Inwood978c6e22018-08-21 15:58:55 +010071 @UnsupportedAppUsage
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080072 private OnCheckedChangeListener mOnCheckedChangeListener;
73 private PassThroughHierarchyChangeListener mPassThroughListener;
74
Felipe Lemec01a8732017-02-22 17:26:06 -080075 // Indicates whether the child was set from resources or dynamically, so it can be used
Felipe Leme640f30a2017-03-06 15:44:06 -080076 // to sanitize autofill requests.
Felipe Lemec01a8732017-02-22 17:26:06 -080077 private int mInitialCheckedId = View.NO_ID;
78
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080079 /**
80 * {@inheritDoc}
81 */
82 public RadioGroup(Context context) {
83 super(context);
84 setOrientation(VERTICAL);
85 init();
86 }
87
88 /**
89 * {@inheritDoc}
90 */
91 public RadioGroup(Context context, AttributeSet attrs) {
92 super(context, attrs);
93
Felipe Lemed04a6972017-03-02 12:56:18 -080094 // RadioGroup is important by default, unless app developer overrode attribute.
95 if (getImportantForAutofill() == IMPORTANT_FOR_AUTOFILL_AUTO) {
96 setImportantForAutofill(IMPORTANT_FOR_AUTOFILL_YES);
97 }
sallyyuen69a146c2019-11-25 17:05:47 -080098 setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_YES);
Felipe Lemed04a6972017-03-02 12:56:18 -080099
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800100 // retrieve selected radio button as requested by the user in the
101 // XML layout file
102 TypedArray attributes = context.obtainStyledAttributes(
103 attrs, com.android.internal.R.styleable.RadioGroup, com.android.internal.R.attr.radioButtonStyle, 0);
Aurimas Liutikasab324cf2019-02-07 16:46:38 -0800104 saveAttributeDataForStyleable(context, com.android.internal.R.styleable.RadioGroup,
105 attrs, attributes, com.android.internal.R.attr.radioButtonStyle, 0);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800106
107 int value = attributes.getResourceId(R.styleable.RadioGroup_checkedButton, View.NO_ID);
108 if (value != View.NO_ID) {
109 mCheckedId = value;
Felipe Lemec01a8732017-02-22 17:26:06 -0800110 mInitialCheckedId = value;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800111 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800112 final int index = attributes.getInt(com.android.internal.R.styleable.RadioGroup_orientation, VERTICAL);
113 setOrientation(index);
114
115 attributes.recycle();
116 init();
117 }
118
119 private void init() {
120 mChildOnCheckedChangeListener = new CheckedStateTracker();
121 mPassThroughListener = new PassThroughHierarchyChangeListener();
122 super.setOnHierarchyChangeListener(mPassThroughListener);
123 }
124
125 /**
126 * {@inheritDoc}
127 */
128 @Override
129 public void setOnHierarchyChangeListener(OnHierarchyChangeListener listener) {
130 // the user listener is delegated to our pass-through listener
131 mPassThroughListener.mOnHierarchyChangeListener = listener;
132 }
133
134 /**
135 * {@inheritDoc}
136 */
137 @Override
138 protected void onFinishInflate() {
139 super.onFinishInflate();
140
141 // checks the appropriate radio button as requested in the XML file
142 if (mCheckedId != -1) {
143 mProtectFromCheckedChange = true;
144 setCheckedStateForView(mCheckedId, true);
145 mProtectFromCheckedChange = false;
146 setCheckedId(mCheckedId);
147 }
148 }
149
150 @Override
151 public void addView(View child, int index, ViewGroup.LayoutParams params) {
152 if (child instanceof RadioButton) {
153 final RadioButton button = (RadioButton) child;
154 if (button.isChecked()) {
155 mProtectFromCheckedChange = true;
156 if (mCheckedId != -1) {
157 setCheckedStateForView(mCheckedId, false);
158 }
159 mProtectFromCheckedChange = false;
160 setCheckedId(button.getId());
161 }
162 }
163
164 super.addView(child, index, params);
165 }
166
167 /**
168 * <p>Sets the selection to the radio button whose identifier is passed in
169 * parameter. Using -1 as the selection identifier clears the selection;
170 * such an operation is equivalent to invoking {@link #clearCheck()}.</p>
171 *
172 * @param id the unique id of the radio button to select in this group
173 *
174 * @see #getCheckedRadioButtonId()
175 * @see #clearCheck()
176 */
Tor Norbye7b9c9122013-05-30 16:48:33 -0700177 public void check(@IdRes int id) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800178 // don't even bother
179 if (id != -1 && (id == mCheckedId)) {
180 return;
181 }
182
183 if (mCheckedId != -1) {
184 setCheckedStateForView(mCheckedId, false);
185 }
186
187 if (id != -1) {
188 setCheckedStateForView(id, true);
189 }
190
191 setCheckedId(id);
192 }
193
Tor Norbye7b9c9122013-05-30 16:48:33 -0700194 private void setCheckedId(@IdRes int id) {
Felipe Leme27d04462018-02-14 15:08:58 -0800195 boolean changed = id != mCheckedId;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800196 mCheckedId = id;
Felipe Leme27d04462018-02-14 15:08:58 -0800197
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800198 if (mOnCheckedChangeListener != null) {
199 mOnCheckedChangeListener.onCheckedChanged(this, mCheckedId);
200 }
Felipe Leme27d04462018-02-14 15:08:58 -0800201 if (changed) {
202 final AutofillManager afm = mContext.getSystemService(AutofillManager.class);
203 if (afm != null) {
204 afm.notifyValueChanged(this);
205 }
Felipe Leme5882c4f2017-02-16 21:46:46 -0800206 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800207 }
208
209 private void setCheckedStateForView(int viewId, boolean checked) {
210 View checkedView = findViewById(viewId);
211 if (checkedView != null && checkedView instanceof RadioButton) {
212 ((RadioButton) checkedView).setChecked(checked);
213 }
214 }
215
216 /**
217 * <p>Returns the identifier of the selected radio button in this group.
218 * Upon empty selection, the returned value is -1.</p>
219 *
220 * @return the unique id of the selected radio button in this group
221 *
222 * @see #check(int)
223 * @see #clearCheck()
Philip Milneaac722a2012-03-26 13:30:26 -0700224 *
225 * @attr ref android.R.styleable#RadioGroup_checkedButton
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800226 */
Tor Norbye7b9c9122013-05-30 16:48:33 -0700227 @IdRes
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800228 public int getCheckedRadioButtonId() {
229 return mCheckedId;
230 }
231
232 /**
233 * <p>Clears the selection. When the selection is cleared, no radio button
234 * in this group is selected and {@link #getCheckedRadioButtonId()} returns
235 * null.</p>
236 *
237 * @see #check(int)
238 * @see #getCheckedRadioButtonId()
239 */
240 public void clearCheck() {
241 check(-1);
242 }
243
244 /**
245 * <p>Register a callback to be invoked when the checked radio button
246 * changes in this group.</p>
247 *
248 * @param listener the callback to call on checked state change
249 */
250 public void setOnCheckedChangeListener(OnCheckedChangeListener listener) {
251 mOnCheckedChangeListener = listener;
252 }
253
254 /**
255 * {@inheritDoc}
256 */
257 @Override
258 public LayoutParams generateLayoutParams(AttributeSet attrs) {
259 return new RadioGroup.LayoutParams(getContext(), attrs);
260 }
261
262 /**
263 * {@inheritDoc}
264 */
265 @Override
266 protected boolean checkLayoutParams(ViewGroup.LayoutParams p) {
267 return p instanceof RadioGroup.LayoutParams;
268 }
269
270 @Override
271 protected LinearLayout.LayoutParams generateDefaultLayoutParams() {
272 return new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
273 }
274
Svetoslav Ganov8a78fd42012-01-17 14:36:46 -0800275 @Override
Dianne Hackborna7bb6fb2015-02-03 18:13:40 -0800276 public CharSequence getAccessibilityClassName() {
277 return RadioGroup.class.getName();
Svetoslav Ganov8a78fd42012-01-17 14:36:46 -0800278 }
279
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800280 /**
281 * <p>This set of layout parameters defaults the width and the height of
282 * the children to {@link #WRAP_CONTENT} when they are not specified in the
283 * XML file. Otherwise, this class ussed the value read from the XML file.</p>
284 *
285 * <p>See
286 * {@link android.R.styleable#LinearLayout_Layout LinearLayout Attributes}
287 * for a list of all child view attributes that this class supports.</p>
288 *
289 */
290 public static class LayoutParams extends LinearLayout.LayoutParams {
291 /**
292 * {@inheritDoc}
293 */
294 public LayoutParams(Context c, AttributeSet attrs) {
295 super(c, attrs);
296 }
297
298 /**
299 * {@inheritDoc}
300 */
301 public LayoutParams(int w, int h) {
302 super(w, h);
303 }
304
305 /**
306 * {@inheritDoc}
307 */
308 public LayoutParams(int w, int h, float initWeight) {
309 super(w, h, initWeight);
310 }
311
312 /**
313 * {@inheritDoc}
314 */
315 public LayoutParams(ViewGroup.LayoutParams p) {
316 super(p);
317 }
318
319 /**
320 * {@inheritDoc}
321 */
322 public LayoutParams(MarginLayoutParams source) {
323 super(source);
324 }
Dave Burke579e1402012-10-18 20:41:55 -0700325
326 /**
327 * <p>Fixes the child's width to
328 * {@link android.view.ViewGroup.LayoutParams#WRAP_CONTENT} and the child's
329 * height to {@link android.view.ViewGroup.LayoutParams#WRAP_CONTENT}
330 * when not specified in the XML file.</p>
331 *
332 * @param a the styled attributes set
333 * @param widthAttr the width attribute to fetch
334 * @param heightAttr the height attribute to fetch
335 */
336 @Override
337 protected void setBaseAttributes(TypedArray a,
338 int widthAttr, int heightAttr) {
339
340 if (a.hasValue(widthAttr)) {
341 width = a.getLayoutDimension(widthAttr, "layout_width");
342 } else {
343 width = WRAP_CONTENT;
344 }
Aurimas Liutikas99441c52016-10-11 16:48:32 -0700345
Dave Burke579e1402012-10-18 20:41:55 -0700346 if (a.hasValue(heightAttr)) {
347 height = a.getLayoutDimension(heightAttr, "layout_height");
348 } else {
349 height = WRAP_CONTENT;
350 }
351 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800352 }
353
354 /**
355 * <p>Interface definition for a callback to be invoked when the checked
356 * radio button changed in this group.</p>
357 */
358 public interface OnCheckedChangeListener {
359 /**
360 * <p>Called when the checked radio button has changed. When the
361 * selection is cleared, checkedId is -1.</p>
362 *
363 * @param group the group in which the checked radio button has changed
364 * @param checkedId the unique identifier of the newly checked radio button
365 */
Tor Norbye7b9c9122013-05-30 16:48:33 -0700366 public void onCheckedChanged(RadioGroup group, @IdRes int checkedId);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800367 }
368
369 private class CheckedStateTracker implements CompoundButton.OnCheckedChangeListener {
Felipe Leme6d553872016-12-08 17:13:25 -0800370 @Override
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800371 public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
372 // prevents from infinite recursion
373 if (mProtectFromCheckedChange) {
374 return;
375 }
376
377 mProtectFromCheckedChange = true;
378 if (mCheckedId != -1) {
379 setCheckedStateForView(mCheckedId, false);
380 }
381 mProtectFromCheckedChange = false;
382
383 int id = buttonView.getId();
384 setCheckedId(id);
385 }
386 }
387
388 /**
389 * <p>A pass-through listener acts upon the events and dispatches them
390 * to another listener. This allows the table layout to set its own internal
391 * hierarchy change listener without preventing the user to setup his.</p>
392 */
393 private class PassThroughHierarchyChangeListener implements
394 ViewGroup.OnHierarchyChangeListener {
395 private ViewGroup.OnHierarchyChangeListener mOnHierarchyChangeListener;
396
397 /**
398 * {@inheritDoc}
399 */
Felipe Leme6d553872016-12-08 17:13:25 -0800400 @Override
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800401 public void onChildViewAdded(View parent, View child) {
402 if (parent == RadioGroup.this && child instanceof RadioButton) {
403 int id = child.getId();
404 // generates an id if it's missing
405 if (id == View.NO_ID) {
Adam Powella9108a22012-07-18 11:18:09 -0700406 id = View.generateViewId();
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800407 child.setId(id);
408 }
409 ((RadioButton) child).setOnCheckedChangeWidgetListener(
410 mChildOnCheckedChangeListener);
411 }
412
413 if (mOnHierarchyChangeListener != null) {
414 mOnHierarchyChangeListener.onChildViewAdded(parent, child);
415 }
416 }
417
418 /**
419 * {@inheritDoc}
420 */
Felipe Leme6d553872016-12-08 17:13:25 -0800421 @Override
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800422 public void onChildViewRemoved(View parent, View child) {
423 if (parent == RadioGroup.this && child instanceof RadioButton) {
424 ((RadioButton) child).setOnCheckedChangeWidgetListener(null);
425 }
426
427 if (mOnHierarchyChangeListener != null) {
428 mOnHierarchyChangeListener.onChildViewRemoved(parent, child);
429 }
430 }
431 }
Felipe Leme6d553872016-12-08 17:13:25 -0800432
Felipe Leme92736c12018-11-13 12:00:59 -0800433 /** @hide */
Felipe Leme0200d9e2017-01-24 15:10:26 -0800434 @Override
Felipe Leme92736c12018-11-13 12:00:59 -0800435 protected void onProvideStructure(@NonNull ViewStructure structure,
436 @ViewStructureType int viewFor, int flags) {
437 super.onProvideStructure(structure, viewFor, flags);
438
439 if (viewFor == VIEW_STRUCTURE_FOR_AUTOFILL) {
440 structure.setDataIsSensitive(mCheckedId != mInitialCheckedId);
441 }
Felipe Lemec01a8732017-02-22 17:26:06 -0800442 }
443
444 @Override
Felipe Leme955e2522017-03-29 17:47:58 -0700445 public void autofill(AutofillValue value) {
446 if (!isEnabled()) return;
Felipe Lemebab851c2017-02-03 18:45:08 -0800447
Felipe Leme955e2522017-03-29 17:47:58 -0700448 if (!value.isList()) {
Philip P. Moltmann96689032017-03-09 13:19:55 -0800449 Log.w(LOG_TAG, value + " could not be autofilled into " + this);
Felipe Leme955e2522017-03-29 17:47:58 -0700450 return;
Philip P. Moltmann96689032017-03-09 13:19:55 -0800451 }
452
Felipe Leme955e2522017-03-29 17:47:58 -0700453 final int index = value.getListValue();
Felipe Leme6d553872016-12-08 17:13:25 -0800454 final View child = getChildAt(index);
455 if (child == null) {
456 Log.w(VIEW_LOG_TAG, "RadioGroup.autoFill(): no child with index " + index);
Felipe Leme955e2522017-03-29 17:47:58 -0700457 return;
Felipe Leme6d553872016-12-08 17:13:25 -0800458 }
Felipe Leme955e2522017-03-29 17:47:58 -0700459
Felipe Leme6d553872016-12-08 17:13:25 -0800460 check(child.getId());
461 }
462
463 @Override
Felipe Leme8931e302017-03-06 13:44:35 -0800464 public @AutofillType int getAutofillType() {
465 return isEnabled() ? AUTOFILL_TYPE_LIST : AUTOFILL_TYPE_NONE;
Felipe Leme6d553872016-12-08 17:13:25 -0800466 }
Felipe Lemebab851c2017-02-03 18:45:08 -0800467
468 @Override
Felipe Leme640f30a2017-03-06 15:44:06 -0800469 public AutofillValue getAutofillValue() {
Felipe Lemed09ccb82017-02-22 15:02:03 -0800470 if (!isEnabled()) return null;
471
472 final int count = getChildCount();
473 for (int i = 0; i < count; i++) {
474 final View child = getChildAt(i);
475 if (child.getId() == mCheckedId) {
Felipe Leme640f30a2017-03-06 15:44:06 -0800476 return AutofillValue.forList(i);
Felipe Lemed09ccb82017-02-22 15:02:03 -0800477 }
478 }
479 return null;
Felipe Lemebab851c2017-02-03 18:45:08 -0800480 }
sallyyuen69a146c2019-11-25 17:05:47 -0800481
482 @Override
483 public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) {
484 super.onInitializeAccessibilityNodeInfo(info);
485 if (this.getOrientation() == HORIZONTAL) {
486 info.setCollectionInfo(AccessibilityNodeInfo.CollectionInfo.obtain(1,
487 getVisibleChildCount(), false,
488 AccessibilityNodeInfo.CollectionInfo.SELECTION_MODE_SINGLE));
489 } else {
490 info.setCollectionInfo(
491 AccessibilityNodeInfo.CollectionInfo.obtain(getVisibleChildCount(),
492 1, false,
493 AccessibilityNodeInfo.CollectionInfo.SELECTION_MODE_SINGLE));
494 }
495 }
496
497 private int getVisibleChildCount() {
498 int count = 0;
499 for (int i = 0; i < getChildCount(); i++) {
500 if (this.getChildAt(i) instanceof RadioButton) {
501 if (((RadioButton) this.getChildAt(i)).getVisibility() == VISIBLE) {
502 count++;
503 }
504 }
505 }
506 return count;
507 }
508
509 int getIndexWithinVisibleButtons(@Nullable View child) {
510 if (!(child instanceof RadioButton)) {
511 return -1;
512 }
513 int index = 0;
514 for (int i = 0; i < getChildCount(); i++) {
515 if (this.getChildAt(i) instanceof RadioButton) {
516 RadioButton radioButton = (RadioButton) this.getChildAt(i);
517 if (radioButton == child) {
518 return index;
519 }
520 if (radioButton.getVisibility() == VISIBLE) {
521 index++;
522 }
523 }
524 }
525 return -1;
526 }
527}