Android RadioGroup 源码分析
Android 的 RadioGroup 可用于容纳 RadioButton,并为其中的 RadioButton 提供统一的行为管理(选中/取消选中)和事件监听(OnCheckedChangeListener)。RadioGroup 可以很方便地管理 RadioButton,但也仅限于 RadioButton,RadioGroup 无法为其他 View 提供类似的功能。
最近在做一个类似 RadioGroup 的 ViewGroup,用于管理任意实现了 Checkable 接口的 View,以解决 RadioGroup 不兼容非 RadioButton 的问题,趁此机会阅读了一下 RadioGroup 的源码,把要点整理如下。
Contents
1. Layout
RadioGroup 继承自 LinearLayout,故可以通过 android:orientation 设定水平或垂直布局,默认使用垂直布局。
public RadioGroup(Context context, AttributeSet attrs) { super(context, attrs); // retrieve selected radio button as requested by the user in the // XML layout file TypedArray attributes = context.obtainStyledAttributes( attrs, com.android.internal.R.styleable.RadioGroup, com.android.internal.R.attr.radioButtonStyle, 0); int value = attributes.getResourceId(R.styleable.RadioGroup_checkedButton, View.NO_ID); if (value != View.NO_ID) { mCheckedId = value; } final int index = attributes.getInt(com.android.internal.R.styleable.RadioGroup_orientation, VERTICAL); setOrientation(index); attributes.recycle(); init(); }
可以通过 android:checkedButton 属性指定默认选中的按钮,从上面的代码也可以看到,这个属性的值被保存在 mCheckedId 中,mCheckedId 用于保存当前选中的 RadioButton 的 id,之后会反复出现。
2. onFinishInflate()
RadioGroup 重写了 onFinishInflate(),用于设置初始选中的 RadioButton:
@Override protected void onFinishInflate() { super.onFinishInflate(); // checks the appropriate radio button as requested in the XML file if (mCheckedId != -1) { mProtectFromCheckedChange = true; setCheckedStateForView(mCheckedId, true); mProtectFromCheckedChange = false; setCheckedId(mCheckedId); } }
这里如果 mCheckedId 不为 -1,说明开发者通过 android:checkedButton 设置了要初始选中的 RadioButton,则通过 setCheckedStateForView() 选中具有该 id 的按钮,并通过 setCheckedId() 保存并通知选中状态的变化。至于 mProtectFromCheckedChange,之后会再做说明。setCheckedStateForView() 和 setCheckedId() 的代码很简单:
private void setCheckedStateForView(int viewId, boolean checked) { View checkedView = findViewById(viewId); if (checkedView != null && checkedView instanceof RadioButton) { ((RadioButton) checkedView).setChecked(checked); } } private void setCheckedId(@IdRes int id) { mCheckedId = id; if (mOnCheckedChangeListener != null) { mOnCheckedChangeListener.onCheckedChanged(this, mCheckedId); } }
值得注意的是,setCheckedStateForView() 中调用了 checkedView 的 setChecked(),setChecked() 是 Checkable 接口中的方法(RadioButton 继承自 CompoundButton,后者实现了 Checkable),但却通过 “instanceof RadioButton” 要求 checkedView 是 RadioButton 的实例,而没有使用 “instanceof Checkable” 这一更宽泛的限制,具体原因后面会说明。
3. addView()
RadioGroup 重写了 addView():
@Override public void addView(View child, int index, ViewGroup.LayoutParams params) { if (child instanceof RadioButton) { final RadioButton button = (RadioButton) child; if (button.isChecked()) { mProtectFromCheckedChange = true; if (mCheckedId != -1) { setCheckedStateForView(mCheckedId, false); } mProtectFromCheckedChange = false; setCheckedId(button.getId()); } } super.addView(child, index, params); }
这里有一个细节:如果加入的 RadioButton 已经是选中的状态,那么在把这个 RadioButton 加入到 RadioGroup中时,就保持该 RadioButton 的选中状态,并取消掉之前选中的 RadioButton。
4. onChildViewAdded()
RadioGroup 还通过 OnHierarchyChangeListener 来监听 ChildView 的加入:
private class PassThroughHierarchyChangeListener implements ViewGroup.OnHierarchyChangeListener { private ViewGroup.OnHierarchyChangeListener mOnHierarchyChangeListener; public void onChildViewAdded(View parent, View child) { if (parent == RadioGroup.this && child instanceof RadioButton) { int id = child.getId(); // generates an id if it's missing if (id == View.NO_ID) { id = View.generateViewId(); child.setId(id); } ((RadioButton) child).setOnCheckedChangeWidgetListener( mChildOnCheckedChangeListener); } if (mOnHierarchyChangeListener != null) { mOnHierarchyChangeListener.onChildViewAdded(parent, child); } } // ... }
这里有两个关键逻辑。其一是如果所要加入的 child 没有自己的 id,就为它设置一个 id。如前所述,RadioGroup 使用 mCheckId 记录被选中的 RadioButton,这就要求 RadioGroup 中的所有 RadioButton 都有自己的 id。其二是通过 setOnCheckedChangeWidgetListener() 监听每个 RadioButton 的选中状态变化。
setOnCheckedChangeWidgetListener() 是 RadioButton 继承自 CompoundButton 的方法,用于内部监听选中状态变化,独立于通过公用方法 setOnCheckedChangeListener() 设置的 OnCheckedChangeListener,由此 RadioGroup 获得了监听其中 RadioButton 的选中状态的能力,也使得 RadioGroup 依赖于 CompoundButton,无法适用于一般的的仅实现了 Checkable 接口的 View。又由于 RadioButton 覆盖了 CompoundButton 的 toggle() 方法:
@Override public void toggle() { // we override to prevent toggle when the radio is already // checked (as opposed to check boxes widgets) if (!isChecked()) { super.toggle(); } }
点击未选中的 RadioButton 会选中 RadioButton,点击已选中的 RadioButton 则不会取消选中国内。由于 RadioButton 不可通过重复点击取消选中状态的特点,RadioGroup 便在各处做了前面所见的 “instanceof RadioButton” 的限制,仅支持 RadioButton。
5. onCheckedChanged()
前面提到 RadioGroup 通过 setOnCheckedChangeWidgetListener() 监听其中 RadioButton 的选中状态,具体处理逻辑如下:
private class CheckedStateTracker implements CompoundButton.OnCheckedChangeListener { public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) { // prevents from infinite recursion if (mProtectFromCheckedChange) { return; } mProtectFromCheckedChange = true; if (mCheckedId != -1) { setCheckedStateForView(mCheckedId, false); } mProtectFromCheckedChange = false; int id = buttonView.getId(); setCheckedId(id); } }
当点击一个未被选中的 RadioButton 时,RadioButton 会自己切换至选中状态,并触发上面的 onCheckedChanged()。由于之前选中的 RadioButton 并不会自动取消选中,首先要通过 setCheckedStateForView() 取消之前被选中的 RadioButton,然后通过 setCheckedId() 保存并通知当前选中的 RadioButton 的 id。点击已被选中的 RadioButton 不会触发 onCheckedChanged(),原因就在上面所列的 RadioButton 的 toggle()。
这里又遇到了 mProtectFromCheckedChange。在 RadioGroup 中可以看到,在每次调用 setCheckedStateForView() 前后,都会先后设置 mProtectFromCheckedChange 为 true 和 false,如果 mProtectFromCheckedChange 为 true,onCheckedChanged() 就会直接 return,否则 setCheckedStateForView() 本身就会触发 onCheckedChanged(),导致无限循环。
6. RadioGroup 的一个问题
通过阅读源码,可以发现 RadioGroup 的一个问题。RadioGroup 通过 mCheckedId 记录选中的 RadioButton,并通过 OnHierarchyChangeListener 确保加入的 RadioButton 都有自己的 id,按照 RadioGroup 的 addView() 逻辑,如果通过 addView() 加入的 RadioButton 已经处于选中状态,则保持该 RadioButton 的状态,并取消之前选中的 RadioButton:
@Override public void addView(View child, int index, ViewGroup.LayoutParams params) { if (child instanceof RadioButton) { final RadioButton button = (RadioButton) child; if (button.isChecked()) { mProtectFromCheckedChange = true; if (mCheckedId != -1) { setCheckedStateForView(mCheckedId, false); } mProtectFromCheckedChange = false; setCheckedId(button.getId()); } } super.addView(child, index, params); }
这里有一个例外情况,addView() 发生在 OnHierarchyChangeListener 的 onChildViewAdded() 之前,而 addView() 并不检查所加入的 RadioButton 是否有 id。如果通过 addView() 加入一个未指定 id 的、已经被选中的 RadioButton,那么上面 setCheckedId(button.getId()) 相当于 setCheckedId(-1),RadioGroup 无法在 mCheckedId 记录新加入的已选中的 RadioButton,该 RadioButton 会一直处于选中状态,用户是无法通过界面来取消选中的。该问题可以在 Android 7.0 上复现。
7. 总结
以上就是 RadioGroup 的主要源码分析,由于 RadioGroup 依赖 CompoundButton 的 setOnCheckedChangeListener() 来监听其中按钮的选中状态,导致 RadioGroup 仅能支持 CompoundButton;又由于 RadioButton 实现了自己的 toggle() 逻辑,RadioGroup 便把自己限制在 RadioButton,仅能管理其中 RadioButton 的状态变化。
Checkable 接口的本意是扩展 View 以支持选中状态,但该接口仅有三个方法:
public interface Checkable { /** * Change the checked state of the view * * @param checked The new checked state */ void setChecked(boolean checked); /** * @return The current checked state of the view */ boolean isChecked(); /** * Change the checked state of the view to the inverse of its current state * */ void toggle(); }
并没有要求实现该接口的 View 提供监听其选中状态的方法,这也是实现通用的“CheckableGroup”的难点。解决方法可以是像 RadioGroup 一样,要求其中的子 View 不但要实现 Checkable,还要提供一个类似 OnCheckedChangeListener 的 Listener(最好是内部的,不影响对外接口),以监听 Checkable 的选中状态。另一种解决方法是利用现有的 View 的接口,比如 setOnCheckedChangeListener(),来监听子 View 的选中状态,但这样就要要求“CheckableGroup”外部不能再使用子 View 的 setOnCheckedChangeListener(),否则就会导致“CheckableGroup”的监听失效。
此外,Checkable 也无法规定实现该接口的 View 是否要能够自行切换选中状态,如 RadioButton 可以通过点击事件来更改选中状态,点击未选中的 RadioButton 后,RadioButton 会自己切换到选中状态;而其他实现了 Checkable 的 View 可能并不具有此行为,而是依赖外部设置(通过 Checkable 的 setChecked() 和 toggle()),这也进一步加大了实现通用“CheckableGroup”的复杂度。