/* * Copyright (C) 2007 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package android.widget; import android.annotation.DrawableRes; import android.annotation.NonNull; import android.annotation.Nullable; import android.graphics.PorterDuff; import android.view.ViewHierarchyEncoder; import com.android.internal.R; import android.content.Context; import android.content.res.ColorStateList; import android.content.res.TypedArray; import android.graphics.Canvas; import android.graphics.drawable.Drawable; import android.os.Parcel; import android.os.Parcelable; import android.util.AttributeSet; import android.view.Gravity; import android.view.SoundEffectConstants; import android.view.ViewDebug; import android.view.accessibility.AccessibilityEvent; import android.view.accessibility.AccessibilityNodeInfo; /** *
* A button with two states, checked and unchecked. When the button is pressed * or clicked, the state changes automatically. *
* *XML attributes
** See {@link android.R.styleable#CompoundButton * CompoundButton Attributes}, {@link android.R.styleable#Button Button * Attributes}, {@link android.R.styleable#TextView TextView Attributes}, {@link * android.R.styleable#View View Attributes} *
*/ public abstract class CompoundButton extends Button implements Checkable { private boolean mChecked; private boolean mBroadcasting; private Drawable mButtonDrawable; private ColorStateList mButtonTintList = null; private PorterDuff.Mode mButtonTintMode = null; private boolean mHasButtonTint = false; private boolean mHasButtonTintMode = false; private OnCheckedChangeListener mOnCheckedChangeListener; private OnCheckedChangeListener mOnCheckedChangeWidgetListener; private static final int[] CHECKED_STATE_SET = { R.attr.state_checked }; public CompoundButton(Context context) { this(context, null); } public CompoundButton(Context context, AttributeSet attrs) { this(context, attrs, 0); } public CompoundButton(Context context, AttributeSet attrs, int defStyleAttr) { this(context, attrs, defStyleAttr, 0); } public CompoundButton(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { super(context, attrs, defStyleAttr, defStyleRes); final TypedArray a = context.obtainStyledAttributes( attrs, com.android.internal.R.styleable.CompoundButton, defStyleAttr, defStyleRes); final Drawable d = a.getDrawable(com.android.internal.R.styleable.CompoundButton_button); if (d != null) { setButtonDrawable(d); } if (a.hasValue(R.styleable.CompoundButton_buttonTintMode)) { mButtonTintMode = Drawable.parseTintMode(a.getInt( R.styleable.CompoundButton_buttonTintMode, -1), mButtonTintMode); mHasButtonTintMode = true; } if (a.hasValue(R.styleable.CompoundButton_buttonTint)) { mButtonTintList = a.getColorStateList(R.styleable.CompoundButton_buttonTint); mHasButtonTint = true; } final boolean checked = a.getBoolean( com.android.internal.R.styleable.CompoundButton_checked, false); setChecked(checked); a.recycle(); applyButtonTint(); } public void toggle() { setChecked(!mChecked); } @Override public boolean performClick() { toggle(); final boolean handled = super.performClick(); if (!handled) { // View only makes a sound effect if the onClickListener was // called, so we'll need to make one here instead. playSoundEffect(SoundEffectConstants.CLICK); } return handled; } @ViewDebug.ExportedProperty public boolean isChecked() { return mChecked; } /** *Changes the checked state of this button.
* * @param checked true to check the button, false to uncheck it */ public void setChecked(boolean checked) { if (mChecked != checked) { mChecked = checked; refreshDrawableState(); notifyViewAccessibilityStateChangedIfNeeded( AccessibilityEvent.CONTENT_CHANGE_TYPE_UNDEFINED); // Avoid infinite recursions if setChecked() is called from a listener if (mBroadcasting) { return; } mBroadcasting = true; if (mOnCheckedChangeListener != null) { mOnCheckedChangeListener.onCheckedChanged(this, mChecked); } if (mOnCheckedChangeWidgetListener != null) { mOnCheckedChangeWidgetListener.onCheckedChanged(this, mChecked); } mBroadcasting = false; } } /** * Register a callback to be invoked when the checked state of this button * changes. * * @param listener the callback to call on checked state change */ public void setOnCheckedChangeListener(OnCheckedChangeListener listener) { mOnCheckedChangeListener = listener; } /** * Register a callback to be invoked when the checked state of this button * changes. This callback is used for internal purpose only. * * @param listener the callback to call on checked state change * @hide */ void setOnCheckedChangeWidgetListener(OnCheckedChangeListener listener) { mOnCheckedChangeWidgetListener = listener; } /** * Interface definition for a callback to be invoked when the checked state * of a compound button changed. */ public static interface OnCheckedChangeListener { /** * Called when the checked state of a compound button has changed. * * @param buttonView The compound button view whose state has changed. * @param isChecked The new checked state of buttonView. */ void onCheckedChanged(CompoundButton buttonView, boolean isChecked); } /** * Sets a drawable as the compound button image given its resource * identifier. * * @param resId the resource identifier of the drawable * @attr ref android.R.styleable#CompoundButton_button */ public void setButtonDrawable(@DrawableRes int resId) { final Drawable d; if (resId != 0) { d = getContext().getDrawable(resId); } else { d = null; } setButtonDrawable(d); } /** * Sets a drawable as the compound button image. * * @param drawable the drawable to set * @attr ref android.R.styleable#CompoundButton_button */ @Nullable public void setButtonDrawable(@Nullable Drawable drawable) { if (mButtonDrawable != drawable) { if (mButtonDrawable != null) { mButtonDrawable.setCallback(null); unscheduleDrawable(mButtonDrawable); } mButtonDrawable = drawable; if (drawable != null) { drawable.setCallback(this); drawable.setLayoutDirection(getLayoutDirection()); if (drawable.isStateful()) { drawable.setState(getDrawableState()); } drawable.setVisible(getVisibility() == VISIBLE, false); setMinHeight(drawable.getIntrinsicHeight()); applyButtonTint(); } } } /** * @hide */ @Override public void onResolveDrawables(@ResolvedLayoutDir int layoutDirection) { super.onResolveDrawables(layoutDirection); if (mButtonDrawable != null) { mButtonDrawable.setLayoutDirection(layoutDirection); } } /** * @return the drawable used as the compound button image * @see #setButtonDrawable(Drawable) * @see #setButtonDrawable(int) */ @Nullable public Drawable getButtonDrawable() { return mButtonDrawable; } /** * Applies a tint to the button drawable. Does not modify the current tint * mode, which is {@link PorterDuff.Mode#SRC_IN} by default. *
* Subsequent calls to {@link #setButtonDrawable(Drawable)} will
* automatically mutate the drawable and apply the specified tint and tint
* mode using
* {@link Drawable#setTintList(ColorStateList)}.
*
* @param tint the tint to apply, may be {@code null} to clear tint
*
* @attr ref android.R.styleable#CompoundButton_buttonTint
* @see #setButtonTintList(ColorStateList)
* @see Drawable#setTintList(ColorStateList)
*/
public void setButtonTintList(@Nullable ColorStateList tint) {
mButtonTintList = tint;
mHasButtonTint = true;
applyButtonTint();
}
/**
* @return the tint applied to the button drawable
* @attr ref android.R.styleable#CompoundButton_buttonTint
* @see #setButtonTintList(ColorStateList)
*/
@Nullable
public ColorStateList getButtonTintList() {
return mButtonTintList;
}
/**
* Specifies the blending mode used to apply the tint specified by
* {@link #setButtonTintList(ColorStateList)}} to the button drawable. The
* default mode is {@link PorterDuff.Mode#SRC_IN}.
*
* @param tintMode the blending mode used to apply the tint, may be
* {@code null} to clear tint
* @attr ref android.R.styleable#CompoundButton_buttonTintMode
* @see #getButtonTintMode()
* @see Drawable#setTintMode(PorterDuff.Mode)
*/
public void setButtonTintMode(@Nullable PorterDuff.Mode tintMode) {
mButtonTintMode = tintMode;
mHasButtonTintMode = true;
applyButtonTint();
}
/**
* @return the blending mode used to apply the tint to the button drawable
* @attr ref android.R.styleable#CompoundButton_buttonTintMode
* @see #setButtonTintMode(PorterDuff.Mode)
*/
@Nullable
public PorterDuff.Mode getButtonTintMode() {
return mButtonTintMode;
}
private void applyButtonTint() {
if (mButtonDrawable != null && (mHasButtonTint || mHasButtonTintMode)) {
mButtonDrawable = mButtonDrawable.mutate();
if (mHasButtonTint) {
mButtonDrawable.setTintList(mButtonTintList);
}
if (mHasButtonTintMode) {
mButtonDrawable.setTintMode(mButtonTintMode);
}
// The drawable (or one of its children) may not have been
// stateful before applying the tint, so let's try again.
if (mButtonDrawable.isStateful()) {
mButtonDrawable.setState(getDrawableState());
}
}
}
@Override
public CharSequence getAccessibilityClassName() {
return CompoundButton.class.getName();
}
/** @hide */
@Override
public void onInitializeAccessibilityEventInternal(AccessibilityEvent event) {
super.onInitializeAccessibilityEventInternal(event);
event.setChecked(mChecked);
}
/** @hide */
@Override
public void onInitializeAccessibilityNodeInfoInternal(AccessibilityNodeInfo info) {
super.onInitializeAccessibilityNodeInfoInternal(info);
info.setCheckable(true);
info.setChecked(mChecked);
}
@Override
public int getCompoundPaddingLeft() {
int padding = super.getCompoundPaddingLeft();
if (!isLayoutRtl()) {
final Drawable buttonDrawable = mButtonDrawable;
if (buttonDrawable != null) {
padding += buttonDrawable.getIntrinsicWidth();
}
}
return padding;
}
@Override
public int getCompoundPaddingRight() {
int padding = super.getCompoundPaddingRight();
if (isLayoutRtl()) {
final Drawable buttonDrawable = mButtonDrawable;
if (buttonDrawable != null) {
padding += buttonDrawable.getIntrinsicWidth();
}
}
return padding;
}
/**
* @hide
*/
@Override
public int getHorizontalOffsetForDrawables() {
final Drawable buttonDrawable = mButtonDrawable;
return (buttonDrawable != null) ? buttonDrawable.getIntrinsicWidth() : 0;
}
@Override
protected void onDraw(Canvas canvas) {
final Drawable buttonDrawable = mButtonDrawable;
if (buttonDrawable != null) {
final int verticalGravity = getGravity() & Gravity.VERTICAL_GRAVITY_MASK;
final int drawableHeight = buttonDrawable.getIntrinsicHeight();
final int drawableWidth = buttonDrawable.getIntrinsicWidth();
final int top;
switch (verticalGravity) {
case Gravity.BOTTOM:
top = getHeight() - drawableHeight;
break;
case Gravity.CENTER_VERTICAL:
top = (getHeight() - drawableHeight) / 2;
break;
default:
top = 0;
}
final int bottom = top + drawableHeight;
final int left = isLayoutRtl() ? getWidth() - drawableWidth : 0;
final int right = isLayoutRtl() ? getWidth() : drawableWidth;
buttonDrawable.setBounds(left, top, right, bottom);
final Drawable background = getBackground();
if (background != null) {
background.setHotspotBounds(left, top, right, bottom);
}
}
super.onDraw(canvas);
if (buttonDrawable != null) {
final int scrollX = mScrollX;
final int scrollY = mScrollY;
if (scrollX == 0 && scrollY == 0) {
buttonDrawable.draw(canvas);
} else {
canvas.translate(scrollX, scrollY);
buttonDrawable.draw(canvas);
canvas.translate(-scrollX, -scrollY);
}
}
}
@Override
protected int[] onCreateDrawableState(int extraSpace) {
final int[] drawableState = super.onCreateDrawableState(extraSpace + 1);
if (isChecked()) {
mergeDrawableStates(drawableState, CHECKED_STATE_SET);
}
return drawableState;
}
@Override
protected void drawableStateChanged() {
super.drawableStateChanged();
if (mButtonDrawable != null) {
int[] myDrawableState = getDrawableState();
// Set the state of the Drawable
mButtonDrawable.setState(myDrawableState);
invalidate();
}
}
@Override
public void drawableHotspotChanged(float x, float y) {
super.drawableHotspotChanged(x, y);
if (mButtonDrawable != null) {
mButtonDrawable.setHotspot(x, y);
}
}
@Override
protected boolean verifyDrawable(Drawable who) {
return super.verifyDrawable(who) || who == mButtonDrawable;
}
@Override
public void jumpDrawablesToCurrentState() {
super.jumpDrawablesToCurrentState();
if (mButtonDrawable != null) mButtonDrawable.jumpToCurrentState();
}
static class SavedState extends BaseSavedState {
boolean checked;
/**
* Constructor called from {@link CompoundButton#onSaveInstanceState()}
*/
SavedState(Parcelable superState) {
super(superState);
}
/**
* Constructor called from {@link #CREATOR}
*/
private SavedState(Parcel in) {
super(in);
checked = (Boolean)in.readValue(null);
}
@Override
public void writeToParcel(Parcel out, int flags) {
super.writeToParcel(out, flags);
out.writeValue(checked);
}
@Override
public String toString() {
return "CompoundButton.SavedState{"
+ Integer.toHexString(System.identityHashCode(this))
+ " checked=" + checked + "}";
}
public static final Parcelable.Creator