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