CompoundButton.java revision 94a6d15ede149189bba9e5f474ed853c98230e75
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.graphics.PorterDuff;
23import android.view.ViewHierarchyEncoder;
24import com.android.internal.R;
25
26import android.content.Context;
27import android.content.res.ColorStateList;
28import android.content.res.TypedArray;
29import android.graphics.Canvas;
30import android.graphics.drawable.Drawable;
31import android.os.Parcel;
32import android.os.Parcelable;
33import android.util.AttributeSet;
34import android.view.Gravity;
35import android.view.SoundEffectConstants;
36import android.view.ViewDebug;
37import android.view.accessibility.AccessibilityEvent;
38import android.view.accessibility.AccessibilityNodeInfo;
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(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    @Nullable
225    public void setButtonDrawable(@Nullable Drawable drawable) {
226        if (mButtonDrawable != drawable) {
227            if (mButtonDrawable != null) {
228                mButtonDrawable.setCallback(null);
229                unscheduleDrawable(mButtonDrawable);
230            }
231
232            mButtonDrawable = drawable;
233
234            if (drawable != null) {
235                drawable.setCallback(this);
236                drawable.setLayoutDirection(getLayoutDirection());
237                if (drawable.isStateful()) {
238                    drawable.setState(getDrawableState());
239                }
240                drawable.setVisible(getVisibility() == VISIBLE, false);
241                setMinHeight(drawable.getIntrinsicHeight());
242                applyButtonTint();
243            }
244        }
245    }
246
247    /**
248     * @return the drawable used as the compound button image
249     * @see #setButtonDrawable(Drawable)
250     * @see #setButtonDrawable(int)
251     */
252    @Nullable
253    public Drawable getButtonDrawable() {
254        return mButtonDrawable;
255    }
256
257    /**
258     * Applies a tint to the button drawable. Does not modify the current tint
259     * mode, which is {@link PorterDuff.Mode#SRC_IN} by default.
260     * <p>
261     * Subsequent calls to {@link #setButtonDrawable(Drawable)} will
262     * automatically mutate the drawable and apply the specified tint and tint
263     * mode using
264     * {@link Drawable#setTintList(ColorStateList)}.
265     *
266     * @param tint the tint to apply, may be {@code null} to clear tint
267     *
268     * @attr ref android.R.styleable#CompoundButton_buttonTint
269     * @see #setButtonTintList(ColorStateList)
270     * @see Drawable#setTintList(ColorStateList)
271     */
272    public void setButtonTintList(@Nullable ColorStateList tint) {
273        mButtonTintList = tint;
274        mHasButtonTint = true;
275
276        applyButtonTint();
277    }
278
279    /**
280     * @return the tint applied to the button drawable
281     * @attr ref android.R.styleable#CompoundButton_buttonTint
282     * @see #setButtonTintList(ColorStateList)
283     */
284    @Nullable
285    public ColorStateList getButtonTintList() {
286        return mButtonTintList;
287    }
288
289    /**
290     * Specifies the blending mode used to apply the tint specified by
291     * {@link #setButtonTintList(ColorStateList)}} to the button drawable. The
292     * default mode is {@link PorterDuff.Mode#SRC_IN}.
293     *
294     * @param tintMode the blending mode used to apply the tint, may be
295     *                 {@code null} to clear tint
296     * @attr ref android.R.styleable#CompoundButton_buttonTintMode
297     * @see #getButtonTintMode()
298     * @see Drawable#setTintMode(PorterDuff.Mode)
299     */
300    public void setButtonTintMode(@Nullable PorterDuff.Mode tintMode) {
301        mButtonTintMode = tintMode;
302        mHasButtonTintMode = true;
303
304        applyButtonTint();
305    }
306
307    /**
308     * @return the blending mode used to apply the tint to the button drawable
309     * @attr ref android.R.styleable#CompoundButton_buttonTintMode
310     * @see #setButtonTintMode(PorterDuff.Mode)
311     */
312    @Nullable
313    public PorterDuff.Mode getButtonTintMode() {
314        return mButtonTintMode;
315    }
316
317    private void applyButtonTint() {
318        if (mButtonDrawable != null && (mHasButtonTint || mHasButtonTintMode)) {
319            mButtonDrawable = mButtonDrawable.mutate();
320
321            if (mHasButtonTint) {
322                mButtonDrawable.setTintList(mButtonTintList);
323            }
324
325            if (mHasButtonTintMode) {
326                mButtonDrawable.setTintMode(mButtonTintMode);
327            }
328
329            // The drawable (or one of its children) may not have been
330            // stateful before applying the tint, so let's try again.
331            if (mButtonDrawable.isStateful()) {
332                mButtonDrawable.setState(getDrawableState());
333            }
334        }
335    }
336
337    @Override
338    public CharSequence getAccessibilityClassName() {
339        return CompoundButton.class.getName();
340    }
341
342    /** @hide */
343    @Override
344    public void onInitializeAccessibilityEventInternal(AccessibilityEvent event) {
345        super.onInitializeAccessibilityEventInternal(event);
346        event.setChecked(mChecked);
347    }
348
349    /** @hide */
350    @Override
351    public void onInitializeAccessibilityNodeInfoInternal(AccessibilityNodeInfo info) {
352        super.onInitializeAccessibilityNodeInfoInternal(info);
353        info.setCheckable(true);
354        info.setChecked(mChecked);
355    }
356
357    @Override
358    public int getCompoundPaddingLeft() {
359        int padding = super.getCompoundPaddingLeft();
360        if (!isLayoutRtl()) {
361            final Drawable buttonDrawable = mButtonDrawable;
362            if (buttonDrawable != null) {
363                padding += buttonDrawable.getIntrinsicWidth();
364            }
365        }
366        return padding;
367    }
368
369    @Override
370    public int getCompoundPaddingRight() {
371        int padding = super.getCompoundPaddingRight();
372        if (isLayoutRtl()) {
373            final Drawable buttonDrawable = mButtonDrawable;
374            if (buttonDrawable != null) {
375                padding += buttonDrawable.getIntrinsicWidth();
376            }
377        }
378        return padding;
379    }
380
381    /**
382     * @hide
383     */
384    @Override
385    public int getHorizontalOffsetForDrawables() {
386        final Drawable buttonDrawable = mButtonDrawable;
387        return (buttonDrawable != null) ? buttonDrawable.getIntrinsicWidth() : 0;
388    }
389
390    @Override
391    protected void onDraw(Canvas canvas) {
392        final Drawable buttonDrawable = mButtonDrawable;
393        if (buttonDrawable != null) {
394            final int verticalGravity = getGravity() & Gravity.VERTICAL_GRAVITY_MASK;
395            final int drawableHeight = buttonDrawable.getIntrinsicHeight();
396            final int drawableWidth = buttonDrawable.getIntrinsicWidth();
397
398            final int top;
399            switch (verticalGravity) {
400                case Gravity.BOTTOM:
401                    top = getHeight() - drawableHeight;
402                    break;
403                case Gravity.CENTER_VERTICAL:
404                    top = (getHeight() - drawableHeight) / 2;
405                    break;
406                default:
407                    top = 0;
408            }
409            final int bottom = top + drawableHeight;
410            final int left = isLayoutRtl() ? getWidth() - drawableWidth : 0;
411            final int right = isLayoutRtl() ? getWidth() : drawableWidth;
412
413            buttonDrawable.setBounds(left, top, right, bottom);
414
415            final Drawable background = getBackground();
416            if (background != null) {
417                background.setHotspotBounds(left, top, right, bottom);
418            }
419        }
420
421        super.onDraw(canvas);
422
423        if (buttonDrawable != null) {
424            final int scrollX = mScrollX;
425            final int scrollY = mScrollY;
426            if (scrollX == 0 && scrollY == 0) {
427                buttonDrawable.draw(canvas);
428            } else {
429                canvas.translate(scrollX, scrollY);
430                buttonDrawable.draw(canvas);
431                canvas.translate(-scrollX, -scrollY);
432            }
433        }
434    }
435
436    @Override
437    protected int[] onCreateDrawableState(int extraSpace) {
438        final int[] drawableState = super.onCreateDrawableState(extraSpace + 1);
439        if (isChecked()) {
440            mergeDrawableStates(drawableState, CHECKED_STATE_SET);
441        }
442        return drawableState;
443    }
444
445    @Override
446    protected void drawableStateChanged() {
447        super.drawableStateChanged();
448
449        if (mButtonDrawable != null) {
450            int[] myDrawableState = getDrawableState();
451
452            // Set the state of the Drawable
453            mButtonDrawable.setState(myDrawableState);
454
455            invalidate();
456        }
457    }
458
459    @Override
460    public void drawableHotspotChanged(float x, float y) {
461        super.drawableHotspotChanged(x, y);
462
463        if (mButtonDrawable != null) {
464            mButtonDrawable.setHotspot(x, y);
465        }
466    }
467
468    @Override
469    protected boolean verifyDrawable(Drawable who) {
470        return super.verifyDrawable(who) || who == mButtonDrawable;
471    }
472
473    @Override
474    public void jumpDrawablesToCurrentState() {
475        super.jumpDrawablesToCurrentState();
476        if (mButtonDrawable != null) mButtonDrawable.jumpToCurrentState();
477    }
478
479    static class SavedState extends BaseSavedState {
480        boolean checked;
481
482        /**
483         * Constructor called from {@link CompoundButton#onSaveInstanceState()}
484         */
485        SavedState(Parcelable superState) {
486            super(superState);
487        }
488
489        /**
490         * Constructor called from {@link #CREATOR}
491         */
492        private SavedState(Parcel in) {
493            super(in);
494            checked = (Boolean)in.readValue(null);
495        }
496
497        @Override
498        public void writeToParcel(Parcel out, int flags) {
499            super.writeToParcel(out, flags);
500            out.writeValue(checked);
501        }
502
503        @Override
504        public String toString() {
505            return "CompoundButton.SavedState{"
506                    + Integer.toHexString(System.identityHashCode(this))
507                    + " checked=" + checked + "}";
508        }
509
510        public static final Parcelable.Creator<SavedState> CREATOR
511                = new Parcelable.Creator<SavedState>() {
512            public SavedState createFromParcel(Parcel in) {
513                return new SavedState(in);
514            }
515
516            public SavedState[] newArray(int size) {
517                return new SavedState[size];
518            }
519        };
520    }
521
522    @Override
523    public Parcelable onSaveInstanceState() {
524        Parcelable superState = super.onSaveInstanceState();
525
526        SavedState ss = new SavedState(superState);
527
528        ss.checked = isChecked();
529        return ss;
530    }
531
532    @Override
533    public void onRestoreInstanceState(Parcelable state) {
534        SavedState ss = (SavedState) state;
535
536        super.onRestoreInstanceState(ss.getSuperState());
537        setChecked(ss.checked);
538        requestLayout();
539    }
540
541    /** @hide */
542    @Override
543    protected void encodeProperties(@NonNull ViewHierarchyEncoder stream) {
544        super.encodeProperties(stream);
545        stream.addProperty("checked", isChecked());
546    }
547}
548