CompoundButton.java revision d8a6c59d3df483536c3b3b538297cb0ef5336976
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 defStyleAttr) {
68        this(context, attrs, defStyleAttr, 0);
69    }
70
71    public CompoundButton(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
72        super(context, attrs, defStyleAttr, defStyleRes);
73
74        final TypedArray a = context.obtainStyledAttributes(
75                attrs, com.android.internal.R.styleable.CompoundButton, defStyleAttr, defStyleRes);
76
77        Drawable d = a.getDrawable(com.android.internal.R.styleable.CompoundButton_button);
78        if (d != null) {
79            setButtonDrawable(d);
80        }
81
82        boolean checked = a
83                .getBoolean(com.android.internal.R.styleable.CompoundButton_checked, false);
84        setChecked(checked);
85
86        a.recycle();
87    }
88
89    public void toggle() {
90        setChecked(!mChecked);
91    }
92
93    @Override
94    public boolean performClick() {
95        /*
96         * XXX: These are tiny, need some surrounding 'expanded touch area',
97         * which will need to be implemented in Button if we only override
98         * performClick()
99         */
100
101        /* When clicked, toggle the state */
102        toggle();
103        return super.performClick();
104    }
105
106    @ViewDebug.ExportedProperty
107    public boolean isChecked() {
108        return mChecked;
109    }
110
111    /**
112     * <p>Changes the checked state of this button.</p>
113     *
114     * @param checked true to check the button, false to uncheck it
115     */
116    public void setChecked(boolean checked) {
117        if (mChecked != checked) {
118            mChecked = checked;
119            refreshDrawableState();
120            notifyViewAccessibilityStateChangedIfNeeded(
121                    AccessibilityEvent.CONTENT_CHANGE_TYPE_UNDEFINED);
122
123            // Avoid infinite recursions if setChecked() is called from a listener
124            if (mBroadcasting) {
125                return;
126            }
127
128            mBroadcasting = true;
129            if (mOnCheckedChangeListener != null) {
130                mOnCheckedChangeListener.onCheckedChanged(this, mChecked);
131            }
132            if (mOnCheckedChangeWidgetListener != null) {
133                mOnCheckedChangeWidgetListener.onCheckedChanged(this, mChecked);
134            }
135
136            mBroadcasting = false;
137        }
138    }
139
140    /**
141     * Register a callback to be invoked when the checked state of this button
142     * changes.
143     *
144     * @param listener the callback to call on checked state change
145     */
146    public void setOnCheckedChangeListener(OnCheckedChangeListener listener) {
147        mOnCheckedChangeListener = listener;
148    }
149
150    /**
151     * Register a callback to be invoked when the checked state of this button
152     * changes. This callback is used for internal purpose only.
153     *
154     * @param listener the callback to call on checked state change
155     * @hide
156     */
157    void setOnCheckedChangeWidgetListener(OnCheckedChangeListener listener) {
158        mOnCheckedChangeWidgetListener = listener;
159    }
160
161    /**
162     * Interface definition for a callback to be invoked when the checked state
163     * of a compound button changed.
164     */
165    public static interface OnCheckedChangeListener {
166        /**
167         * Called when the checked state of a compound button has changed.
168         *
169         * @param buttonView The compound button view whose state has changed.
170         * @param isChecked  The new checked state of buttonView.
171         */
172        void onCheckedChanged(CompoundButton buttonView, boolean isChecked);
173    }
174
175    /**
176     * Set the background to a given Drawable, identified by its resource id.
177     *
178     * @param resid the resource id of the drawable to use as the background
179     */
180    public void setButtonDrawable(int resid) {
181        if (resid != 0 && resid == mButtonResource) {
182            return;
183        }
184
185        mButtonResource = resid;
186
187        Drawable d = null;
188        if (mButtonResource != 0) {
189            d = getResources().getDrawable(mButtonResource);
190        }
191        setButtonDrawable(d);
192    }
193
194    /**
195     * Set the background to a given Drawable
196     *
197     * @param d The Drawable to use as the background
198     */
199    public void setButtonDrawable(Drawable d) {
200        if (d != null) {
201            if (mButtonDrawable != null) {
202                mButtonDrawable.setCallback(null);
203                unscheduleDrawable(mButtonDrawable);
204            }
205            d.setCallback(this);
206            d.setVisible(getVisibility() == VISIBLE, false);
207            mButtonDrawable = d;
208            setMinHeight(mButtonDrawable.getIntrinsicHeight());
209        }
210
211        refreshDrawableState();
212    }
213
214    @Override
215    public void onInitializeAccessibilityEvent(AccessibilityEvent event) {
216        super.onInitializeAccessibilityEvent(event);
217        event.setClassName(CompoundButton.class.getName());
218        event.setChecked(mChecked);
219    }
220
221    @Override
222    public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) {
223        super.onInitializeAccessibilityNodeInfo(info);
224        info.setClassName(CompoundButton.class.getName());
225        info.setCheckable(true);
226        info.setChecked(mChecked);
227    }
228
229    @Override
230    public int getCompoundPaddingLeft() {
231        int padding = super.getCompoundPaddingLeft();
232        if (!isLayoutRtl()) {
233            final Drawable buttonDrawable = mButtonDrawable;
234            if (buttonDrawable != null) {
235                padding += buttonDrawable.getIntrinsicWidth();
236            }
237        }
238        return padding;
239    }
240
241    @Override
242    public int getCompoundPaddingRight() {
243        int padding = super.getCompoundPaddingRight();
244        if (isLayoutRtl()) {
245            final Drawable buttonDrawable = mButtonDrawable;
246            if (buttonDrawable != null) {
247                padding += buttonDrawable.getIntrinsicWidth();
248            }
249        }
250        return padding;
251    }
252
253    /**
254     * @hide
255     */
256    @Override
257    public int getHorizontalOffsetForDrawables() {
258        final Drawable buttonDrawable = mButtonDrawable;
259        return (buttonDrawable != null) ? buttonDrawable.getIntrinsicWidth() : 0;
260    }
261
262    @Override
263    protected void onDraw(Canvas canvas) {
264        super.onDraw(canvas);
265
266        final Drawable buttonDrawable = mButtonDrawable;
267        if (buttonDrawable != null) {
268            final int verticalGravity = getGravity() & Gravity.VERTICAL_GRAVITY_MASK;
269            final int drawableHeight = buttonDrawable.getIntrinsicHeight();
270            final int drawableWidth = buttonDrawable.getIntrinsicWidth();
271
272            int top = 0;
273            switch (verticalGravity) {
274                case Gravity.BOTTOM:
275                    top = getHeight() - drawableHeight;
276                    break;
277                case Gravity.CENTER_VERTICAL:
278                    top = (getHeight() - drawableHeight) / 2;
279                    break;
280            }
281            int bottom = top + drawableHeight;
282            int left = isLayoutRtl() ? getWidth() - drawableWidth : 0;
283            int right = isLayoutRtl() ? getWidth() : drawableWidth;
284
285            buttonDrawable.setBounds(left, top, right, bottom);
286            buttonDrawable.draw(canvas);
287        }
288    }
289
290    @Override
291    protected int[] onCreateDrawableState(int extraSpace) {
292        final int[] drawableState = super.onCreateDrawableState(extraSpace + 1);
293        if (isChecked()) {
294            mergeDrawableStates(drawableState, CHECKED_STATE_SET);
295        }
296        return drawableState;
297    }
298
299    @Override
300    protected void drawableStateChanged() {
301        super.drawableStateChanged();
302
303        if (mButtonDrawable != null) {
304            int[] myDrawableState = getDrawableState();
305
306            // Set the state of the Drawable
307            mButtonDrawable.setState(myDrawableState);
308
309            invalidate();
310        }
311    }
312
313    @Override
314    protected boolean verifyDrawable(Drawable who) {
315        return super.verifyDrawable(who) || who == mButtonDrawable;
316    }
317
318    @Override
319    public void jumpDrawablesToCurrentState() {
320        super.jumpDrawablesToCurrentState();
321        if (mButtonDrawable != null) mButtonDrawable.jumpToCurrentState();
322    }
323
324    static class SavedState extends BaseSavedState {
325        boolean checked;
326
327        /**
328         * Constructor called from {@link CompoundButton#onSaveInstanceState()}
329         */
330        SavedState(Parcelable superState) {
331            super(superState);
332        }
333
334        /**
335         * Constructor called from {@link #CREATOR}
336         */
337        private SavedState(Parcel in) {
338            super(in);
339            checked = (Boolean)in.readValue(null);
340        }
341
342        @Override
343        public void writeToParcel(Parcel out, int flags) {
344            super.writeToParcel(out, flags);
345            out.writeValue(checked);
346        }
347
348        @Override
349        public String toString() {
350            return "CompoundButton.SavedState{"
351                    + Integer.toHexString(System.identityHashCode(this))
352                    + " checked=" + checked + "}";
353        }
354
355        public static final Parcelable.Creator<SavedState> CREATOR
356                = new Parcelable.Creator<SavedState>() {
357            public SavedState createFromParcel(Parcel in) {
358                return new SavedState(in);
359            }
360
361            public SavedState[] newArray(int size) {
362                return new SavedState[size];
363            }
364        };
365    }
366
367    @Override
368    public Parcelable onSaveInstanceState() {
369        Parcelable superState = super.onSaveInstanceState();
370
371        SavedState ss = new SavedState(superState);
372
373        ss.checked = isChecked();
374        return ss;
375    }
376
377    @Override
378    public void onRestoreInstanceState(Parcelable state) {
379        SavedState ss = (SavedState) state;
380
381        super.onRestoreInstanceState(ss.getSuperState());
382        setChecked(ss.checked);
383        requestLayout();
384    }
385}
386