旧问新解·ListView 中的 OnItemSelectedListener 不生效

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() 方法的调用

旧问新解·ListView 中的 OnItemSelectedListener 不生效_第1张图片
layoutChildren() 的方法调用

我们观察到选中的三个方法会调用了layoutChildren() 方法,这三个方法分别是:
1、setSelectionInt() —— 最终被commonKey()调用
2、commonKey() —— 最终被onKeyMultiple()调用
3、onFocusChanged()

旧问新解·ListView 中的 OnItemSelectedListener 不生效_第2张图片
黑人问号脸

在 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;
    }

你可能感兴趣的:(旧问新解·ListView 中的 OnItemSelectedListener 不生效)