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