1/* 2 * Copyright (C) 2007 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.content.Context; 22import android.content.res.TypedArray; 23import android.graphics.Canvas; 24import android.graphics.drawable.Drawable; 25import android.os.Parcel; 26import android.os.Parcelable; 27import android.util.AttributeSet; 28import android.view.Gravity; 29import android.view.ViewDebug; 30import android.view.accessibility.AccessibilityEvent; 31 32/** 33 * <p> 34 * A button with two states, checked and unchecked. When the button is pressed 35 * or clicked, the state changes automatically. 36 * </p> 37 * 38 * <p><strong>XML attributes</strong></p> 39 * <p> 40 * See {@link android.R.styleable#CompoundButton 41 * CompoundButton Attributes}, {@link android.R.styleable#Button Button 42 * Attributes}, {@link android.R.styleable#TextView TextView Attributes}, {@link 43 * android.R.styleable#View View Attributes} 44 * </p> 45 */ 46public abstract class CompoundButton extends Button implements Checkable { 47 private boolean mChecked; 48 private int mButtonResource; 49 private boolean mBroadcasting; 50 private Drawable mButtonDrawable; 51 private OnCheckedChangeListener mOnCheckedChangeListener; 52 private OnCheckedChangeListener mOnCheckedChangeWidgetListener; 53 54 private static final int[] CHECKED_STATE_SET = { 55 R.attr.state_checked 56 }; 57 58 public CompoundButton(Context context) { 59 this(context, null); 60 } 61 62 public CompoundButton(Context context, AttributeSet attrs) { 63 this(context, attrs, 0); 64 } 65 66 public CompoundButton(Context context, AttributeSet attrs, int defStyle) { 67 super(context, attrs, defStyle); 68 69 TypedArray a = 70 context.obtainStyledAttributes( 71 attrs, com.android.internal.R.styleable.CompoundButton, defStyle, 0); 72 73 Drawable d = a.getDrawable(com.android.internal.R.styleable.CompoundButton_button); 74 if (d != null) { 75 setButtonDrawable(d); 76 } 77 78 boolean checked = a 79 .getBoolean(com.android.internal.R.styleable.CompoundButton_checked, false); 80 setChecked(checked); 81 82 a.recycle(); 83 } 84 85 public void toggle() { 86 setChecked(!mChecked); 87 } 88 89 @Override 90 public boolean performClick() { 91 /* 92 * XXX: These are tiny, need some surrounding 'expanded touch area', 93 * which will need to be implemented in Button if we only override 94 * performClick() 95 */ 96 97 /* When clicked, toggle the state */ 98 toggle(); 99 return super.performClick(); 100 } 101 102 @ViewDebug.ExportedProperty 103 public boolean isChecked() { 104 return mChecked; 105 } 106 107 /** 108 * <p>Changes the checked state of this button.</p> 109 * 110 * @param checked true to check the button, false to uncheck it 111 */ 112 public void setChecked(boolean checked) { 113 if (mChecked != checked) { 114 mChecked = checked; 115 refreshDrawableState(); 116 117 // Avoid infinite recursions if setChecked() is called from a listener 118 if (mBroadcasting) { 119 return; 120 } 121 122 mBroadcasting = true; 123 if (mOnCheckedChangeListener != null) { 124 mOnCheckedChangeListener.onCheckedChanged(this, mChecked); 125 } 126 if (mOnCheckedChangeWidgetListener != null) { 127 mOnCheckedChangeWidgetListener.onCheckedChanged(this, mChecked); 128 } 129 130 mBroadcasting = false; 131 } 132 } 133 134 /** 135 * Register a callback to be invoked when the checked state of this button 136 * changes. 137 * 138 * @param listener the callback to call on checked state change 139 */ 140 public void setOnCheckedChangeListener(OnCheckedChangeListener listener) { 141 mOnCheckedChangeListener = listener; 142 } 143 144 /** 145 * Register a callback to be invoked when the checked state of this button 146 * changes. This callback is used for internal purpose only. 147 * 148 * @param listener the callback to call on checked state change 149 * @hide 150 */ 151 void setOnCheckedChangeWidgetListener(OnCheckedChangeListener listener) { 152 mOnCheckedChangeWidgetListener = listener; 153 } 154 155 /** 156 * Interface definition for a callback to be invoked when the checked state 157 * of a compound button changed. 158 */ 159 public static interface OnCheckedChangeListener { 160 /** 161 * Called when the checked state of a compound button has changed. 162 * 163 * @param buttonView The compound button view whose state has changed. 164 * @param isChecked The new checked state of buttonView. 165 */ 166 void onCheckedChanged(CompoundButton buttonView, boolean isChecked); 167 } 168 169 /** 170 * Set the background to a given Drawable, identified by its resource id. 171 * 172 * @param resid the resource id of the drawable to use as the background 173 */ 174 public void setButtonDrawable(int resid) { 175 if (resid != 0 && resid == mButtonResource) { 176 return; 177 } 178 179 mButtonResource = resid; 180 181 Drawable d = null; 182 if (mButtonResource != 0) { 183 d = getResources().getDrawable(mButtonResource); 184 } 185 setButtonDrawable(d); 186 } 187 188 /** 189 * Set the background to a given Drawable 190 * 191 * @param d The Drawable to use as the background 192 */ 193 public void setButtonDrawable(Drawable d) { 194 if (d != null) { 195 if (mButtonDrawable != null) { 196 mButtonDrawable.setCallback(null); 197 unscheduleDrawable(mButtonDrawable); 198 } 199 d.setCallback(this); 200 d.setState(getDrawableState()); 201 d.setVisible(getVisibility() == VISIBLE, false); 202 mButtonDrawable = d; 203 mButtonDrawable.setState(null); 204 setMinHeight(mButtonDrawable.getIntrinsicHeight()); 205 } 206 207 refreshDrawableState(); 208 } 209 210 @Override 211 public boolean dispatchPopulateAccessibilityEvent(AccessibilityEvent event) { 212 boolean populated = super.dispatchPopulateAccessibilityEvent(event); 213 214 if (!populated) { 215 int resourceId = 0; 216 if (mChecked) { 217 resourceId = R.string.accessibility_compound_button_selected; 218 } else { 219 resourceId = R.string.accessibility_compound_button_unselected; 220 } 221 String state = getResources().getString(resourceId); 222 event.getText().add(state); 223 event.setChecked(mChecked); 224 } 225 226 return populated; 227 } 228 229 @Override 230 protected void onDraw(Canvas canvas) { 231 super.onDraw(canvas); 232 233 final Drawable buttonDrawable = mButtonDrawable; 234 if (buttonDrawable != null) { 235 final int verticalGravity = getGravity() & Gravity.VERTICAL_GRAVITY_MASK; 236 final int height = buttonDrawable.getIntrinsicHeight(); 237 238 int y = 0; 239 240 switch (verticalGravity) { 241 case Gravity.BOTTOM: 242 y = getHeight() - height; 243 break; 244 case Gravity.CENTER_VERTICAL: 245 y = (getHeight() - height) / 2; 246 break; 247 } 248 249 buttonDrawable.setBounds(0, y, buttonDrawable.getIntrinsicWidth(), y + height); 250 buttonDrawable.draw(canvas); 251 } 252 } 253 254 @Override 255 protected int[] onCreateDrawableState(int extraSpace) { 256 final int[] drawableState = super.onCreateDrawableState(extraSpace + 1); 257 if (isChecked()) { 258 mergeDrawableStates(drawableState, CHECKED_STATE_SET); 259 } 260 return drawableState; 261 } 262 263 @Override 264 protected void drawableStateChanged() { 265 super.drawableStateChanged(); 266 267 if (mButtonDrawable != null) { 268 int[] myDrawableState = getDrawableState(); 269 270 // Set the state of the Drawable 271 mButtonDrawable.setState(myDrawableState); 272 273 invalidate(); 274 } 275 } 276 277 @Override 278 protected boolean verifyDrawable(Drawable who) { 279 return super.verifyDrawable(who) || who == mButtonDrawable; 280 } 281 282 static class SavedState extends BaseSavedState { 283 boolean checked; 284 285 /** 286 * Constructor called from {@link CompoundButton#onSaveInstanceState()} 287 */ 288 SavedState(Parcelable superState) { 289 super(superState); 290 } 291 292 /** 293 * Constructor called from {@link #CREATOR} 294 */ 295 private SavedState(Parcel in) { 296 super(in); 297 checked = (Boolean)in.readValue(null); 298 } 299 300 @Override 301 public void writeToParcel(Parcel out, int flags) { 302 super.writeToParcel(out, flags); 303 out.writeValue(checked); 304 } 305 306 @Override 307 public String toString() { 308 return "CompoundButton.SavedState{" 309 + Integer.toHexString(System.identityHashCode(this)) 310 + " checked=" + checked + "}"; 311 } 312 313 public static final Parcelable.Creator<SavedState> CREATOR 314 = new Parcelable.Creator<SavedState>() { 315 public SavedState createFromParcel(Parcel in) { 316 return new SavedState(in); 317 } 318 319 public SavedState[] newArray(int size) { 320 return new SavedState[size]; 321 } 322 }; 323 } 324 325 @Override 326 public Parcelable onSaveInstanceState() { 327 // Force our ancestor class to save its state 328 setFreezesText(true); 329 Parcelable superState = super.onSaveInstanceState(); 330 331 SavedState ss = new SavedState(superState); 332 333 ss.checked = isChecked(); 334 return ss; 335 } 336 337 @Override 338 public void onRestoreInstanceState(Parcelable state) { 339 SavedState ss = (SavedState) state; 340 341 super.onRestoreInstanceState(ss.getSuperState()); 342 setChecked(ss.checked); 343 requestLayout(); 344 } 345} 346