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