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