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