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