退出选择模式后,ListView的选择仍然保持不变

42

我有一个ListView子类,当上下文操作栏(CAB)处于活动状态时,允许在其上进行选择。 CAB被设置为onItemLongClick事件的回调函数:

public boolean onCreateActionMode(ActionMode mode, Menu menu) {
    // Inflate a menu resource providing context menu items
    MenuInflater inflater = mode.getMenuInflater();
    inflater.inflate(context_menu, menu);
    getListView().setChoiceMode(ListView.CHOICE_MODE_SINGLE);
    return true;
}

这很好,ListView按预期工作,并且当前选定的项目在触摸时保持高亮显示。

当我关闭 CAB 时,我希望 ListView 返回正常状态(即触摸模式)。问题在于,无论我尝试使用什么方法来清除它,最后选择的项都会无限期地保持高亮显示:

public void onDestroyActionMode(ActionMode mode) {
    //Unselect any rows
    ListView lv = getListView();
    lv.clearChoices(); // Has no effect
    lv.setChoiceMode(ListView.CHOICE_MODE_NONE); // Has no effect on the highlighted item 
    lv.setFocusable(false); // Has no effect
    lv.setSelection(0); // Has no effect
    mActionMode = null;
}

有什么建议吗?

11个回答

35
主要问题的原因在于一旦 ListView 的选择模式切换为 CHOICE_MODE_NONE,框架会将 clear 操作优化掉,因为它不再支持“选择”。我通过手动清除选择状态并延迟设置模式的方式稍微改进了上述解决方法,这样框架就有机会在将模式切换为 CHOICE_MODE_NONE 之前清除状态。
final ListView lv = getListView();
lv.clearChoices();
for (int i = 0; i < lv.getCount(); i++)
    lv.setItemChecked(i, false);
lv.post(new Runnable() {
    @Override
    public void run() {
        lv.setChoiceMode(ListView.CHOICE_MODE_NONE);
    }
});

这实际上是做这个的最好、最不破坏性的方式。 - nickmartens1980
1
当我这样做时,我得到了 StackOverflowErrors。调用 setItemChecked() 会再次触发 onDestroyActionMode() - CommonsWare
getChildCount() 不同于 getCount()getChildCount() 包括头部和尾部,并且仅包括列表中可见的视图(从0开始,无论您在列表中滚动到哪个位置)。setItemChecked() 接受一个 position 参数,该参数与子项索引表示的位置不同。 - Joe
2
实际上,在这里我并没有看到 lv.clearChoices() 是必需的,即使没有它,代码对我来说也可以正常运行。 - Or B
根据Joe的建议,修复了代码以使用getCount()。 - Rudi
显示剩余3条评论

21
我遇到了同样的问题,由于请求布局对我也没有解决问题,所以我实现了一个小技巧来解决。也许这是同样的问题,因为我在 CHOICE_MODE_SINGLECHOICE_MODE_NONE 之间切换。
当操作模式结束时,我调用这段代码。 clearChoices 确保所有项目都不再被选中(内部上)。对视图进行迭代可以确保所有当前可见的视图都被重置并且不再被选中。
mListView.clearChoices();

for (int i = 0; i < mListView.getChildCount(); i++) {
    ((Checkable) mListView.getChildAt(i)).setChecked(false);
}

mListView.setChoiceMode(ListView.CHOICE_MODE_NONE);

我认为这是针对API版本早于11且没有AbsListView.MultiChoiceModeListener的正确答案。 - dzeikei
3
因为我的ListItems是RelativeLayouts,所以我无法让它们工作。有什么建议吗? - AlexIIP
谢谢,我认为这是唯一的选择。 - S.D.
嗨@S.D.,你可以尝试这个。 'code' for (int i = 0; i < mListView.getCount(); i++) mListView.semidetached(i, false); - hims_3009

18
在查看ListView源代码后,唯一的解决方法是将ListView设置为CHOICE_MODE_NONE,然后重新分配ListAdapter(这将清除内部选择列表,无论选择模式如何)。即在ListFragment/ListActivity中。
getListView().setChoiceMode(ListView.CHOICE_MODE_NONE);
getListView().setAdapter(getListAdapter())

3
非常感谢!重置适配器就解决了问题,我认为这个方法也适用于所有的API版本…… - Takhion
1
一切都很好,只是ListView会滚动到最顶部的位置。 - Dmitry Zaytsev

4

我在API Level 17遇到了这个问题,并通过以下方式解决:

