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