CompoundButton.java revision 8a78fd4d9572dff95432fcc4ba0e87563415b728
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 com.android.internal.R;
20
21import android.content.Context;
22import android.content.res.TypedArray;
23import android.graphics.Canvas;
24import android.graphics.drawable.Drawable;
25import android.os.Parcel;
26import android.os.Parcelable;
27import android.util.AttributeSet;
28import android.view.Gravity;
29import android.view.ViewDebug;
30import android.view.accessibility.AccessibilityEvent;
31import android.view.accessibility.AccessibilityNodeInfo;
32
33/**
34 * <p>
35 * A button with two states, checked and unchecked. When the button is pressed
36 * or clicked, the state changes automatically.
37 * </p>
38 *
39 * <p><strong>XML attributes</strong></p>
40 * <p>
41 * See {@link android.R.styleable#CompoundButton
42 * CompoundButton Attributes}, {@link android.R.styleable#Button Button
43 * Attributes}, {@link android.R.styleable#TextView TextView Attributes}, {@link
44 * android.R.styleable#View View Attributes}
45 * </p>
46 */
47public abstract class CompoundButton extends Button implements Checkable {
48    private boolean mChecked;
49    private int mButtonResource;
50    private boolean mBroadcasting;
51    private Drawable mButtonDrawable;
52    private OnCheckedChangeListener mOnCheckedChangeListener;
53    private OnCheckedChangeListener mOnCheckedChangeWidgetListener;
54
55    private static final int[] CHECKED_STATE_SET = {
56        R.attr.state_checked
57    };
58
59    public CompoundButton(Context context) {
60        this(context, null);
61    }
62
63    public CompoundButton(Context context, AttributeSet attrs) {
64        this(context, attrs, 0);
65    }
66
67    public CompoundButton(Context context, AttributeSet attrs, int defStyle) {
68        super(context, attrs, defStyle);
69
70        TypedArray a =
71                context.obtainStyledAttributes(
72                        attrs, com.android.internal.R.styleable.CompoundButton, defStyle, 0);
73
74        Drawable d = a.getDrawable(com.android.internal.R.styleable.CompoundButton_button);
75        if (d != null) {
76            setButtonDrawable(d);
77        }
78
79        boolean checked = a
80                .getBoolean(com.android.internal.R.styleable.CompoundButton_checked, false);
81        setChecked(checked);
82
83        a.recycle();
84    }
85
86    public void toggle() {
87        setChecked(!mChecked);
88    }
89
90    @Override
91    public boolean performClick() {
92        /*
93         * XXX: These are tiny, need some surrounding 'expanded touch area',
94         * which will need to be implemented in Button if we only override
95         * performClick()
96         */
97
98        /* When clicked, toggle the state */
99        toggle();
100        return super.performClick();
101    }
102
103    @ViewDebug.ExportedProperty
104    public boolean isChecked() {
105        return mChecked;
106    }
107
108    /**
109     * <p>Changes the checked state of this button.</p>
110     *
111     * @param checked true to check the button, false to uncheck it
112     */
113    public void setChecked(boolean checked) {
114        if (mChecked != checked) {
115            mChecked = checked;
116            refreshDrawableState();
117
118            // Avoid infinite recursions if setChecked() is called from a listener
119            if (mBroadcasting) {
120                return;
121            }
122
123            mBroadcasting = true;
124            if (mOnCheckedChangeListener != null) {
125                mOnCheckedChangeListener.onCheckedChanged(this, mChecked);
126            }
127            if (mOnCheckedChangeWidgetListener != null) {
128                mOnCheckedChangeWidgetListener.onCheckedChanged(this, mChecked);
129            }
130
131            mBroadcasting = false;
132        }
133    }
134
135    /**
136     * Register a callback to be invoked when the checked state of this button
137     * changes.
138     *
139     * @param listener the callback to call on checked state change
140     */
141    public void setOnCheckedChangeListener(OnCheckedChangeListener listener) {
142        mOnCheckedChangeListener = listener;
143    }
144
145    /**
146     * Register a callback to be invoked when the checked state of this button
147     * changes. This callback is used for internal purpose only.
148     *
149     * @param listener the callback to call on checked state change
150     * @hide
151     */
152    void setOnCheckedChangeWidgetListener(OnCheckedChangeListener listener) {
153        mOnCheckedChangeWidgetListener = listener;
154    }
155
156    /**
157     * Interface definition for a callback to be invoked when the checked state
158     * of a compound button changed.
159     */
160    public static interface OnCheckedChangeListener {
161        /**
162         * Called when the checked state of a compound button has changed.
163         *
164         * @param buttonView The compound button view whose state has changed.
165         * @param isChecked  The new checked state of buttonView.
166         */
167        void onCheckedChanged(CompoundButton buttonView, boolean isChecked);
168    }
169
170    /**
171     * Set the background to a given Drawable, identified by its resource id.
172     *
173     * @param resid the resource id of the drawable to use as the background
174     */
175    public void setButtonDrawable(int resid) {
176        if (resid != 0 && resid == mButtonResource) {
177            return;
178        }
179
180        mButtonResource = resid;
181
182        Drawable d = null;
183        if (mButtonResource != 0) {
184            d = getResources().getDrawable(mButtonResource);
185        }
186        setButtonDrawable(d);
187    }
188
189    /**
190     * Set the background to a given Drawable
191     *
192     * @param d The Drawable to use as the background
193     */
194    public void setButtonDrawable(Drawable d) {
195        if (d != null) {
196            if (mButtonDrawable != null) {
197                mButtonDrawable.setCallback(null);
198                unscheduleDrawable(mButtonDrawable);
199            }
200            d.setCallback(this);
201            d.setState(getDrawableState());
202            d.setVisible(getVisibility() == VISIBLE, false);
203            mButtonDrawable = d;
204            mButtonDrawable.setState(null);
205            setMinHeight(mButtonDrawable.getIntrinsicHeight());
206        }
207
208        refreshDrawableState();
209    }
210
211    @Override
212    public void onInitializeAccessibilityEvent(AccessibilityEvent event) {
213        super.onInitializeAccessibilityEvent(event);
214        event.setClassName(CompoundButton.class.getName());
215        event.setChecked(mChecked);
216    }
217
218    @Override
219    public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) {
220        super.onInitializeAccessibilityNodeInfo(info);
221        info.setClassName(CompoundButton.class.getName());
222        info.setCheckable(true);
223        info.setChecked(mChecked);
224    }
225
226    @Override
227    protected void onDraw(Canvas canvas) {
228        super.onDraw(canvas);
229
230        final Drawable buttonDrawable = mButtonDrawable;
231        if (buttonDrawable != null) {
232            final int verticalGravity = getGravity() & Gravity.VERTICAL_GRAVITY_MASK;
233            final int height = buttonDrawable.getIntrinsicHeight();
234
235            int y = 0;
236
237            switch (verticalGravity) {
238                case Gravity.BOTTOM:
239                    y = getHeight() - height;
240                    break;
241                case Gravity.CENTER_VERTICAL:
242                    y = (getHeight() - height) / 2;
243                    break;
244            }
245
246            buttonDrawable.setBounds(0, y, buttonDrawable.getIntrinsicWidth(), y + height);
247            buttonDrawable.draw(canvas);
248        }
249    }
250
251    @Override
252    protected int[] onCreateDrawableState(int extraSpace) {
253        final int[] drawableState = super.onCreateDrawableState(extraSpace + 1);
254        if (isChecked()) {
255            mergeDrawableStates(drawableState, CHECKED_STATE_SET);
256        }
257        return drawableState;
258    }
259
260    @Override
261    protected void drawableStateChanged() {
262        super.drawableStateChanged();
263
264        if (mButtonDrawable != null) {
265            int[] myDrawableState = getDrawableState();
266
267            // Set the state of the Drawable
268            mButtonDrawable.setState(myDrawableState);
269
270            invalidate();
271        }
272    }
273
274    @Override
275    protected boolean verifyDrawable(Drawable who) {
276        return super.verifyDrawable(who) || who == mButtonDrawable;
277    }
278
279    @Override
280    public void jumpDrawablesToCurrentState() {
281        super.jumpDrawablesToCurrentState();
282        if (mButtonDrawable != null) mButtonDrawable.jumpToCurrentState();
283    }
284
285    static class SavedState extends BaseSavedState {
286        boolean checked;
287
288        /**
289         * Constructor called from {@link CompoundButton#onSaveInstanceState()}
290         */
291        SavedState(Parcelable superState) {
292            super(superState);
293        }
294
295        /**
296         * Constructor called from {@link #CREATOR}
297         */
298        private SavedState(Parcel in) {
299            super(in);
300            checked = (Boolean)in.readValue(null);
301        }
302
303        @Override
304        public void writeToParcel(Parcel out, int flags) {
305            super.writeToParcel(out, flags);
306            out.writeValue(checked);
307        }
308
309        @Override
310        public String toString() {
311            return "CompoundButton.SavedState{"
312                    + Integer.toHexString(System.identityHashCode(this))
313                    + " checked=" + checked + "}";
314        }
315
316        public static final Parcelable.Creator<SavedState> CREATOR
317                = new Parcelable.Creator<SavedState>() {
318            public SavedState createFromParcel(Parcel in) {
319                return new SavedState(in);
320            }
321
322            public SavedState[] newArray(int size) {
323                return new SavedState[size];
324            }
325        };
326    }
327
328    @Override
329    public Parcelable onSaveInstanceState() {
330        // Force our ancestor class to save its state
331        setFreezesText(true);
332        Parcelable superState = super.onSaveInstanceState();
333
334        SavedState ss = new SavedState(superState);
335
336        ss.checked = isChecked();
337        return ss;
338    }
339
340    @Override
341    public void onRestoreInstanceState(Parcelable state) {
342        SavedState ss = (SavedState) state;
343
344        super.onRestoreInstanceState(ss.getSuperState());
345        setChecked(ss.checked);
346        requestLayout();
347    }
348}
349