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 boolean changed = id != mCheckedId; 187 mCheckedId = id; 188 189 if (mOnCheckedChangeListener != null) { 190 mOnCheckedChangeListener.onCheckedChanged(this, mCheckedId); 191 } 192 if (changed) { 193 final AutofillManager afm = mContext.getSystemService(AutofillManager.class); 194 if (afm != null) { 195 afm.notifyValueChanged(this); 196 } 197 } 198 } 199 setCheckedStateForView(int viewId, boolean checked)200 private void setCheckedStateForView(int viewId, boolean checked) { 201 View checkedView = findViewById(viewId); 202 if (checkedView != null && checkedView instanceof RadioButton) { 203 ((RadioButton) checkedView).setChecked(checked); 204 } 205 } 206 207 /** 208 * <p>Returns the identifier of the selected radio button in this group. 209 * Upon empty selection, the returned value is -1.</p> 210 * 211 * @return the unique id of the selected radio button in this group 212 * 213 * @see #check(int) 214 * @see #clearCheck() 215 * 216 * @attr ref android.R.styleable#RadioGroup_checkedButton 217 */ 218 @IdRes getCheckedRadioButtonId()219 public int getCheckedRadioButtonId() { 220 return mCheckedId; 221 } 222 223 /** 224 * <p>Clears the selection. When the selection is cleared, no radio button 225 * in this group is selected and {@link #getCheckedRadioButtonId()} returns 226 * null.</p> 227 * 228 * @see #check(int) 229 * @see #getCheckedRadioButtonId() 230 */ clearCheck()231 public void clearCheck() { 232 check(-1); 233 } 234 235 /** 236 * <p>Register a callback to be invoked when the checked radio button 237 * changes in this group.</p> 238 * 239 * @param listener the callback to call on checked state change 240 */ setOnCheckedChangeListener(OnCheckedChangeListener listener)241 public void setOnCheckedChangeListener(OnCheckedChangeListener listener) { 242 mOnCheckedChangeListener = listener; 243 } 244 245 /** 246 * {@inheritDoc} 247 */ 248 @Override generateLayoutParams(AttributeSet attrs)249 public LayoutParams generateLayoutParams(AttributeSet attrs) { 250 return new RadioGroup.LayoutParams(getContext(), attrs); 251 } 252 253 /** 254 * {@inheritDoc} 255 */ 256 @Override checkLayoutParams(ViewGroup.LayoutParams p)257 protected boolean checkLayoutParams(ViewGroup.LayoutParams p) { 258 return p instanceof RadioGroup.LayoutParams; 259 } 260 261 @Override generateDefaultLayoutParams()262 protected LinearLayout.LayoutParams generateDefaultLayoutParams() { 263 return new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT); 264 } 265 266 @Override getAccessibilityClassName()267 public CharSequence getAccessibilityClassName() { 268 return RadioGroup.class.getName(); 269 } 270 271 /** 272 * <p>This set of layout parameters defaults the width and the height of 273 * the children to {@link #WRAP_CONTENT} when they are not specified in the 274 * XML file. Otherwise, this class ussed the value read from the XML file.</p> 275 * 276 * <p>See 277 * {@link android.R.styleable#LinearLayout_Layout LinearLayout Attributes} 278 * for a list of all child view attributes that this class supports.</p> 279 * 280 */ 281 public static class LayoutParams extends LinearLayout.LayoutParams { 282 /** 283 * {@inheritDoc} 284 */ LayoutParams(Context c, AttributeSet attrs)285 public LayoutParams(Context c, AttributeSet attrs) { 286 super(c, attrs); 287 } 288 289 /** 290 * {@inheritDoc} 291 */ LayoutParams(int w, int h)292 public LayoutParams(int w, int h) { 293 super(w, h); 294 } 295 296 /** 297 * {@inheritDoc} 298 */ LayoutParams(int w, int h, float initWeight)299 public LayoutParams(int w, int h, float initWeight) { 300 super(w, h, initWeight); 301 } 302 303 /** 304 * {@inheritDoc} 305 */ LayoutParams(ViewGroup.LayoutParams p)306 public LayoutParams(ViewGroup.LayoutParams p) { 307 super(p); 308 } 309 310 /** 311 * {@inheritDoc} 312 */ LayoutParams(MarginLayoutParams source)313 public LayoutParams(MarginLayoutParams source) { 314 super(source); 315 } 316 317 /** 318 * <p>Fixes the child's width to 319 * {@link android.view.ViewGroup.LayoutParams#WRAP_CONTENT} and the child's 320 * height to {@link android.view.ViewGroup.LayoutParams#WRAP_CONTENT} 321 * when not specified in the XML file.</p> 322 * 323 * @param a the styled attributes set 324 * @param widthAttr the width attribute to fetch 325 * @param heightAttr the height attribute to fetch 326 */ 327 @Override setBaseAttributes(TypedArray a, int widthAttr, int heightAttr)328 protected void setBaseAttributes(TypedArray a, 329 int widthAttr, int heightAttr) { 330 331 if (a.hasValue(widthAttr)) { 332 width = a.getLayoutDimension(widthAttr, "layout_width"); 333 } else { 334 width = WRAP_CONTENT; 335 } 336 337 if (a.hasValue(heightAttr)) { 338 height = a.getLayoutDimension(heightAttr, "layout_height"); 339 } else { 340 height = WRAP_CONTENT; 341 } 342 } 343 } 344 345 /** 346 * <p>Interface definition for a callback to be invoked when the checked 347 * radio button changed in this group.</p> 348 */ 349 public interface OnCheckedChangeListener { 350 /** 351 * <p>Called when the checked radio button has changed. When the 352 * selection is cleared, checkedId is -1.</p> 353 * 354 * @param group the group in which the checked radio button has changed 355 * @param checkedId the unique identifier of the newly checked radio button 356 */ onCheckedChanged(RadioGroup group, @IdRes int checkedId)357 public void onCheckedChanged(RadioGroup group, @IdRes int checkedId); 358 } 359 360 private class CheckedStateTracker implements CompoundButton.OnCheckedChangeListener { 361 @Override onCheckedChanged(CompoundButton buttonView, boolean isChecked)362 public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) { 363 // prevents from infinite recursion 364 if (mProtectFromCheckedChange) { 365 return; 366 } 367 368 mProtectFromCheckedChange = true; 369 if (mCheckedId != -1) { 370 setCheckedStateForView(mCheckedId, false); 371 } 372 mProtectFromCheckedChange = false; 373 374 int id = buttonView.getId(); 375 setCheckedId(id); 376 } 377 } 378 379 /** 380 * <p>A pass-through listener acts upon the events and dispatches them 381 * to another listener. This allows the table layout to set its own internal 382 * hierarchy change listener without preventing the user to setup his.</p> 383 */ 384 private class PassThroughHierarchyChangeListener implements 385 ViewGroup.OnHierarchyChangeListener { 386 private ViewGroup.OnHierarchyChangeListener mOnHierarchyChangeListener; 387 388 /** 389 * {@inheritDoc} 390 */ 391 @Override onChildViewAdded(View parent, View child)392 public void onChildViewAdded(View parent, View child) { 393 if (parent == RadioGroup.this && child instanceof RadioButton) { 394 int id = child.getId(); 395 // generates an id if it's missing 396 if (id == View.NO_ID) { 397 id = View.generateViewId(); 398 child.setId(id); 399 } 400 ((RadioButton) child).setOnCheckedChangeWidgetListener( 401 mChildOnCheckedChangeListener); 402 } 403 404 if (mOnHierarchyChangeListener != null) { 405 mOnHierarchyChangeListener.onChildViewAdded(parent, child); 406 } 407 } 408 409 /** 410 * {@inheritDoc} 411 */ 412 @Override onChildViewRemoved(View parent, View child)413 public void onChildViewRemoved(View parent, View child) { 414 if (parent == RadioGroup.this && child instanceof RadioButton) { 415 ((RadioButton) child).setOnCheckedChangeWidgetListener(null); 416 } 417 418 if (mOnHierarchyChangeListener != null) { 419 mOnHierarchyChangeListener.onChildViewRemoved(parent, child); 420 } 421 } 422 } 423 424 @Override onProvideAutofillStructure(ViewStructure structure, int flags)425 public void onProvideAutofillStructure(ViewStructure structure, int flags) { 426 super.onProvideAutofillStructure(structure, flags); 427 structure.setDataIsSensitive(mCheckedId != mInitialCheckedId); 428 } 429 430 @Override autofill(AutofillValue value)431 public void autofill(AutofillValue value) { 432 if (!isEnabled()) return; 433 434 if (!value.isList()) { 435 Log.w(LOG_TAG, value + " could not be autofilled into " + this); 436 return; 437 } 438 439 final int index = value.getListValue(); 440 final View child = getChildAt(index); 441 if (child == null) { 442 Log.w(VIEW_LOG_TAG, "RadioGroup.autoFill(): no child with index " + index); 443 return; 444 } 445 446 check(child.getId()); 447 } 448 449 @Override getAutofillType()450 public @AutofillType int getAutofillType() { 451 return isEnabled() ? AUTOFILL_TYPE_LIST : AUTOFILL_TYPE_NONE; 452 } 453 454 @Override getAutofillValue()455 public AutofillValue getAutofillValue() { 456 if (!isEnabled()) return null; 457 458 final int count = getChildCount(); 459 for (int i = 0; i < count; i++) { 460 final View child = getChildAt(i); 461 if (child.getId() == mCheckedId) { 462 return AutofillValue.forList(i); 463 } 464 } 465 return null; 466 } 467 } 468