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