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