RadioGroup应该算是一个很常用的控件了,用于作为RadioButton的父控件,可以实现单选框。然而最近用了类似flux的单向数据流架构后,再使用RadioGroup立马遇到了一个大坑。
架构如上图,View的状态由ViewHolder中的变量决定,而ViewHolder中值的修改由用户输入触发控件的各种OnChangeListener改变。这里用到的是RadioGroup.OnChangeListener。
void onCheckedChanged (RadioGroup group, int checkedId)
onCheckChanged中int checkedId官方给的解释是:the unique identifier of the newly checked radio button。也就是说,在RadioGroup中一旦有RadioButton.checked改变了,就可以通过这个Listener获取到通知。
public void check(@IdRes int id) {
// don't even bother
if (id != -1 && (id == mCheckedId)) {
return;
}
if (mCheckedId != -1) {
setCheckedStateForView(mCheckedId, false);
}
if (id != -1) {
setCheckedStateForView(id, true);
}
setCheckedId(id);
}
RadioGroup.check函数可以修改子控件的check值,并且在进入函数的第一行就做了去重处理,防止Listener触发check函数,check函数又触发Lisnter导致无限调用地狱。这一切看着是如此完美,然后这就是坑的开始。一旦按照这个架构搭建好之后,如果只是用户触发该RadioGroup中的RadioButton触发值的刷新,这个流程完全没有任何问题。但是使用该架构,为的就是使UI能实时响应ViewHolder中的值更改,随时刷新页面。例如在一个其它的页面也能设置一个这个选项值,导致了Model的更新,Model的更新触发了ViewHolder的更新,ViewHolder的更新触发了RadioGroup.check函数。这时就会出现ANR,按前面的分析,一切都是那么的完美,不科学啊!而且通过单步调试,发现onCheckedChanged中收到的checkedId居然是错误的,百思不得其解啊!
这时,就需要看看RadioGroup的工作原理了。在RadioGroup.check函数中,setCheckedStateForView(mCheckedId, false);
这行调用触发了上一个被checked的RadioButton.check函数。下面是RadioButton.check函数的源码:
public void setChecked(boolean checked) {
if (mChecked != checked) {
mChecked = checked;
refreshDrawableState();
notifyViewAccessibilityStateChangedIfNeeded(
AccessibilityEvent.CONTENT_CHANGE_TYPE_UNDEFINED);
// Avoid infinite recursions if setChecked() is called from a listener
if (mBroadcasting) {
return;
}
mBroadcasting = true;
if (mOnCheckedChangeListener != null) {
mOnCheckedChangeListener.onCheckedChanged(this, mChecked);
}
if (mOnCheckedChangeWidgetListener != null) {
mOnCheckedChangeWidgetListener.onCheckedChanged(this, mChecked);
}
mBroadcasting = false;
}
}
其中可以看到 mOnCheckedChangeWidgetListener.onCheckedChanged(this, mChecked);
这行调用通知了RadioGroup,说我的checked值改变了。我们来看看通知RadioGroup之后,它做了什么。
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);
}
}
可以看到,里面居然没有使用传递进来的isChecked值!也就是说RadioGroup根本没有检查回调的RadioButton是不是被checked的。紧接着就调用setCheckedId函数,把触发的控件当作了是被checked的对待:
private void setCheckedId(@IdRes int id) {
mCheckedId = id;
if (mOnCheckedChangeListener != null) {
mOnCheckedChangeListener.onCheckedChanged(this, mCheckedId);
}
}
在里面,再次触发了RadioGroup.OnCheckedChangeListener,并且把不是被checked的id当作参数传递了进去。于是乎,这就改变了ViewHolder中的值,接着触发了RadioGroup.check函数,接下来就是一系列的无穷调用...为什么在不使用响应式架构的时候不会出现bug呢,因为以前Listener中的值不会触发RadioGroup.check函数,而在check函数会触发两次Listener,在最后一次会将正确的id值传入Listener中。
知道了坑在哪之后,解决的方法就是不要直接使用Listener传递的checkedId,还要检查该RadioButton.checked属性是不是true,只有为true时才将值设置给ViewHolder,这样地狱调用也就不会出现了。