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