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