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            notifyViewAccessibilityStateChangedIfNeeded(
118                    AccessibilityEvent.CONTENT_CHANGE_TYPE_UNDEFINED);
119
120            // Avoid infinite recursions if setChecked() is called from a listener
121            if (mBroadcasting) {
122                return;
123            }
124
125            mBroadcasting = true;
126            if (mOnCheckedChangeListener != null) {
127                mOnCheckedChangeListener.onCheckedChanged(this, mChecked);
128            }
129            if (mOnCheckedChangeWidgetListener != null) {
130                mOnCheckedChangeWidgetListener.onCheckedChanged(this, mChecked);
131            }
132
133            mBroadcasting = false;
134        }
135    }
136
137    /**
138     * Register a callback to be invoked when the checked state of this button
139     * changes.
140     *
141     * @param listener the callback to call on checked state change
142     */
143    public void setOnCheckedChangeListener(OnCheckedChangeListener listener) {
144        mOnCheckedChangeListener = listener;
145    }
146
147    /**
148     * Register a callback to be invoked when the checked state of this button
149     * changes. This callback is used for internal purpose only.
150     *
151     * @param listener the callback to call on checked state change
152     * @hide
153     */
154    void setOnCheckedChangeWidgetListener(OnCheckedChangeListener listener) {
155        mOnCheckedChangeWidgetListener = listener;
156    }
157
158    /**
159     * Interface definition for a callback to be invoked when the checked state
160     * of a compound button changed.
161     */
162    public static interface OnCheckedChangeListener {
163        /**
164         * Called when the checked state of a compound button has changed.
165         *
166         * @param buttonView The compound button view whose state has changed.
167         * @param isChecked  The new checked state of buttonView.
168         */
169        void onCheckedChanged(CompoundButton buttonView, boolean isChecked);
170    }
171
172    /**
173     * Set the background to a given Drawable, identified by its resource id.
174     *
175     * @param resid the resource id of the drawable to use as the background
176     */
177    public void setButtonDrawable(int resid) {
178        if (resid != 0 && resid == mButtonResource) {
179            return;
180        }
181
182        mButtonResource = resid;
183
184        Drawable d = null;
185        if (mButtonResource != 0) {
186            d = getResources().getDrawable(mButtonResource);
187        }
188        setButtonDrawable(d);
189    }
190
191    /**
192     * Set the background to a given Drawable
193     *
194     * @param d The Drawable to use as the background
195     */
196    public void setButtonDrawable(Drawable d) {
197        if (d != null) {
198            if (mButtonDrawable != null) {
199                mButtonDrawable.setCallback(null);
200                unscheduleDrawable(mButtonDrawable);
201            }
202            d.setCallback(this);
203            d.setVisible(getVisibility() == VISIBLE, false);
204            mButtonDrawable = d;
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    public int getCompoundPaddingLeft() {
228        int padding = super.getCompoundPaddingLeft();
229        if (!isLayoutRtl()) {
230            final Drawable buttonDrawable = mButtonDrawable;
231            if (buttonDrawable != null) {
232                padding += buttonDrawable.getIntrinsicWidth();
233            }
234        }
235        return padding;
236    }
237
238    @Override
239    public int getCompoundPaddingRight() {
240        int padding = super.getCompoundPaddingRight();
241        if (isLayoutRtl()) {
242            final Drawable buttonDrawable = mButtonDrawable;
243            if (buttonDrawable != null) {
244                padding += buttonDrawable.getIntrinsicWidth();
245            }
246        }
247        return padding;
248    }
249
250    /**
251     * @hide
252     */
253    @Override
254    public int getHorizontalOffsetForDrawables() {
255        final Drawable buttonDrawable = mButtonDrawable;
256        return (buttonDrawable != null) ? buttonDrawable.getIntrinsicWidth() : 0;
257    }
258
259    @Override
260    protected void onDraw(Canvas canvas) {
261        super.onDraw(canvas);
262
263        final Drawable buttonDrawable = mButtonDrawable;
264        if (buttonDrawable != null) {
265            final int verticalGravity = getGravity() & Gravity.VERTICAL_GRAVITY_MASK;
266            final int drawableHeight = buttonDrawable.getIntrinsicHeight();
267            final int drawableWidth = buttonDrawable.getIntrinsicWidth();
268
269            int top = 0;
270            switch (verticalGravity) {
271                case Gravity.BOTTOM:
272                    top = getHeight() - drawableHeight;
273                    break;
274                case Gravity.CENTER_VERTICAL:
275                    top = (getHeight() - drawableHeight) / 2;
276                    break;
277            }
278            int bottom = top + drawableHeight;
279            int left = isLayoutRtl() ? getWidth() - drawableWidth : 0;
280            int right = isLayoutRtl() ? getWidth() : drawableWidth;
281
282            buttonDrawable.setBounds(left, top, right, bottom);
283            buttonDrawable.draw(canvas);
284        }
285    }
286
287    @Override
288    protected int[] onCreateDrawableState(int extraSpace) {
289        final int[] drawableState = super.onCreateDrawableState(extraSpace + 1);
290        if (isChecked()) {
291            mergeDrawableStates(drawableState, CHECKED_STATE_SET);
292        }
293        return drawableState;
294    }
295
296    @Override
297    protected void drawableStateChanged() {
298        super.drawableStateChanged();
299
300        if (mButtonDrawable != null) {
301            int[] myDrawableState = getDrawableState();
302
303            // Set the state of the Drawable
304            mButtonDrawable.setState(myDrawableState);
305
306            invalidate();
307        }
308    }
309
310    @Override
311    protected boolean verifyDrawable(Drawable who) {
312        return super.verifyDrawable(who) || who == mButtonDrawable;
313    }
314
315    @Override
316    public void jumpDrawablesToCurrentState() {
317        super.jumpDrawablesToCurrentState();
318        if (mButtonDrawable != null) mButtonDrawable.jumpToCurrentState();
319    }
320
321    static class SavedState extends BaseSavedState {
322        boolean checked;
323
324        /**
325         * Constructor called from {@link CompoundButton#onSaveInstanceState()}
326         */
327        SavedState(Parcelable superState) {
328            super(superState);
329        }
330
331        /**
332         * Constructor called from {@link #CREATOR}
333         */
334        private SavedState(Parcel in) {
335            super(in);
336            checked = (Boolean)in.readValue(null);
337        }
338
339        @Override
340        public void writeToParcel(Parcel out, int flags) {
341            super.writeToParcel(out, flags);
342            out.writeValue(checked);
343        }
344
345        @Override
346        public String toString() {
347            return "CompoundButton.SavedState{"
348                    + Integer.toHexString(System.identityHashCode(this))
349                    + " checked=" + checked + "}";
350        }
351
352        public static final Parcelable.Creator<SavedState> CREATOR
353                = new Parcelable.Creator<SavedState>() {
354            public SavedState createFromParcel(Parcel in) {
355                return new SavedState(in);
356            }
357
358            public SavedState[] newArray(int size) {
359                return new SavedState[size];
360            }
361        };
362    }
363
364    @Override
365    public Parcelable onSaveInstanceState() {
366        // Force our ancestor class to save its state
367        setFreezesText(true);
368        Parcelable superState = super.onSaveInstanceState();
369
370        SavedState ss = new SavedState(superState);
371
372        ss.checked = isChecked();
373        return ss;
374    }
375
376    @Override
377    public void onRestoreInstanceState(Parcelable state) {
378        SavedState ss = (SavedState) state;
379
380        super.onRestoreInstanceState(ss.getSuperState());
381        setChecked(ss.checked);
382        requestLayout();
383    }
384}
385