今天和朋友聊天,被问到了一个RadioGroup的回调问题,说在调用clearCheck方法的时候,OnCheckChangeListener回调了两次,对他的业务逻辑造成了影响,我们一起看了看源码中这里的实现,然后想到了一个比较合适的解决方案,在这里给大家分享一下,供遇到类似问题的朋友参考。
想要解决问题,必须要先知道问题是如何产生的,这样才是最高效的,所以我们先看一下为什么会有两次回调!
我们调用的是clearCheck方法,那么他的源码呢?
/**
* Clears the selection. When the selection is cleared, no radio button
* in this group is selected and {@link #getCheckedRadioButtonId()} returns
* null.
*
* @see #check(int)
* @see #getCheckedRadioButtonId()
*/
public void clearCheck() {
check(-1);
}
直接调用check方法的,传参-1
/**
* Sets the selection to the radio button whose identifier is passed in
* parameter. Using -1 as the selection identifier clears the selection;
* such an operation is equivalent to invoking {@link #clearCheck()}.
*
* @param id the unique id of the radio button to select in this group
*
* @see #getCheckedRadioButtonId()
* @see #clearCheck()
*/
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);
}
-1是RadioGroup的初始值,也是什么都不选中状态的值。mCheckId是当前选中的Button的Id,所以此时认为不是-1的(不选中调用clearCheck也没有意义),那么我们接着看setCheckedStateForView方法是做什么的。
private void setCheckedStateForView(int viewId, boolean checked) {
View checkedView = findViewById(viewId);
if (checkedView != null && checkedView instanceof RadioButton) {
((RadioButton) checkedView).setChecked(checked);
}
}
和名称一样,就是改变选中Button的状态,然后继续跟进看后面有什么动作。
/**
* Changes the checked state of this button.
*
* @param checked true to check the button, false to uncheck it
*/
@Override
public void setChecked(boolean checked) {
if (mChecked != checked) {
mCheckedFromResource = false;
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);
}
final AutofillManager afm = mContext.getSystemService(AutofillManager.class);
if (afm != null) {
afm.notifyValueChanged(this);
}
mBroadcasting = false;
}
}
这个方法是RadioButton的父类CompoundButton的,看起来有可能和RadioGroup交互的地方就是那两个回调了。
没错!重点就在mOnCheckChangeWidgetListener上面,这个回调是在RadioGroup上面设置过的,我们在RadioGroup的源码中搜索setOnCheckedChangeWidgetListener,可以看到在onChildViewAdded的回调中有添加监听的代码,看名字就知道是在添加RadioButton后设置的回调。
/**
* {@inheritDoc}
*/
@Override
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);
}
}
继续寻找这个mChildOnCheckedChangeListener的实现是什么样的,可以看到在init的时候,初始化成了CheckedStateTracker对象,我们来看CheckedStateTracker是如何实现的。
private class CheckedStateTracker implements CompoundButton.OnCheckedChangeListener {
@Override
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);
}
}
最后,将会执行setCheckedId方法,这个方法在最初调用check(-1)的时候,最后一行也是这样的,所以这个方法被调用了两次,先传入被取消checked的id,再传入-1,我们来看一下具体实现。
private void setCheckedId(@IdRes int id) {
mCheckedId = id;
if (mOnCheckedChangeListener != null) {
mOnCheckedChangeListener.onCheckedChanged(this, mCheckedId);
}
final AutofillManager afm = mContext.getSystemService(AutofillManager.class);
if (afm != null) {
afm.notifyValueChanged(this);
}
}
mOnCheckedChangeListener.onCheckedChanged(this, mCheckedId);疑惑解决,在这里进行了对应的回调,这就是我们看到的两次回调。
根据源码我们看到,只有在使用check方法的时候,才会调用两次setCheckedId方法,所以平时可以直接对RadioButton进行setChecked。
知道原因之后,我们的解决方案就很简单了。
个人觉得比较合适的一种:
我们可以在OnCheckedChangeListener的回调中对相应的场景做一下处理,简单的示例代码如下:
rg.setOnCheckedChangeListener(new RadioGroup.OnCheckedChangeListener() {
@Override
public void onCheckedChanged(RadioGroup radioGroup, int i) {
if(-1 == i) {
//此处是清除选中的回调。
return;
}
View checkedView = findViewById(i);
if (checkedView != null && checkedView instanceof RadioButton) {
if(!((RadioButton) checkedView).isChecked()) {
//此处是某个选中按钮被取消的回调,在调用check方法修改选中的时候会触发
return;
}
}
//正常选中我们要进行的处理
}
});
}