如何修复动态适配器包装器 Android 中的 notifyDataSetChanged/ListView 问题

Posted

技术标签:

【中文标题】如何修复动态适配器包装器 Android 中的 notifyDataSetChanged/ListView 问题【英文标题】:How to fix notifyDataSetChanged/ListView problems in dynamic Adapter wrapper Android 【发布时间】:2011-06-01 18:05:23 【问题描述】:

总结:尝试通过自定义适配器包装器将标题行动态添加到 ListView。 ListView 无法保持滚动位置同步。提供可运行的演示项目。

我想根据 CursorAdapter 中的值将项目动态添加到列表中,在用户当前查看的位置之前几个位置。为此,我有一个适配器,它包装 CursorAdapter 并将新内容索引在 SparseArray 中。将项目添加到自定义适配器时需要更新 ListView,但我遇到了很多试图让它工作的陷阱,希望得到一些建议。

演示项目可以在这里下载:DynamicSectionedList.zip

在演示中,通过向前看 10 个位置来动态添加标题,以找到列表项切换到下一个字母的正确位置。 notifyDataSetChanged 的​​每个实现都有描述的问题:

演示 1 这个演示展示了 notifyDataSetChanged() 的重要性。单击任何内容时,应用程序都会崩溃。这是由于 ListView 中的一些健全性检查...mItemCount != adapter.getItemCount()。道德是,我们需要通知列表数据已更改。

演示 2 下一步自然是在发生更改时通知 ListView 更改。不幸的是,在 ListView 滚动时这样做会破坏所有触摸交互,直到应用程序退出触摸模式。您需要“滚动”足够远以生成新标题才能注意到这一点。点击屏幕不会导致滚动停止,一旦停止,列表项将不可点击。这是由于 AbsListView.onTouchEvent() 中的一些 if (!mDataChanged) /* do very important stuff */ 代码造成的。

演示 3 为了解决这个问题,Demo 3 引入了一个 pendingChanges 标志,并且自定义 Adapter 获得了一个 notifyDataSetChangedIfNeeded(),一旦 ListView 进入“安全”状态以进行更改,就可以调用它。必须通知更改的第一点是在 ListView.layoutChildren() 中,因此我重写了该方法,以便在需要时首先通知更改,然后调用。跳过至少一个标题,然后单击一个列表项。

这并不完全正确,尽管我不完全确定为什么。使用键盘/轨迹球点击或选择一个项目会导致列表刷新,而不会正确同步旧位置。它滚动到列表的顶部,这是不可接受的。

演示 4 Demo 3 中的滚动问题是可以克服的,至少在触摸模式下是可以的。通过在触摸时添加对 notifyDataSetChangedIfNeeded() 的调用,数据更改恰好发生在所有触摸交互按预期工作并且列表位置正确同步的时间。

但是,当设备不处于触摸模式时,我找不到类似的东西,更不用说它绝对看起来像是一个 hack。该列表几乎总是滚动回顶部,我无法找出导致它偶尔保持正确位置的原因。

由于 android 在每一步都在与我作斗争,我觉得应该有更好的方法。请尝试演示,如果可以应用任何修复以使其正常工作,那就太好了!

非常感谢任何可以研究此问题的人,希望如果我们可以使代码正常工作,它将对其他尝试对带有标题的列表进行相同优化的人有用。

【问题讨论】:

LinearLayout buttons 正下方的 for 循环似乎没有有效的 Java 语法。您似乎还缺少填充列表的任何代码。我建议您创建一个包含这些内容的完整项目,将其压缩,然后从您的问题中链接到它。即便如此,我也不会让你获得太多帮助,因为这是一大堆难以破译的代码,而且你并没有真正在你的问题中提出任何问题。 您可能会考虑开始第二个问题,扩展真正的问题(“列表中的数据量可能非常大,并且标题的计算可能被认为足够昂贵,需要优化”) .您在这里所拥有的东西与您试图解决实际问题的实际代码有些相关。但是,您可能更容易获得真正问题本身的帮助,弄清楚为什么“计算标题”是“昂贵的”以及如何解决它。当然,你也是在假期周末前的星期四问这个...... 顺便说一句,FWIW,我写了一篇博客文章,表达了我对向CursorAdapter添加标题的想法:commonsware.com/blog/2010/12/30/… @commonsware 感谢您抽出宝贵时间回复,并感谢您的博文。你在那里提到的一些事情解决了我没有正确解决的其他问题。在我的特殊情况下,“真正的问题”可以通过一些官僚决策和/或通过优化我们目前使用的航向预计算方法来解决。但是,出于这个和其他目的,似乎应该可以通过这种方式动态添加到列表中,我只需要帮助找出 notifyDataSetChanged。 【参考方案1】:

上述方法的主要问题是数据集是直接从超类执行的代码中更改的。通过填充 getView() 中的标题,我正在更改可能是操作中间的数据或超类中的“不安全”状态。这就是为什么在快速滚动期间调用 onNotifyDataSetChanged 时所有触摸交互都会中断的原因。数据需要从外部源添加,而不是从适配器方法中添加。

这可以通过使用 Handler 或 AsyncTask 将标题添加到适配器并调用其 notifyDataSetChanged 来完成。这些将在 UI 线程上运行,并且不会干扰超类状态。感谢 CommonsWare 的 EndlessAdapter 示例引入了 AsyncTask 的概念来将数据添加到列表中。

进行该更改后,不再需要 onTouchEvent() 和 layoutChildren() hack。

【讨论】:

以上是关于如何修复动态适配器包装器 Android 中的 notifyDataSetChanged/ListView 问题的主要内容,如果未能解决你的问题,请参考以下文章

IO流的设计模式

Android 的 BaseAdapter 是适配器模式的一个例子吗?

可滚动 div 和固定底部 div 如何在包装器内动态更改高度?

在 Android 中动态禁用微调器项目

装饰器、包装器和适配器模式之间有啥区别?

运行时动态修复dex