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