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