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 android.annotation.Nullable; 20import android.graphics.PorterDuff; 21import com.android.internal.R; 22 23import android.content.Context; 24import android.content.res.ColorStateList; 25import android.content.res.TypedArray; 26import android.graphics.Canvas; 27import android.graphics.drawable.Drawable; 28import android.os.Parcel; 29import android.os.Parcelable; 30import android.util.AttributeSet; 31import android.view.Gravity; 32import android.view.SoundEffectConstants; 33import android.view.ViewDebug; 34import android.view.accessibility.AccessibilityEvent; 35import android.view.accessibility.AccessibilityNodeInfo; 36 37/** 38 * <p> 39 * A button with two states, checked and unchecked. When the button is pressed 40 * or clicked, the state changes automatically. 41 * </p> 42 * 43 * <p><strong>XML attributes</strong></p> 44 * <p> 45 * See {@link android.R.styleable#CompoundButton 46 * CompoundButton Attributes}, {@link android.R.styleable#Button Button 47 * Attributes}, {@link android.R.styleable#TextView TextView Attributes}, {@link 48 * android.R.styleable#View View Attributes} 49 * </p> 50 */ 51public abstract class CompoundButton extends Button implements Checkable { 52 private boolean mChecked; 53 private int mButtonResource; 54 private boolean mBroadcasting; 55 56 private Drawable mButtonDrawable; 57 private ColorStateList mButtonTintList = null; 58 private PorterDuff.Mode mButtonTintMode = null; 59 private boolean mHasButtonTint = false; 60 private boolean mHasButtonTintMode = false; 61 62 private OnCheckedChangeListener mOnCheckedChangeListener; 63 private OnCheckedChangeListener mOnCheckedChangeWidgetListener; 64 65 private static final int[] CHECKED_STATE_SET = { 66 R.attr.state_checked 67 }; 68 69 public CompoundButton(Context context) { 70 this(context, null); 71 } 72 73 public CompoundButton(Context context, AttributeSet attrs) { 74 this(context, attrs, 0); 75 } 76 77 public CompoundButton(Context context, AttributeSet attrs, int defStyleAttr) { 78 this(context, attrs, defStyleAttr, 0); 79 } 80 81 public CompoundButton(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { 82 super(context, attrs, defStyleAttr, defStyleRes); 83 84 final TypedArray a = context.obtainStyledAttributes( 85 attrs, com.android.internal.R.styleable.CompoundButton, defStyleAttr, defStyleRes); 86 87 final Drawable d = a.getDrawable(com.android.internal.R.styleable.CompoundButton_button); 88 if (d != null) { 89 setButtonDrawable(d); 90 } 91 92 if (a.hasValue(R.styleable.CompoundButton_buttonTintMode)) { 93 mButtonTintMode = Drawable.parseTintMode(a.getInt( 94 R.styleable.CompoundButton_buttonTintMode, -1), mButtonTintMode); 95 mHasButtonTintMode = true; 96 } 97 98 if (a.hasValue(R.styleable.CompoundButton_buttonTint)) { 99 mButtonTintList = a.getColorStateList(R.styleable.CompoundButton_buttonTint); 100 mHasButtonTint = true; 101 } 102 103 final boolean checked = a.getBoolean( 104 com.android.internal.R.styleable.CompoundButton_checked, false); 105 setChecked(checked); 106 107 a.recycle(); 108 109 applyButtonTint(); 110 } 111 112 public void toggle() { 113 setChecked(!mChecked); 114 } 115 116 @Override 117 public boolean performClick() { 118 toggle(); 119 120 final boolean handled = super.performClick(); 121 if (!handled) { 122 // View only makes a sound effect if the onClickListener was 123 // called, so we'll need to make one here instead. 124 playSoundEffect(SoundEffectConstants.CLICK); 125 } 126 127 return handled; 128 } 129 130 @ViewDebug.ExportedProperty 131 public boolean isChecked() { 132 return mChecked; 133 } 134 135 /** 136 * <p>Changes the checked state of this button.</p> 137 * 138 * @param checked true to check the button, false to uncheck it 139 */ 140 public void setChecked(boolean checked) { 141 if (mChecked != checked) { 142 mChecked = checked; 143 refreshDrawableState(); 144 notifyViewAccessibilityStateChangedIfNeeded( 145 AccessibilityEvent.CONTENT_CHANGE_TYPE_UNDEFINED); 146 147 // Avoid infinite recursions if setChecked() is called from a listener 148 if (mBroadcasting) { 149 return; 150 } 151 152 mBroadcasting = true; 153 if (mOnCheckedChangeListener != null) { 154 mOnCheckedChangeListener.onCheckedChanged(this, mChecked); 155 } 156 if (mOnCheckedChangeWidgetListener != null) { 157 mOnCheckedChangeWidgetListener.onCheckedChanged(this, mChecked); 158 } 159 160 mBroadcasting = false; 161 } 162 } 163 164 /** 165 * Register a callback to be invoked when the checked state of this button 166 * changes. 167 * 168 * @param listener the callback to call on checked state change 169 */ 170 public void setOnCheckedChangeListener(OnCheckedChangeListener listener) { 171 mOnCheckedChangeListener = listener; 172 } 173 174 /** 175 * Register a callback to be invoked when the checked state of this button 176 * changes. This callback is used for internal purpose only. 177 * 178 * @param listener the callback to call on checked state change 179 * @hide 180 */ 181 void setOnCheckedChangeWidgetListener(OnCheckedChangeListener listener) { 182 mOnCheckedChangeWidgetListener = listener; 183 } 184 185 /** 186 * Interface definition for a callback to be invoked when the checked state 187 * of a compound button changed. 188 */ 189 public static interface OnCheckedChangeListener { 190 /** 191 * Called when the checked state of a compound button has changed. 192 * 193 * @param buttonView The compound button view whose state has changed. 194 * @param isChecked The new checked state of buttonView. 195 */ 196 void onCheckedChanged(CompoundButton buttonView, boolean isChecked); 197 } 198 199 /** 200 * Set the button graphic to a given Drawable, identified by its resource 201 * id. 202 * 203 * @param resid the resource id of the drawable to use as the button 204 * graphic 205 */ 206 public void setButtonDrawable(int resid) { 207 if (resid != 0 && resid == mButtonResource) { 208 return; 209 } 210 211 mButtonResource = resid; 212 213 Drawable d = null; 214 if (mButtonResource != 0) { 215 d = getContext().getDrawable(mButtonResource); 216 } 217 setButtonDrawable(d); 218 } 219 220 /** 221 * Set the button graphic to a given Drawable 222 * 223 * @param d The Drawable to use as the button graphic 224 */ 225 public void setButtonDrawable(Drawable d) { 226 if (mButtonDrawable != d) { 227 if (mButtonDrawable != null) { 228 mButtonDrawable.setCallback(null); 229 unscheduleDrawable(mButtonDrawable); 230 } 231 232 mButtonDrawable = d; 233 234 if (d != null) { 235 d.setCallback(this); 236 d.setLayoutDirection(getLayoutDirection()); 237 if (d.isStateful()) { 238 d.setState(getDrawableState()); 239 } 240 d.setVisible(getVisibility() == VISIBLE, false); 241 setMinHeight(d.getIntrinsicHeight()); 242 applyButtonTint(); 243 } 244 } 245 } 246 247 /** 248 * Applies a tint to the button drawable. Does not modify the current tint 249 * mode, which is {@link PorterDuff.Mode#SRC_IN} by default. 250 * <p> 251 * Subsequent calls to {@link #setButtonDrawable(Drawable)} will 252 * automatically mutate the drawable and apply the specified tint and tint 253 * mode using 254 * {@link Drawable#setTintList(ColorStateList)}. 255 * 256 * @param tint the tint to apply, may be {@code null} to clear tint 257 * 258 * @attr ref android.R.styleable#CompoundButton_buttonTint 259 * @see #setButtonTintList(ColorStateList) 260 * @see Drawable#setTintList(ColorStateList) 261 */ 262 public void setButtonTintList(@Nullable ColorStateList tint) { 263 mButtonTintList = tint; 264 mHasButtonTint = true; 265 266 applyButtonTint(); 267 } 268 269 /** 270 * @return the tint applied to the button drawable 271 * @attr ref android.R.styleable#CompoundButton_buttonTint 272 * @see #setButtonTintList(ColorStateList) 273 */ 274 @Nullable 275 public ColorStateList getButtonTintList() { 276 return mButtonTintList; 277 } 278 279 /** 280 * Specifies the blending mode used to apply the tint specified by 281 * {@link #setButtonTintList(ColorStateList)}} to the button drawable. The 282 * default mode is {@link PorterDuff.Mode#SRC_IN}. 283 * 284 * @param tintMode the blending mode used to apply the tint, may be 285 * {@code null} to clear tint 286 * @attr ref android.R.styleable#CompoundButton_buttonTintMode 287 * @see #getButtonTintMode() 288 * @see Drawable#setTintMode(PorterDuff.Mode) 289 */ 290 public void setButtonTintMode(@Nullable PorterDuff.Mode tintMode) { 291 mButtonTintMode = tintMode; 292 mHasButtonTintMode = true; 293 294 applyButtonTint(); 295 } 296 297 /** 298 * @return the blending mode used to apply the tint to the button drawable 299 * @attr ref android.R.styleable#CompoundButton_buttonTintMode 300 * @see #setButtonTintMode(PorterDuff.Mode) 301 */ 302 @Nullable 303 public PorterDuff.Mode getButtonTintMode() { 304 return mButtonTintMode; 305 } 306 307 private void applyButtonTint() { 308 if (mButtonDrawable != null && (mHasButtonTint || mHasButtonTintMode)) { 309 mButtonDrawable = mButtonDrawable.mutate(); 310 311 if (mHasButtonTint) { 312 mButtonDrawable.setTintList(mButtonTintList); 313 } 314 315 if (mHasButtonTintMode) { 316 mButtonDrawable.setTintMode(mButtonTintMode); 317 } 318 319 // The drawable (or one of its children) may not have been 320 // stateful before applying the tint, so let's try again. 321 if (mButtonDrawable.isStateful()) { 322 mButtonDrawable.setState(getDrawableState()); 323 } 324 } 325 } 326 327 @Override 328 public void onInitializeAccessibilityEvent(AccessibilityEvent event) { 329 super.onInitializeAccessibilityEvent(event); 330 event.setClassName(CompoundButton.class.getName()); 331 event.setChecked(mChecked); 332 } 333 334 @Override 335 public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) { 336 super.onInitializeAccessibilityNodeInfo(info); 337 info.setClassName(CompoundButton.class.getName()); 338 info.setCheckable(true); 339 info.setChecked(mChecked); 340 } 341 342 @Override 343 public int getCompoundPaddingLeft() { 344 int padding = super.getCompoundPaddingLeft(); 345 if (!isLayoutRtl()) { 346 final Drawable buttonDrawable = mButtonDrawable; 347 if (buttonDrawable != null) { 348 padding += buttonDrawable.getIntrinsicWidth(); 349 } 350 } 351 return padding; 352 } 353 354 @Override 355 public int getCompoundPaddingRight() { 356 int padding = super.getCompoundPaddingRight(); 357 if (isLayoutRtl()) { 358 final Drawable buttonDrawable = mButtonDrawable; 359 if (buttonDrawable != null) { 360 padding += buttonDrawable.getIntrinsicWidth(); 361 } 362 } 363 return padding; 364 } 365 366 /** 367 * @hide 368 */ 369 @Override 370 public int getHorizontalOffsetForDrawables() { 371 final Drawable buttonDrawable = mButtonDrawable; 372 return (buttonDrawable != null) ? buttonDrawable.getIntrinsicWidth() : 0; 373 } 374 375 @Override 376 protected void onDraw(Canvas canvas) { 377 final Drawable buttonDrawable = mButtonDrawable; 378 if (buttonDrawable != null) { 379 final int verticalGravity = getGravity() & Gravity.VERTICAL_GRAVITY_MASK; 380 final int drawableHeight = buttonDrawable.getIntrinsicHeight(); 381 final int drawableWidth = buttonDrawable.getIntrinsicWidth(); 382 383 final int top; 384 switch (verticalGravity) { 385 case Gravity.BOTTOM: 386 top = getHeight() - drawableHeight; 387 break; 388 case Gravity.CENTER_VERTICAL: 389 top = (getHeight() - drawableHeight) / 2; 390 break; 391 default: 392 top = 0; 393 } 394 final int bottom = top + drawableHeight; 395 final int left = isLayoutRtl() ? getWidth() - drawableWidth : 0; 396 final int right = isLayoutRtl() ? getWidth() : drawableWidth; 397 398 buttonDrawable.setBounds(left, top, right, bottom); 399 400 final Drawable background = getBackground(); 401 if (background != null) { 402 background.setHotspotBounds(left, top, right, bottom); 403 } 404 } 405 406 super.onDraw(canvas); 407 408 if (buttonDrawable != null) { 409 final int scrollX = mScrollX; 410 final int scrollY = mScrollY; 411 if (scrollX == 0 && scrollY == 0) { 412 buttonDrawable.draw(canvas); 413 } else { 414 canvas.translate(scrollX, scrollY); 415 buttonDrawable.draw(canvas); 416 canvas.translate(-scrollX, -scrollY); 417 } 418 } 419 } 420 421 @Override 422 protected int[] onCreateDrawableState(int extraSpace) { 423 final int[] drawableState = super.onCreateDrawableState(extraSpace + 1); 424 if (isChecked()) { 425 mergeDrawableStates(drawableState, CHECKED_STATE_SET); 426 } 427 return drawableState; 428 } 429 430 @Override 431 protected void drawableStateChanged() { 432 super.drawableStateChanged(); 433 434 if (mButtonDrawable != null) { 435 int[] myDrawableState = getDrawableState(); 436 437 // Set the state of the Drawable 438 mButtonDrawable.setState(myDrawableState); 439 440 invalidate(); 441 } 442 } 443 444 @Override 445 public void drawableHotspotChanged(float x, float y) { 446 super.drawableHotspotChanged(x, y); 447 448 if (mButtonDrawable != null) { 449 mButtonDrawable.setHotspot(x, y); 450 } 451 } 452 453 @Override 454 protected boolean verifyDrawable(Drawable who) { 455 return super.verifyDrawable(who) || who == mButtonDrawable; 456 } 457 458 @Override 459 public void jumpDrawablesToCurrentState() { 460 super.jumpDrawablesToCurrentState(); 461 if (mButtonDrawable != null) mButtonDrawable.jumpToCurrentState(); 462 } 463 464 static class SavedState extends BaseSavedState { 465 boolean checked; 466 467 /** 468 * Constructor called from {@link CompoundButton#onSaveInstanceState()} 469 */ 470 SavedState(Parcelable superState) { 471 super(superState); 472 } 473 474 /** 475 * Constructor called from {@link #CREATOR} 476 */ 477 private SavedState(Parcel in) { 478 super(in); 479 checked = (Boolean)in.readValue(null); 480 } 481 482 @Override 483 public void writeToParcel(Parcel out, int flags) { 484 super.writeToParcel(out, flags); 485 out.writeValue(checked); 486 } 487 488 @Override 489 public String toString() { 490 return "CompoundButton.SavedState{" 491 + Integer.toHexString(System.identityHashCode(this)) 492 + " checked=" + checked + "}"; 493 } 494 495 public static final Parcelable.Creator<SavedState> CREATOR 496 = new Parcelable.Creator<SavedState>() { 497 public SavedState createFromParcel(Parcel in) { 498 return new SavedState(in); 499 } 500 501 public SavedState[] newArray(int size) { 502 return new SavedState[size]; 503 } 504 }; 505 } 506 507 @Override 508 public Parcelable onSaveInstanceState() { 509 Parcelable superState = super.onSaveInstanceState(); 510 511 SavedState ss = new SavedState(superState); 512 513 ss.checked = isChecked(); 514 return ss; 515 } 516 517 @Override 518 public void onRestoreInstanceState(Parcelable state) { 519 SavedState ss = (SavedState) state; 520 521 super.onRestoreInstanceState(ss.getSuperState()); 522 setChecked(ss.checked); 523 requestLayout(); 524 } 525} 526