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