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