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