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 
17 package android.widget;
18 
19 import android.annotation.DrawableRes;
20 import android.annotation.NonNull;
21 import android.annotation.Nullable;
22 import android.graphics.PorterDuff;
23 import android.view.ViewHierarchyEncoder;
24 import com.android.internal.R;
25 
26 import android.content.Context;
27 import android.content.res.ColorStateList;
28 import android.content.res.TypedArray;
29 import android.graphics.Canvas;
30 import android.graphics.drawable.Drawable;
31 import android.os.Parcel;
32 import android.os.Parcelable;
33 import android.util.AttributeSet;
34 import android.view.Gravity;
35 import android.view.SoundEffectConstants;
36 import android.view.ViewDebug;
37 import android.view.accessibility.AccessibilityEvent;
38 import 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  */
54 public 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 
CompoundButton(Context context)71     public CompoundButton(Context context) {
72         this(context, null);
73     }
74 
CompoundButton(Context context, AttributeSet attrs)75     public CompoundButton(Context context, AttributeSet attrs) {
76         this(context, attrs, 0);
77     }
78 
CompoundButton(Context context, AttributeSet attrs, int defStyleAttr)79     public CompoundButton(Context context, AttributeSet attrs, int defStyleAttr) {
80         this(context, attrs, defStyleAttr, 0);
81     }
82 
CompoundButton(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes)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 
toggle()114     public void toggle() {
115         setChecked(!mChecked);
116     }
117 
118     @Override
performClick()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
isChecked()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      */
setChecked(boolean checked)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      */
setOnCheckedChangeListener(OnCheckedChangeListener listener)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      */
setOnCheckedChangeWidgetListener(OnCheckedChangeListener listener)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          */
onCheckedChanged(CompoundButton buttonView, boolean isChecked)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      */
setButtonDrawable(@rawableRes int resId)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
setButtonDrawable(@ullable Drawable drawable)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
onResolveDrawables(@esolvedLayoutDir int layoutDirection)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
getButtonDrawable()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      */
setButtonTintList(@ullable ColorStateList tint)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
getButtonTintList()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      */
setButtonTintMode(@ullable PorterDuff.Mode tintMode)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
getButtonTintMode()324     public PorterDuff.Mode getButtonTintMode() {
325         return mButtonTintMode;
326     }
327 
applyButtonTint()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
getAccessibilityClassName()349     public CharSequence getAccessibilityClassName() {
350         return CompoundButton.class.getName();
351     }
352 
353     /** @hide */
354     @Override
onInitializeAccessibilityEventInternal(AccessibilityEvent event)355     public void onInitializeAccessibilityEventInternal(AccessibilityEvent event) {
356         super.onInitializeAccessibilityEventInternal(event);
357         event.setChecked(mChecked);
358     }
359 
360     /** @hide */
361     @Override
onInitializeAccessibilityNodeInfoInternal(AccessibilityNodeInfo info)362     public void onInitializeAccessibilityNodeInfoInternal(AccessibilityNodeInfo info) {
363         super.onInitializeAccessibilityNodeInfoInternal(info);
364         info.setCheckable(true);
365         info.setChecked(mChecked);
366     }
367 
368     @Override
getCompoundPaddingLeft()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
getCompoundPaddingRight()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
getHorizontalOffsetForDrawables()396     public int getHorizontalOffsetForDrawables() {
397         final Drawable buttonDrawable = mButtonDrawable;
398         return (buttonDrawable != null) ? buttonDrawable.getIntrinsicWidth() : 0;
399     }
400 
401     @Override
onDraw(Canvas canvas)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
onCreateDrawableState(int extraSpace)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
drawableStateChanged()457     protected void drawableStateChanged() {
458         super.drawableStateChanged();
459 
460         if (mButtonDrawable != null) {
461             int[] myDrawableState = getDrawableState();
462 
463             // Set the state of the Drawable
464             mButtonDrawable.setState(myDrawableState);
465 
466             invalidate();
467         }
468     }
469 
470     @Override
drawableHotspotChanged(float x, float y)471     public void drawableHotspotChanged(float x, float y) {
472         super.drawableHotspotChanged(x, y);
473 
474         if (mButtonDrawable != null) {
475             mButtonDrawable.setHotspot(x, y);
476         }
477     }
478 
479     @Override
verifyDrawable(Drawable who)480     protected boolean verifyDrawable(Drawable who) {
481         return super.verifyDrawable(who) || who == mButtonDrawable;
482     }
483 
484     @Override
jumpDrawablesToCurrentState()485     public void jumpDrawablesToCurrentState() {
486         super.jumpDrawablesToCurrentState();
487         if (mButtonDrawable != null) mButtonDrawable.jumpToCurrentState();
488     }
489 
490     static class SavedState extends BaseSavedState {
491         boolean checked;
492 
493         /**
494          * Constructor called from {@link CompoundButton#onSaveInstanceState()}
495          */
SavedState(Parcelable superState)496         SavedState(Parcelable superState) {
497             super(superState);
498         }
499 
500         /**
501          * Constructor called from {@link #CREATOR}
502          */
SavedState(Parcel in)503         private SavedState(Parcel in) {
504             super(in);
505             checked = (Boolean)in.readValue(null);
506         }
507 
508         @Override
writeToParcel(Parcel out, int flags)509         public void writeToParcel(Parcel out, int flags) {
510             super.writeToParcel(out, flags);
511             out.writeValue(checked);
512         }
513 
514         @Override
toString()515         public String toString() {
516             return "CompoundButton.SavedState{"
517                     + Integer.toHexString(System.identityHashCode(this))
518                     + " checked=" + checked + "}";
519         }
520 
521         public static final Parcelable.Creator<SavedState> CREATOR
522                 = new Parcelable.Creator<SavedState>() {
523             public SavedState createFromParcel(Parcel in) {
524                 return new SavedState(in);
525             }
526 
527             public SavedState[] newArray(int size) {
528                 return new SavedState[size];
529             }
530         };
531     }
532 
533     @Override
onSaveInstanceState()534     public Parcelable onSaveInstanceState() {
535         Parcelable superState = super.onSaveInstanceState();
536 
537         SavedState ss = new SavedState(superState);
538 
539         ss.checked = isChecked();
540         return ss;
541     }
542 
543     @Override
onRestoreInstanceState(Parcelable state)544     public void onRestoreInstanceState(Parcelable state) {
545         SavedState ss = (SavedState) state;
546 
547         super.onRestoreInstanceState(ss.getSuperState());
548         setChecked(ss.checked);
549         requestLayout();
550     }
551 
552     /** @hide */
553     @Override
encodeProperties(@onNull ViewHierarchyEncoder stream)554     protected void encodeProperties(@NonNull ViewHierarchyEncoder stream) {
555         super.encodeProperties(stream);
556         stream.addProperty("checked", isChecked());
557     }
558 }
559