Android RadioGroup 源码分析

  Android 的 RadioGroup 可用于容纳 RadioButton,并为其中的 RadioButton 提供统一的行为管理(选中/取消选中)和事件监听(OnCheckedChangeListener)。RadioGroup 可以很方便地管理 RadioButton,但也仅限于 RadioButton,RadioGroup 无法为其他 View 提供类似的功能。

  最近在做一个类似 RadioGroup 的 ViewGroup,用于管理任意实现了 Checkable 接口的 View,以解决 RadioGroup 不兼容非 RadioButton 的问题,趁此机会阅读了一下 RadioGroup 的源码,把要点整理如下。

1. Layout

  RadioGroup 继承自 LinearLayout,故可以通过 android:orientation 设定水平或垂直布局,默认使用垂直布局。

可以通过 android:checkedButton 属性指定默认选中的按钮,从上面的代码也可以看到,这个属性的值被保存在 mCheckedId 中,mCheckedId 用于保存当前选中的 RadioButton 的 id,之后会反复出现。

2. onFinishInflate()

  RadioGroup 重写了 onFinishInflate(),用于设置初始选中的 RadioButton:

这里如果 mCheckedId 不为 -1,说明开发者通过 android:checkedButton 设置了要初始选中的 RadioButton,则通过 setCheckedStateForView() 选中具有该 id 的按钮,并通过 setCheckedId() 保存并通知选中状态的变化。至于 mProtectFromCheckedChange,之后会再做说明。setCheckedStateForView() 和 setCheckedId() 的代码很简单:

值得注意的是,setCheckedStateForView() 中调用了 checkedView 的 setChecked(),setChecked() 是 Checkable 接口中的方法(RadioButton 继承自 CompoundButton,后者实现了 Checkable),但却通过 “instanceof RadioButton” 要求 checkedView 是 RadioButton 的实例,而没有使用 “instanceof Checkable” 这一更宽泛的限制,具体原因后面会说明。

3. addView()

  RadioGroup 重写了 addView():

这里有一个细节:如果加入的 RadioButton 已经是选中的状态,那么在把这个 RadioButton 加入到 RadioGroup中时,就保持该 RadioButton 的选中状态,并取消掉之前选中的 RadioButton。

4. onChildViewAdded()

  RadioGroup 还通过 OnHierarchyChangeListener 来监听 ChildView 的加入:

这里有两个关键逻辑。其一是如果所要加入的 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() 方法:

点击未选中的 RadioButton 会选中 RadioButton,点击已选中的 RadioButton 则不会取消选中国内。由于 RadioButton 不可通过重复点击取消选中状态的特点,RadioGroup 便在各处做了前面所见的 “instanceof RadioButton” 的限制,仅支持 RadioButton。

5. onCheckedChanged()

  前面提到 RadioGroup 通过 setOnCheckedChangeWidgetListener() 监听其中 RadioButton 的选中状态,具体处理逻辑如下:

当点击一个未被选中的 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:

这里有一个例外情况,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 以支持选中状态,但该接口仅有三个方法:

并没有要求实现该接口的 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”的复杂度。