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