listView.clearChoices();
listView.invalidateViews();

2

对我来说,接受的答案似乎对于不可见的项目无效,而且没有必要调用

for (int i = 0; i < lv.getCount(); i++)
        lv.setItemChecked(i, false);

相反,只需调用

lv.requestLayout();

为了完全解决我的问题,我调用了以下代码:
lv.clearChoices();
lv.requestLayout();

onDestroyActionMode()中,并调用。
lv.setItemChecked(position, false)

onItemClick() 方法在非 ActionMode 的情况下触发。

然而,我并没有确定调用 setItemChecked() 是否会导致性能问题。


1
这已经被记录为一个AOSP bug,但由于某些原因被标记为过时。
通常情况下,您会期望它能够正常工作:
getListView().clearChoices();
getListView().setChoiceMode(ListView.CHOICE_MODE_NONE);

很抱歉,它并没有。在下一个布局传递中推迟设置选择模式为无将起作用:
getListView().clearChoices();
getListView().post(new Runnable() {
    @Override
    public void run() {
        getListView().setChoiceMode(ListView.CHOICE_MODE_NONE);
    }
});

0

不确定是否已经太晚,只是想分享一下。我创建了一个意图到同一页,这样一旦捕获到点击数据,它就会重新创建一个新的页面,没有任何点击持久性。


0

我发现在这里(API 19)只有两种方法有效:

  • 重置列表适配器,但不希望这样做因为会回到列表顶部;
  • new Runnable中设置选择模式为CHOICE_MODE_NONE

如果没有使用listView.post(new Runnable())更改选择模式,则它不起作用。有没有人能解释一下为什么呢?

抱歉没有进行评论,我没有声誉。

谢谢。


0

我已经尝试了上述所有的方法,但是它们对我都没有起作用。最后,我决定采用以下的解决方法。关键思想是:

在多模式下,我们将创建一个全新的视图而不是重用“缓存”的视图。虽然效率不高,但至少“部分地”解决了我的问题。

下面是我自定义的ArrayAdapter的代码:

@Override
public View getView(int position, View convertView, ViewGroup parent) {
    // Key to solve this problem. When we are in multimode, we will not reusing the cached view.
    View rowView = this.multimode ? null : convertView;

    if (rowView == null) {
        LayoutInflater inflater = activity.getLayoutInflater();
        rowView = inflater.inflate(R.layout.watchlist_row_layout, null);
        ViewHolder viewHolder = new ViewHolder();
        viewHolder.textView0 = (TextView) rowView.findViewById(R.id.text_view_0);
        viewHolder.textView1 = (TextView) rowView.findViewById(R.id.text_view_1);
        viewHolder.textView2 = (TextView) rowView.findViewById(R.id.text_view_2);
        rowView.setTag(viewHolder);
    }

此外,我觉得将以下代码放在ActionMode.Callback中会更安全,尽管我不确定它有多大帮助。
    @Override
    public void onDestroyActionMode(ActionMode mode) {
        MyFragment.this.myArrayAdapter.setMultimode(false);

        // https://dev59.com/xmkw5IYBdhLWcg3wn8CO
        // Using View.post is the key to solve the problem.
        final ListView listView = MyFragment.this.getListView();
        listView.clearChoices();
        for (int i = 0, ei = listView.getChildCount(); i < ei; i++) {
            listView.setItemChecked(i, false);
        }
        listView.post(new Runnable() {
            @Override
            public void run() {
                listView.setChoiceMode(ListView.CHOICE_MODE_NONE);
            }
        });
        actionMode = null;
    }

侧面说明

使用MultiChoiceModeListener和CHOICE_MODE_MULTIPLE_MODAL相结合将使此错误消失。 但是,API级别低于11的设备将无法使用此解决方案。


请注意,这并不能完全消除问题,只能减少其发生的频率。 - Cheok Yan Cheng

0

我知道这个问题已经有答案了,但是之前的回答在ListView维护的缓存/回收视图方面仍然给我带来了问题,当滚动回到视图时,它们的状态没有更新。

因此,上述解决方案稍作修改:

    lv.clearChoices();  

    ArrayList<View> list = new ArrayList<View>();
    lv.reclaimViews(list);
    for (View view : list) {
        ((Checkable) view).setChecked(false);
    }

    lv.setChoiceMode(lv.CHOICE_MODE_NONE);

使用这种方法比使用getChildAt(i)更好,因为后者只会给你当前可见的视图,而不考虑那些不可见的内部缓存视图。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接