RadioGroup.java revision 2f8fb1f62f2840701e3e16497eb8191f38b72e0b
1/*
2 * Copyright (C) 2006 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 android.annotation.IdRes;
20import android.content.Context;
21import android.content.res.TypedArray;
22import android.util.AttributeSet;
23import android.util.Log;
24import android.view.View;
25import android.view.ViewGroup;
26import android.view.ViewStructure;
27import android.view.autofill.AutofillManager;
28import android.view.autofill.AutofillValue;
29
30import com.android.internal.R;
31
32
33/**
34 * <p>This class is used to create a multiple-exclusion scope for a set of radio
35 * buttons. Checking one radio button that belongs to a radio group unchecks
36 * any previously checked radio button within the same group.</p>
37 *
38 * <p>Intially, all of the radio buttons are unchecked. While it is not possible
39 * to uncheck a particular radio button, the radio group can be cleared to
40 * remove the checked state.</p>
41 *
42 * <p>The selection is identified by the unique id of the radio button as defined
43 * in the XML layout file.</p>
44 *
45 * <p><strong>XML Attributes</strong></p>
46 * <p>See {@link android.R.styleable#RadioGroup RadioGroup Attributes},
47 * {@link android.R.styleable#LinearLayout LinearLayout Attributes},
48 * {@link android.R.styleable#ViewGroup ViewGroup Attributes},
49 * {@link android.R.styleable#View View Attributes}</p>
50 * <p>Also see
51 * {@link android.widget.LinearLayout.LayoutParams LinearLayout.LayoutParams}
52 * for layout attributes.</p>
53 *
54 * @see RadioButton
55 *
56 */
57public class RadioGroup extends LinearLayout {
58
59    // holds the checked id; the selection is empty by default
60    private int mCheckedId = -1;
61    // tracks children radio buttons checked state
62    private CompoundButton.OnCheckedChangeListener mChildOnCheckedChangeListener;
63    // when true, mOnCheckedChangeListener discards events
64    private boolean mProtectFromCheckedChange = false;
65    private OnCheckedChangeListener mOnCheckedChangeListener;
66    private PassThroughHierarchyChangeListener mPassThroughListener;
67
68    // Indicates whether the child was set from resources or dynamically, so it can be used
69    // to sanitize autofill requests.
70    private int mInitialCheckedId = View.NO_ID;
71
72    /**
73     * {@inheritDoc}
74     */
75    public RadioGroup(Context context) {
76        super(context);
77        setOrientation(VERTICAL);
78        init();
79    }
80
81    /**
82     * {@inheritDoc}
83     */
84    public RadioGroup(Context context, AttributeSet attrs) {
85        super(context, attrs);
86
87        // RadioGroup is important by default, unless app developer overrode attribute.
88        if (getImportantForAutofill() == IMPORTANT_FOR_AUTOFILL_AUTO) {
89            setImportantForAutofill(IMPORTANT_FOR_AUTOFILL_YES);
90        }
91
92        // retrieve selected radio button as requested by the user in the
93        // XML layout file
94        TypedArray attributes = context.obtainStyledAttributes(
95                attrs, com.android.internal.R.styleable.RadioGroup, com.android.internal.R.attr.radioButtonStyle, 0);
96
97        int value = attributes.getResourceId(R.styleable.RadioGroup_checkedButton, View.NO_ID);
98        if (value != View.NO_ID) {
99            mCheckedId = value;
100            mInitialCheckedId = value;
101        }
102        final int index = attributes.getInt(com.android.internal.R.styleable.RadioGroup_orientation, VERTICAL);
103        setOrientation(index);
104
105        attributes.recycle();
106        init();
107    }
108
109    private void init() {
110        mChildOnCheckedChangeListener = new CheckedStateTracker();
111        mPassThroughListener = new PassThroughHierarchyChangeListener();
112        super.setOnHierarchyChangeListener(mPassThroughListener);
113    }
114
115    /**
116     * {@inheritDoc}
117     */
118    @Override
119    public void setOnHierarchyChangeListener(OnHierarchyChangeListener listener) {
120        // the user listener is delegated to our pass-through listener
121        mPassThroughListener.mOnHierarchyChangeListener = listener;
122    }
123
124    /**
125     * {@inheritDoc}
126     */
127    @Override
128    protected void onFinishInflate() {
129        super.onFinishInflate();
130
131        // checks the appropriate radio button as requested in the XML file
132        if (mCheckedId != -1) {
133            mProtectFromCheckedChange = true;
134            setCheckedStateForView(mCheckedId, true);
135            mProtectFromCheckedChange = false;
136            setCheckedId(mCheckedId);
137        }
138    }
139
140    @Override
141    public void addView(View child, int index, ViewGroup.LayoutParams params) {
142        if (child instanceof RadioButton) {
143            final RadioButton button = (RadioButton) child;
144            if (button.isChecked()) {
145                mProtectFromCheckedChange = true;
146                if (mCheckedId != -1) {
147                    setCheckedStateForView(mCheckedId, false);
148                }
149                mProtectFromCheckedChange = false;
150                setCheckedId(button.getId());
151            }
152        }
153
154        super.addView(child, index, params);
155    }
156
157    /**
158     * <p>Sets the selection to the radio button whose identifier is passed in
159     * parameter. Using -1 as the selection identifier clears the selection;
160     * such an operation is equivalent to invoking {@link #clearCheck()}.</p>
161     *
162     * @param id the unique id of the radio button to select in this group
163     *
164     * @see #getCheckedRadioButtonId()
165     * @see #clearCheck()
166     */
167    public void check(@IdRes int id) {
168        // don't even bother
169        if (id != -1 && (id == mCheckedId)) {
170            return;
171        }
172
173        if (mCheckedId != -1) {
174            setCheckedStateForView(mCheckedId, false);
175        }
176
177        if (id != -1) {
178            setCheckedStateForView(id, true);
179        }
180
181        setCheckedId(id);
182    }
183
184    private void setCheckedId(@IdRes int id) {
185        mCheckedId = id;
186        if (mOnCheckedChangeListener != null) {
187            mOnCheckedChangeListener.onCheckedChanged(this, mCheckedId);
188        }
189        final AutofillManager afm = mContext.getSystemService(AutofillManager.class);
190        if (afm != null) {
191            afm.notifyValueChanged(this);
192        }
193    }
194
195    private void setCheckedStateForView(int viewId, boolean checked) {
196        View checkedView = findViewById(viewId);
197        if (checkedView != null && checkedView instanceof RadioButton) {
198            ((RadioButton) checkedView).setChecked(checked);
199        }
200    }
201
202    /**
203     * <p>Returns the identifier of the selected radio button in this group.
204     * Upon empty selection, the returned value is -1.</p>
205     *
206     * @return the unique id of the selected radio button in this group
207     *
208     * @see #check(int)
209     * @see #clearCheck()
210     *
211     * @attr ref android.R.styleable#RadioGroup_checkedButton
212     */
213    @IdRes
214    public int getCheckedRadioButtonId() {
215        return mCheckedId;
216    }
217
218    /**
219     * <p>Clears the selection. When the selection is cleared, no radio button
220     * in this group is selected and {@link #getCheckedRadioButtonId()} returns
221     * null.</p>
222     *
223     * @see #check(int)
224     * @see #getCheckedRadioButtonId()
225     */
226    public void clearCheck() {
227        check(-1);
228    }
229
230    /**
231     * <p>Register a callback to be invoked when the checked radio button
232     * changes in this group.</p>
233     *
234     * @param listener the callback to call on checked state change
235     */
236    public void setOnCheckedChangeListener(OnCheckedChangeListener listener) {
237        mOnCheckedChangeListener = listener;
238    }
239
240    /**
241     * {@inheritDoc}
242     */
243    @Override
244    public LayoutParams generateLayoutParams(AttributeSet attrs) {
245        return new RadioGroup.LayoutParams(getContext(), attrs);
246    }
247
248    /**
249     * {@inheritDoc}
250     */
251    @Override
252    protected boolean checkLayoutParams(ViewGroup.LayoutParams p) {
253        return p instanceof RadioGroup.LayoutParams;
254    }
255
256    @Override
257    protected LinearLayout.LayoutParams generateDefaultLayoutParams() {
258        return new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
259    }
260
261    @Override
262    public CharSequence getAccessibilityClassName() {
263        return RadioGroup.class.getName();
264    }
265
266    /**
267     * <p>This set of layout parameters defaults the width and the height of
268     * the children to {@link #WRAP_CONTENT} when they are not specified in the
269     * XML file. Otherwise, this class ussed the value read from the XML file.</p>
270     *
271     * <p>See
272     * {@link android.R.styleable#LinearLayout_Layout LinearLayout Attributes}
273     * for a list of all child view attributes that this class supports.</p>
274     *
275     */
276    public static class LayoutParams extends LinearLayout.LayoutParams {
277        /**
278         * {@inheritDoc}
279         */
280        public LayoutParams(Context c, AttributeSet attrs) {
281            super(c, attrs);
282        }
283
284        /**
285         * {@inheritDoc}
286         */
287        public LayoutParams(int w, int h) {
288            super(w, h);
289        }
290
291        /**
292         * {@inheritDoc}
293         */
294        public LayoutParams(int w, int h, float initWeight) {
295            super(w, h, initWeight);
296        }
297
298        /**
299         * {@inheritDoc}
300         */
301        public LayoutParams(ViewGroup.LayoutParams p) {
302            super(p);
303        }
304
305        /**
306         * {@inheritDoc}
307         */
308        public LayoutParams(MarginLayoutParams source) {
309            super(source);
310        }
311
312        /**
313         * <p>Fixes the child's width to
314         * {@link android.view.ViewGroup.LayoutParams#WRAP_CONTENT} and the child's
315         * height to  {@link android.view.ViewGroup.LayoutParams#WRAP_CONTENT}
316         * when not specified in the XML file.</p>
317         *
318         * @param a the styled attributes set
319         * @param widthAttr the width attribute to fetch
320         * @param heightAttr the height attribute to fetch
321         */
322        @Override
323        protected void setBaseAttributes(TypedArray a,
324                int widthAttr, int heightAttr) {
325
326            if (a.hasValue(widthAttr)) {
327                width = a.getLayoutDimension(widthAttr, "layout_width");
328            } else {
329                width = WRAP_CONTENT;
330            }
331
332            if (a.hasValue(heightAttr)) {
333                height = a.getLayoutDimension(heightAttr, "layout_height");
334            } else {
335                height = WRAP_CONTENT;
336            }
337        }
338    }
339
340    /**
341     * <p>Interface definition for a callback to be invoked when the checked
342     * radio button changed in this group.</p>
343     */
344    public interface OnCheckedChangeListener {
345        /**
346         * <p>Called when the checked radio button has changed. When the
347         * selection is cleared, checkedId is -1.</p>
348         *
349         * @param group the group in which the checked radio button has changed
350         * @param checkedId the unique identifier of the newly checked radio button
351         */
352        public void onCheckedChanged(RadioGroup group, @IdRes int checkedId);
353    }
354
355    private class CheckedStateTracker implements CompoundButton.OnCheckedChangeListener {
356        @Override
357        public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
358            // prevents from infinite recursion
359            if (mProtectFromCheckedChange) {
360                return;
361            }
362
363            mProtectFromCheckedChange = true;
364            if (mCheckedId != -1) {
365                setCheckedStateForView(mCheckedId, false);
366            }
367            mProtectFromCheckedChange = false;
368
369            int id = buttonView.getId();
370            setCheckedId(id);
371        }
372    }
373
374    /**
375     * <p>A pass-through listener acts upon the events and dispatches them
376     * to another listener. This allows the table layout to set its own internal
377     * hierarchy change listener without preventing the user to setup his.</p>
378     */
379    private class PassThroughHierarchyChangeListener implements
380            ViewGroup.OnHierarchyChangeListener {
381        private ViewGroup.OnHierarchyChangeListener mOnHierarchyChangeListener;
382
383        /**
384         * {@inheritDoc}
385         */
386        @Override
387        public void onChildViewAdded(View parent, View child) {
388            if (parent == RadioGroup.this && child instanceof RadioButton) {
389                int id = child.getId();
390                // generates an id if it's missing
391                if (id == View.NO_ID) {
392                    id = View.generateViewId();
393                    child.setId(id);
394                }
395                ((RadioButton) child).setOnCheckedChangeWidgetListener(
396                        mChildOnCheckedChangeListener);
397            }
398
399            if (mOnHierarchyChangeListener != null) {
400                mOnHierarchyChangeListener.onChildViewAdded(parent, child);
401            }
402        }
403
404        /**
405         * {@inheritDoc}
406         */
407        @Override
408        public void onChildViewRemoved(View parent, View child) {
409            if (parent == RadioGroup.this && child instanceof RadioButton) {
410                ((RadioButton) child).setOnCheckedChangeWidgetListener(null);
411            }
412
413            if (mOnHierarchyChangeListener != null) {
414                mOnHierarchyChangeListener.onChildViewRemoved(parent, child);
415            }
416        }
417    }
418
419    // TODO(b/33197203): add unit/CTS tests for autofill methods (and make sure they handle enable)
420
421    @Override
422    public void onProvideAutofillStructure(ViewStructure structure, int flags) {
423        super.onProvideAutofillStructure(structure, flags);
424        structure.setSanitized(mCheckedId == mInitialCheckedId);
425    }
426
427    @Override
428    public void autofill(AutofillValue value) {
429        if (!isEnabled()) return;
430
431        final int index = value.getListValue();
432        final View child = getChildAt(index);
433        if (child == null) {
434            Log.w(VIEW_LOG_TAG, "RadioGroup.autoFill(): no child with index " + index);
435            return;
436        }
437        check(child.getId());
438    }
439
440    @Override
441    public @AutofillType int getAutofillType() {
442        return isEnabled() ? AUTOFILL_TYPE_LIST : AUTOFILL_TYPE_NONE;
443    }
444
445    @Override
446    public AutofillValue getAutofillValue() {
447        if (!isEnabled()) return null;
448
449        final int count = getChildCount();
450        for (int i = 0; i < count; i++) {
451            final View child = getChildAt(i);
452            if (child.getId() == mCheckedId) {
453                return AutofillValue.forList(i);
454            }
455        }
456        return null;
457    }
458}
459