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    @Override
328    public void onInitializeAccessibilityEvent(AccessibilityEvent event) {
329        super.onInitializeAccessibilityEvent(event);
330        event.setClassName(CompoundButton.class.getName());
331        event.setChecked(mChecked);
332    }
333
334    @Override
335    public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) {
336        super.onInitializeAccessibilityNodeInfo(info);
337        info.setClassName(CompoundButton.class.getName());
338        info.setCheckable(true);
339        info.setChecked(mChecked);
340    }
341
342    @Override
343    public int getCompoundPaddingLeft() {
344        int padding = super.getCompoundPaddingLeft();
345        if (!isLayoutRtl()) {
346            final Drawable buttonDrawable = mButtonDrawable;
347            if (buttonDrawable != null) {
348                padding += buttonDrawable.getIntrinsicWidth();
349            }
350        }
351        return padding;
352    }
353
354    @Override
355    public int getCompoundPaddingRight() {
356        int padding = super.getCompoundPaddingRight();
357        if (isLayoutRtl()) {
358            final Drawable buttonDrawable = mButtonDrawable;
359            if (buttonDrawable != null) {
360                padding += buttonDrawable.getIntrinsicWidth();
361            }
362        }
363        return padding;
364    }
365
366    /**
367     * @hide
368     */
369    @Override
370    public int getHorizontalOffsetForDrawables() {
371        final Drawable buttonDrawable = mButtonDrawable;
372        return (buttonDrawable != null) ? buttonDrawable.getIntrinsicWidth() : 0;
373    }
374
375    @Override
376    protected void onDraw(Canvas canvas) {
377        final Drawable buttonDrawable = mButtonDrawable;
378        if (buttonDrawable != null) {
379            final int verticalGravity = getGravity() & Gravity.VERTICAL_GRAVITY_MASK;
380            final int drawableHeight = buttonDrawable.getIntrinsicHeight();
381            final int drawableWidth = buttonDrawable.getIntrinsicWidth();
382
383            final int top;
384            switch (verticalGravity) {
385                case Gravity.BOTTOM:
386                    top = getHeight() - drawableHeight;
387                    break;
388                case Gravity.CENTER_VERTICAL:
389                    top = (getHeight() - drawableHeight) / 2;
390                    break;
391                default:
392                    top = 0;
393            }
394            final int bottom = top + drawableHeight;
395            final int left = isLayoutRtl() ? getWidth() - drawableWidth : 0;
396            final int right = isLayoutRtl() ? getWidth() : drawableWidth;
397
398            buttonDrawable.setBounds(left, top, right, bottom);
399
400            final Drawable background = getBackground();
401            if (background != null) {
402                background.setHotspotBounds(left, top, right, bottom);
403            }
404        }
405
406        super.onDraw(canvas);
407
408        if (buttonDrawable != null) {
409            final int scrollX = mScrollX;
410            final int scrollY = mScrollY;
411            if (scrollX == 0 && scrollY == 0) {
412                buttonDrawable.draw(canvas);
413            } else {
414                canvas.translate(scrollX, scrollY);
415                buttonDrawable.draw(canvas);
416                canvas.translate(-scrollX, -scrollY);
417            }
418        }
419    }
420
421    @Override
422    protected int[] onCreateDrawableState(int extraSpace) {
423        final int[] drawableState = super.onCreateDrawableState(extraSpace + 1);
424        if (isChecked()) {
425            mergeDrawableStates(drawableState, CHECKED_STATE_SET);
426        }
427        return drawableState;
428    }
429
430    @Override
431    protected void drawableStateChanged() {
432        super.drawableStateChanged();
433
434        if (mButtonDrawable != null) {
435            int[] myDrawableState = getDrawableState();
436
437            // Set the state of the Drawable
438            mButtonDrawable.setState(myDrawableState);
439
440            invalidate();
441        }
442    }
443
444    @Override
445    public void drawableHotspotChanged(float x, float y) {
446        super.drawableHotspotChanged(x, y);
447
448        if (mButtonDrawable != null) {
449            mButtonDrawable.setHotspot(x, y);
450        }
451    }
452
453    @Override
454    protected boolean verifyDrawable(Drawable who) {
455        return super.verifyDrawable(who) || who == mButtonDrawable;
456    }
457
458    @Override
459    public void jumpDrawablesToCurrentState() {
460        super.jumpDrawablesToCurrentState();
461        if (mButtonDrawable != null) mButtonDrawable.jumpToCurrentState();
462    }
463
464    static class SavedState extends BaseSavedState {
465        boolean checked;
466
467        /**
468         * Constructor called from {@link CompoundButton#onSaveInstanceState()}
469         */
470        SavedState(Parcelable superState) {
471            super(superState);
472        }
473
474        /**
475         * Constructor called from {@link #CREATOR}
476         */
477        private SavedState(Parcel in) {
478            super(in);
479            checked = (Boolean)in.readValue(null);
480        }
481
482        @Override
483        public void writeToParcel(Parcel out, int flags) {
484            super.writeToParcel(out, flags);
485            out.writeValue(checked);
486        }
487
488        @Override
489        public String toString() {
490            return "CompoundButton.SavedState{"
491                    + Integer.toHexString(System.identityHashCode(this))
492                    + " checked=" + checked + "}";
493        }
494
495        public static final Parcelable.Creator<SavedState> CREATOR
496                = new Parcelable.Creator<SavedState>() {
497            public SavedState createFromParcel(Parcel in) {
498                return new SavedState(in);
499            }
500
501            public SavedState[] newArray(int size) {
502                return new SavedState[size];
503            }
504        };
505    }
506
507    @Override
508    public Parcelable onSaveInstanceState() {
509        Parcelable superState = super.onSaveInstanceState();
510
511        SavedState ss = new SavedState(superState);
512
513        ss.checked = isChecked();
514        return ss;
515    }
516
517    @Override
518    public void onRestoreInstanceState(Parcelable state) {
519        SavedState ss = (SavedState) state;
520
521        super.onRestoreInstanceState(ss.getSuperState());
522        setChecked(ss.checked);
523        requestLayout();
524    }
525}
526