1、概述
今天在写颜色识别的Demo 时有个场景是需要用户做出单项选择,脑中蹦出首选的方案就是 ListView 配合 ChoiceMode。
但实际在编写过程中却出了问题 :ListView 中的 OnItemSelectedListener 没有从 ListView 中接收回调。出现问题并不可怕,可怕的是对问题视而不见的态度。
2、解决问题
2.1、OnItemSelectedListener的定义
OnItemSelectedListener 是当视图被选中
时会触发的回调。
- onItemSelected 只有当状态与前一个状态不同时,才会触发。
- onNothingSelected 当视图不可见或者数据源为空时会触发该方法。
public abstract class AdapterView extends ViewGroup {
public interface OnItemSelectedListener {
void onItemSelected(AdapterView> parent, View view, int position, long id);
void onNothingSelected(AdapterView> parent);
}
}
2.2、设置 OnItemSelectedListener
通过调用setOnItemSelectedListener()
方法为mOnItemSelectedListener
初始化。通过查询onItemSelected()
方法的调用,追踪到下面方法。
// AdapterView
private void fireOnSelected() {
if (mOnItemSelectedListener == null) {
return;
}
final int selection = getSelectedItemPosition();
if (selection >= 0) {
View v = getSelectedView();
mOnItemSelectedListener.onItemSelected(this, v, selection,
getAdapter().getItemId(selection));
} else {
mOnItemSelectedListener.onNothingSelected(this);
}
}
最终是由fireOnSelected()
方法封装了对事件的回调,接着查到是dispatchOnItemSelected ()
中调用了fireOnSelected()
方法。
// AdapterView
private void dispatchOnItemSelected() {
fireOnSelected();
performAccessibilityActionsOnSelected();
}
接着跟踪dispatchOnItemSelected()
方法,查找到两处使用场景。
// 使用场景1
private class SelectionNotifier implements Runnable {
public void run() {
mPendingSelectionNotifier = null;
if (mDataChanged && getViewRootImpl() != null
&& getViewRootImpl().isLayoutRequested()) {
if (getAdapter() != null) {
mPendingSelectionNotifier = this;
}
} else {
dispatchOnItemSelected();
}
}
}
// 使用场景2
void selectionChanged() {
mPendingSelectionNotifier = null;
if (mOnItemSelectedListener != null
|| AccessibilityManager.getInstance(mContext).isEnabled()) {
if (mInLayout || mBlockLayoutRequests) {
if (mSelectionNotifier == null) {
mSelectionNotifier = new SelectionNotifier();
} else {
removeCallbacks(mSelectionNotifier);
}
post(mSelectionNotifier);
} else {
dispatchOnItemSelected();
}
}
}
2.2.1、使用场景 SelectionNotifier
因为类SelectionNotifier
是私有访问权限,所以只需要在当前的类(AdapterView
)中查找即可。最终发现SelectionNotifier
的创建竟然在selectionChanged()
方法中,所以我们可以直接对下一个场景展开分析了。
2.2.2、使用场景 selectionChanged()
通过查找方法selectionChanged()
调用,我们定位到方法checkSelectionChanged()
。
void checkSelectionChanged() {
if ((mSelectedPosition != mOldSelectedPosition) || (mSelectedRowId != mOldSelectedRowId)) {
selectionChanged();
mOldSelectedPosition = mSelectedPosition;
mOldSelectedRowId = mSelectedRowId;
}
if (mPendingSelectionNotifier != null) {
mPendingSelectionNotifier.run();
}
}
继续对方法checkSelectionChanged()
展开搜索,定位到下面代码块。handleDataChanged()
的代码比较多,就不贴出代码了。
// AdapterView
void handleDataChanged() {
// 1、获取待新选中的位置
// 2、如果选中的位置与之前的位置是同一个位置,就不触发视图更新了
// 3、如果选中的位置与之前不一样,设置当前位置为新的位置。并通知分发位置改变事件。
// 4、如果发现没有选中位置的匹配项,则会分发一个未选择的事件。
}
3、4
都会最终触发checkSelectionChanged()
事件,所以问题的关键变成了谁调用了handleDataChanged()
方法。
2.3、追踪handleDataChanged()方法
我们在 ListView 中找到了对handleDataChanged()方法的调用,我们发现两条线索触发了对handleDataChanged()方法的调用。
1、layoutChildren() 方法的调用
2、mDataChanged的取值
@Override
protected void layoutChildren() {
// ...
boolean dataChanged = mDataChanged;
if (dataChanged) {
handleDataChanged();
}
// ...
}
2.3.1、layoutChildren() 方法的调用
我们观察到选中的三个方法会调用了layoutChildren() 方法,这三个方法分别是:
1、setSelectionInt() —— 最终被commonKey()调用
2、commonKey() —— 最终被onKeyMultiple()调用
3、onFocusChanged()
在 ListView 中layoutChildren() 方法 与 点击事件 扯不上半点关系,反而跟 Focus 纠缠不清。
2.3.2、mDataChanged的取值
当数据变更或者失效的时候都会引起mDataChanged值的变更。
// AdapterView
class AdapterDataSetObserver extends DataSetObserver {
// ...
@Override
public void onChanged() {
mDataChanged = true;
}
// ...
@Override
public void onInvalidated() {
mDataChanged = true;
// ...
}
}
总结
在 ListView 中点击 Item 并没有调用 OnItemSelectListener回调,所以最开始期望以注册OnItemSelectListener来接收点击 Item 的回调行为是不成立的。
至少在ListView中 OnItemSelectListener是用于接收焦点的变化的。
解决办法
1、在 ChoiceMode 是 CHOICE_MODE_SINGLE的情况下,你可以选择使用方法getCheckedItemPosition()
。
public int getCheckedItemPosition() {
if (mChoiceMode == CHOICE_MODE_SINGLE && mCheckStates != null && mCheckStates.size() == 1) {
return mCheckStates.keyAt(0);
}
return INVALID_POSITION;
}
如果是其他 ChoideMode,则可以选择getCheckedItemPositions()
方法。
public SparseBooleanArray getCheckedItemPositions() {
if (mChoiceMode != CHOICE_MODE_NONE) {
return mCheckStates;
}
return null;
}