CompoundButton.java revision 6a394f4def305560c9b7ca3a14b3a313556db36e
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.DrawableRes; 20import android.annotation.Nullable; 21import android.graphics.PorterDuff; 22import com.android.internal.R; 23 24import android.content.Context; 25import android.content.res.ColorStateList; 26import android.content.res.TypedArray; 27import android.graphics.Canvas; 28import android.graphics.drawable.Drawable; 29import android.os.Parcel; 30import android.os.Parcelable; 31import android.util.AttributeSet; 32import android.view.Gravity; 33import android.view.SoundEffectConstants; 34import android.view.ViewDebug; 35import android.view.accessibility.AccessibilityEvent; 36import android.view.accessibility.AccessibilityNodeInfo; 37 38/** 39 * <p> 40 * A button with two states, checked and unchecked. When the button is pressed 41 * or clicked, the state changes automatically. 42 * </p> 43 * 44 * <p><strong>XML attributes</strong></p> 45 * <p> 46 * See {@link android.R.styleable#CompoundButton 47 * CompoundButton Attributes}, {@link android.R.styleable#Button Button 48 * Attributes}, {@link android.R.styleable#TextView TextView Attributes}, {@link 49 * android.R.styleable#View View Attributes} 50 * </p> 51 */ 52public abstract class CompoundButton extends Button implements Checkable { 53 private boolean mChecked; 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 * Sets a drawable as the compound button image given its resource 201 * identifier. 202 * 203 * @param resId the resource identifier of the drawable 204 * @attr ref android.R.styleable#CompoundButton_button 205 */ 206 public void setButtonDrawable(@DrawableRes int resId) { 207 final Drawable d; 208 if (resId != 0) { 209 d = getContext().getDrawable(resId); 210 } else { 211 d = null; 212 } 213 setButtonDrawable(d); 214 } 215 216 /** 217 * Sets a drawable as the compound button image. 218 * 219 * @param drawable the drawable to set 220 * @attr ref android.R.styleable#CompoundButton_button 221 */ 222 @Nullable 223 public void setButtonDrawable(@Nullable Drawable drawable) { 224 if (mButtonDrawable != drawable) { 225 if (mButtonDrawable != null) { 226 mButtonDrawable.setCallback(null); 227 unscheduleDrawable(mButtonDrawable); 228 } 229 230 mButtonDrawable = drawable; 231 232 if (drawable != null) { 233 drawable.setCallback(this); 234 drawable.setLayoutDirection(getLayoutDirection()); 235 if (drawable.isStateful()) { 236 drawable.setState(getDrawableState()); 237 } 238 drawable.setVisible(getVisibility() == VISIBLE, false); 239 setMinHeight(drawable.getIntrinsicHeight()); 240 applyButtonTint(); 241 } 242 } 243 } 244 245 /** 246 * @return the drawable used as the compound button image 247 * @see #setButtonDrawable(Drawable) 248 * @see #setButtonDrawable(int) 249 */ 250 @Nullable 251 public Drawable getButtonDrawable() { 252 return mButtonDrawable; 253 } 254 255 /** 256 * Applies a tint to the button drawable. Does not modify the current tint 257 * mode, which is {@link PorterDuff.Mode#SRC_IN} by default. 258 * <p> 259 * Subsequent calls to {@link #setButtonDrawable(Drawable)} will 260 * automatically mutate the drawable and apply the specified tint and tint 261 * mode using 262 * {@link Drawable#setTintList(ColorStateList)}. 263 * 264 * @param tint the tint to apply, may be {@code null} to clear tint 265 * 266 * @attr ref android.R.styleable#CompoundButton_buttonTint 267 * @see #setButtonTintList(ColorStateList) 268 * @see Drawable#setTintList(ColorStateList) 269 */ 270 public void setButtonTintList(@Nullable ColorStateList tint) { 271 mButtonTintList = tint; 272 mHasButtonTint = true; 273 274 applyButtonTint(); 275 } 276 277 /** 278 * @return the tint applied to the button drawable 279 * @attr ref android.R.styleable#CompoundButton_buttonTint 280 * @see #setButtonTintList(ColorStateList) 281 */ 282 @Nullable 283 public ColorStateList getButtonTintList() { 284 return mButtonTintList; 285 } 286 287 /** 288 * Specifies the blending mode used to apply the tint specified by 289 * {@link #setButtonTintList(ColorStateList)}} to the button drawable. The 290 * default mode is {@link PorterDuff.Mode#SRC_IN}. 291 * 292 * @param tintMode the blending mode used to apply the tint, may be 293 * {@code null} to clear tint 294 * @attr ref android.R.styleable#CompoundButton_buttonTintMode 295 * @see #getButtonTintMode() 296 * @see Drawable#setTintMode(PorterDuff.Mode) 297 */ 298 public void setButtonTintMode(@Nullable PorterDuff.Mode tintMode) { 299 mButtonTintMode = tintMode; 300 mHasButtonTintMode = true; 301 302 applyButtonTint(); 303 } 304 305 /** 306 * @return the blending mode used to apply the tint to the button drawable 307 * @attr ref android.R.styleable#CompoundButton_buttonTintMode 308 * @see #setButtonTintMode(PorterDuff.Mode) 309 */ 310 @Nullable 311 public PorterDuff.Mode getButtonTintMode() { 312 return mButtonTintMode; 313 } 314 315 private void applyButtonTint() { 316 if (mButtonDrawable != null && (mHasButtonTint || mHasButtonTintMode)) { 317 mButtonDrawable = mButtonDrawable.mutate(); 318 319 if (mHasButtonTint) { 320 mButtonDrawable.setTintList(mButtonTintList); 321 } 322 323 if (mHasButtonTintMode) { 324 mButtonDrawable.setTintMode(mButtonTintMode); 325 } 326 327 // The drawable (or one of its children) may not have been 328 // stateful before applying the tint, so let's try again. 329 if (mButtonDrawable.isStateful()) { 330 mButtonDrawable.setState(getDrawableState()); 331 } 332 } 333 } 334 335 @Override 336 public CharSequence getAccessibilityClassName() { 337 return CompoundButton.class.getName(); 338 } 339 340 /** @hide */ 341 @Override 342 public void onInitializeAccessibilityEventInternal(AccessibilityEvent event) { 343 super.onInitializeAccessibilityEventInternal(event); 344 event.setChecked(mChecked); 345 } 346 347 /** @hide */ 348 @Override 349 public void onInitializeAccessibilityNodeInfoInternal(AccessibilityNodeInfo info) { 350 super.onInitializeAccessibilityNodeInfoInternal(info); 351 info.setCheckable(true); 352 info.setChecked(mChecked); 353 } 354 355 @Override 356 public int getCompoundPaddingLeft() { 357 int padding = super.getCompoundPaddingLeft(); 358 if (!isLayoutRtl()) { 359 final Drawable buttonDrawable = mButtonDrawable; 360 if (buttonDrawable != null) { 361 padding += buttonDrawable.getIntrinsicWidth(); 362 } 363 } 364 return padding; 365 } 366 367 @Override 368 public int getCompoundPaddingRight() { 369 int padding = super.getCompoundPaddingRight(); 370 if (isLayoutRtl()) { 371 final Drawable buttonDrawable = mButtonDrawable; 372 if (buttonDrawable != null) { 373 padding += buttonDrawable.getIntrinsicWidth(); 374 } 375 } 376 return padding; 377 } 378 379 /** 380 * @hide 381 */ 382 @Override 383 public int getHorizontalOffsetForDrawables() { 384 final Drawable buttonDrawable = mButtonDrawable; 385 return (buttonDrawable != null) ? buttonDrawable.getIntrinsicWidth() : 0; 386 } 387 388 @Override 389 protected void onDraw(Canvas canvas) { 390 final Drawable buttonDrawable = mButtonDrawable; 391 if (buttonDrawable != null) { 392 final int verticalGravity = getGravity() & Gravity.VERTICAL_GRAVITY_MASK; 393 final int drawableHeight = buttonDrawable.getIntrinsicHeight(); 394 final int drawableWidth = buttonDrawable.getIntrinsicWidth(); 395 396 final int top; 397 switch (verticalGravity) { 398 case Gravity.BOTTOM: 399 top = getHeight() - drawableHeight; 400 break; 401 case Gravity.CENTER_VERTICAL: 402 top = (getHeight() - drawableHeight) / 2; 403 break; 404 default: 405 top = 0; 406 } 407 final int bottom = top + drawableHeight; 408 final int left = isLayoutRtl() ? getWidth() - drawableWidth : 0; 409 final int right = isLayoutRtl() ? getWidth() : drawableWidth; 410 411 buttonDrawable.setBounds(left, top, right, bottom); 412 413 final Drawable background = getBackground(); 414 if (background != null) { 415 background.setHotspotBounds(left, top, right, bottom); 416 } 417 } 418 419 super.onDraw(canvas); 420 421 if (buttonDrawable != null) { 422 final int scrollX = mScrollX; 423 final int scrollY = mScrollY; 424 if (scrollX == 0 && scrollY == 0) { 425 buttonDrawable.draw(canvas); 426 } else { 427 canvas.translate(scrollX, scrollY); 428 buttonDrawable.draw(canvas); 429 canvas.translate(-scrollX, -scrollY); 430 } 431 } 432 } 433 434 @Override 435 protected int[] onCreateDrawableState(int extraSpace) { 436 final int[] drawableState = super.onCreateDrawableState(extraSpace + 1); 437 if (isChecked()) { 438 mergeDrawableStates(drawableState, CHECKED_STATE_SET); 439 } 440 return drawableState; 441 } 442 443 @Override 444 protected void drawableStateChanged() { 445 super.drawableStateChanged(); 446 447 if (mButtonDrawable != null) { 448 int[] myDrawableState = getDrawableState(); 449 450 // Set the state of the Drawable 451 mButtonDrawable.setState(myDrawableState); 452 453 invalidate(); 454 } 455 } 456 457 @Override 458 public void drawableHotspotChanged(float x, float y) { 459 super.drawableHotspotChanged(x, y); 460 461 if (mButtonDrawable != null) { 462 mButtonDrawable.setHotspot(x, y); 463 } 464 } 465 466 @Override 467 protected boolean verifyDrawable(Drawable who) { 468 return super.verifyDrawable(who) || who == mButtonDrawable; 469 } 470 471 @Override 472 public void jumpDrawablesToCurrentState() { 473 super.jumpDrawablesToCurrentState(); 474 if (mButtonDrawable != null) mButtonDrawable.jumpToCurrentState(); 475 } 476 477 static class SavedState extends BaseSavedState { 478 boolean checked; 479 480 /** 481 * Constructor called from {@link CompoundButton#onSaveInstanceState()} 482 */ 483 SavedState(Parcelable superState) { 484 super(superState); 485 } 486 487 /** 488 * Constructor called from {@link #CREATOR} 489 */ 490 private SavedState(Parcel in) { 491 super(in); 492 checked = (Boolean)in.readValue(null); 493 } 494 495 @Override 496 public void writeToParcel(Parcel out, int flags) { 497 super.writeToParcel(out, flags); 498 out.writeValue(checked); 499 } 500 501 @Override 502 public String toString() { 503 return "CompoundButton.SavedState{" 504 + Integer.toHexString(System.identityHashCode(this)) 505 + " checked=" + checked + "}"; 506 } 507 508 public static final Parcelable.Creator<SavedState> CREATOR 509 = new Parcelable.Creator<SavedState>() { 510 public SavedState createFromParcel(Parcel in) { 511 return new SavedState(in); 512 } 513 514 public SavedState[] newArray(int size) { 515 return new SavedState[size]; 516 } 517 }; 518 } 519 520 @Override 521 public Parcelable onSaveInstanceState() { 522 Parcelable superState = super.onSaveInstanceState(); 523 524 SavedState ss = new SavedState(superState); 525 526 ss.checked = isChecked(); 527 return ss; 528 } 529 530 @Override 531 public void onRestoreInstanceState(Parcelable state) { 532 SavedState ss = (SavedState) state; 533 534 super.onRestoreInstanceState(ss.getSuperState()); 535 setChecked(ss.checked); 536 requestLayout(); 537 } 538} 539