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