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