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