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