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