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