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         mCheckedId = id;
    187         if (mOnCheckedChangeListener != null) {
    188             mOnCheckedChangeListener.onCheckedChanged(this, mCheckedId);
    189         }
    190         final AutofillManager afm = mContext.getSystemService(AutofillManager.class);
    191         if (afm != null) {
    192             afm.notifyValueChanged(this);
    193         }
    194     }
    195 
    196     private void setCheckedStateForView(int viewId, boolean checked) {
    197         View checkedView = findViewById(viewId);
    198         if (checkedView != null && checkedView instanceof RadioButton) {
    199             ((RadioButton) checkedView).setChecked(checked);
    200         }
    201     }
    202 
    203     /**
    204      * <p>Returns the identifier of the selected radio button in this group.
    205      * Upon empty selection, the returned value is -1.</p>
    206      *
    207      * @return the unique id of the selected radio button in this group
    208      *
    209      * @see #check(int)
    210      * @see #clearCheck()
    211      *
    212      * @attr ref android.R.styleable#RadioGroup_checkedButton
    213      */
    214     @IdRes
    215     public int getCheckedRadioButtonId() {
    216         return mCheckedId;
    217     }
    218 
    219     /**
    220      * <p>Clears the selection. When the selection is cleared, no radio button
    221      * in this group is selected and {@link #getCheckedRadioButtonId()} returns
    222      * null.</p>
    223      *
    224      * @see #check(int)
    225      * @see #getCheckedRadioButtonId()
    226      */
    227     public void clearCheck() {
    228         check(-1);
    229     }
    230 
    231     /**
    232      * <p>Register a callback to be invoked when the checked radio button
    233      * changes in this group.</p>
    234      *
    235      * @param listener the callback to call on checked state change
    236      */
    237     public void setOnCheckedChangeListener(OnCheckedChangeListener listener) {
    238         mOnCheckedChangeListener = listener;
    239     }
    240 
    241     /**
    242      * {@inheritDoc}
    243      */
    244     @Override
    245     public LayoutParams generateLayoutParams(AttributeSet attrs) {
    246         return new RadioGroup.LayoutParams(getContext(), attrs);
    247     }
    248 
    249     /**
    250      * {@inheritDoc}
    251      */
    252     @Override
    253     protected boolean checkLayoutParams(ViewGroup.LayoutParams p) {
    254         return p instanceof RadioGroup.LayoutParams;
    255     }
    256 
    257     @Override
    258     protected LinearLayout.LayoutParams generateDefaultLayoutParams() {
    259         return new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
    260     }
    261 
    262     @Override
    263     public CharSequence getAccessibilityClassName() {
    264         return RadioGroup.class.getName();
    265     }
    266 
    267     /**
    268      * <p>This set of layout parameters defaults the width and the height of
    269      * the children to {@link #WRAP_CONTENT} when they are not specified in the
    270      * XML file. Otherwise, this class ussed the value read from the XML file.</p>
    271      *
    272      * <p>See
    273      * {@link android.R.styleable#LinearLayout_Layout LinearLayout Attributes}
    274      * for a list of all child view attributes that this class supports.</p>
    275      *
    276      */
    277     public static class LayoutParams extends LinearLayout.LayoutParams {
    278         /**
    279          * {@inheritDoc}
    280          */
    281         public LayoutParams(Context c, AttributeSet attrs) {
    282             super(c, attrs);
    283         }
    284 
    285         /**
    286          * {@inheritDoc}
    287          */
    288         public LayoutParams(int w, int h) {
    289             super(w, h);
    290         }
    291 
    292         /**
    293          * {@inheritDoc}
    294          */
    295         public LayoutParams(int w, int h, float initWeight) {
    296             super(w, h, initWeight);
    297         }
    298 
    299         /**
    300          * {@inheritDoc}
    301          */
    302         public LayoutParams(ViewGroup.LayoutParams p) {
    303             super(p);
    304         }
    305 
    306         /**
    307          * {@inheritDoc}
    308          */
    309         public LayoutParams(MarginLayoutParams source) {
    310             super(source);
    311         }
    312 
    313         /**
    314          * <p>Fixes the child's width to
    315          * {@link android.view.ViewGroup.LayoutParams#WRAP_CONTENT} and the child's
    316          * height to  {@link android.view.ViewGroup.LayoutParams#WRAP_CONTENT}
    317          * when not specified in the XML file.</p>
    318          *
    319          * @param a the styled attributes set
    320          * @param widthAttr the width attribute to fetch
    321          * @param heightAttr the height attribute to fetch
    322          */
    323         @Override
    324         protected void setBaseAttributes(TypedArray a,
    325                 int widthAttr, int heightAttr) {
    326 
    327             if (a.hasValue(widthAttr)) {
    328                 width = a.getLayoutDimension(widthAttr, "layout_width");
    329             } else {
    330                 width = WRAP_CONTENT;
    331             }
    332 
    333             if (a.hasValue(heightAttr)) {
    334                 height = a.getLayoutDimension(heightAttr, "layout_height");
    335             } else {
    336                 height = WRAP_CONTENT;
    337             }
    338         }
    339     }
    340 
    341     /**
    342      * <p>Interface definition for a callback to be invoked when the checked
    343      * radio button changed in this group.</p>
    344      */
    345     public interface OnCheckedChangeListener {
    346         /**
    347          * <p>Called when the checked radio button has changed. When the
    348          * selection is cleared, checkedId is -1.</p>
    349          *
    350          * @param group the group in which the checked radio button has changed
    351          * @param checkedId the unique identifier of the newly checked radio button
    352          */
    353         public void onCheckedChanged(RadioGroup group, @IdRes int checkedId);
    354     }
    355 
    356     private class CheckedStateTracker implements CompoundButton.OnCheckedChangeListener {
    357         @Override
    358         public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
    359             // prevents from infinite recursion
    360             if (mProtectFromCheckedChange) {
    361                 return;
    362             }
    363 
    364             mProtectFromCheckedChange = true;
    365             if (mCheckedId != -1) {
    366                 setCheckedStateForView(mCheckedId, false);
    367             }
    368             mProtectFromCheckedChange = false;
    369 
    370             int id = buttonView.getId();
    371             setCheckedId(id);
    372         }
    373     }
    374 
    375     /**
    376      * <p>A pass-through listener acts upon the events and dispatches them
    377      * to another listener. This allows the table layout to set its own internal
    378      * hierarchy change listener without preventing the user to setup his.</p>
    379      */
    380     private class PassThroughHierarchyChangeListener implements
    381             ViewGroup.OnHierarchyChangeListener {
    382         private ViewGroup.OnHierarchyChangeListener mOnHierarchyChangeListener;
    383 
    384         /**
    385          * {@inheritDoc}
    386          */
    387         @Override
    388         public void onChildViewAdded(View parent, View child) {
    389             if (parent == RadioGroup.this && child instanceof RadioButton) {
    390                 int id = child.getId();
    391                 // generates an id if it's missing
    392                 if (id == View.NO_ID) {
    393                     id = View.generateViewId();
    394                     child.setId(id);
    395                 }
    396                 ((RadioButton) child).setOnCheckedChangeWidgetListener(
    397                         mChildOnCheckedChangeListener);
    398             }
    399 
    400             if (mOnHierarchyChangeListener != null) {
    401                 mOnHierarchyChangeListener.onChildViewAdded(parent, child);
    402             }
    403         }
    404 
    405         /**
    406          * {@inheritDoc}
    407          */
    408         @Override
    409         public void onChildViewRemoved(View parent, View child) {
    410             if (parent == RadioGroup.this && child instanceof RadioButton) {
    411                 ((RadioButton) child).setOnCheckedChangeWidgetListener(null);
    412             }
    413 
    414             if (mOnHierarchyChangeListener != null) {
    415                 mOnHierarchyChangeListener.onChildViewRemoved(parent, child);
    416             }
    417         }
    418     }
    419 
    420     @Override
    421     public void onProvideAutofillStructure(ViewStructure structure, int flags) {
    422         super.onProvideAutofillStructure(structure, flags);
    423         structure.setDataIsSensitive(mCheckedId != mInitialCheckedId);
    424     }
    425 
    426     @Override
    427     public void autofill(AutofillValue value) {
    428         if (!isEnabled()) return;
    429 
    430         if (!value.isList()) {
    431             Log.w(LOG_TAG, value + " could not be autofilled into " + this);
    432             return;
    433         }
    434 
    435         final int index = value.getListValue();
    436         final View child = getChildAt(index);
    437         if (child == null) {
    438             Log.w(VIEW_LOG_TAG, "RadioGroup.autoFill(): no child with index " + index);
    439             return;
    440         }
    441 
    442         check(child.getId());
    443     }
    444 
    445     @Override
    446     public @AutofillType int getAutofillType() {
    447         return isEnabled() ? AUTOFILL_TYPE_LIST : AUTOFILL_TYPE_NONE;
    448     }
    449 
    450     @Override
    451     public AutofillValue getAutofillValue() {
    452         if (!isEnabled()) return null;
    453 
    454         final int count = getChildCount();
    455         for (int i = 0; i < count; i++) {
    456             final View child = getChildAt(i);
    457             if (child.getId() == mCheckedId) {
    458                 return AutofillValue.forList(i);
    459             }
    460         }
    461         return null;
    462     }
    463 }
    464