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