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