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