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 }