Android列表点击单项触发展开逻辑时,出现全视图异常刷新问题
问题根因定位
两个直接原因导致了全量刷新的异常表现:
- 当前使用的
notifyDataSetChanged()是Adapter层级粒度最粗的刷新方法,调用后会强制让RecyclerView把所有可见条目重新执行完整的onBindViewHolder流程,不管条目实际状态有没有变化,天生会引发全量视图重绘。 TransitionManager.beginDelayedTransition(root)的作用域绑定在了整个页面的根ViewGroup上,只要容器内有视图发生尺寸、可见性变化,整个根容器下的所有视图都会被纳入动画过渡的计算范围,进一步放大了全量刷新的观感,看起来就像整个页面被重新加载了一样。
另外现有点击判断逻辑还有隐性错位风险:直接使用bind阶段传入的position值、临时存储的isExpanded值做判断,在列表快速滑动、条目复用的时候很容易出现点击位置和实际展开位置不匹配的问题。
具体修正方案
1. 替换粗粒度的全量刷新,改用带Payload的局部更新
- 定义一个固定的刷新标识,用来标记这次刷新只和展开状态有关:
companion object { const val PAYLOAD_EXPAND_STATE = "expand_state_update" }
- 点击事件回调里不要直接调用
notifyDataSetChanged(),改为记录新旧展开位置,只对状态发生变化的两个条目发局部刷新指令:
holder.itemView.setOnClickListener { // 优先拿bindingAdapterPosition,避免复用导致的position错位 val clickPos = holder.bindingAdapterPosition if (clickPos == RecyclerView.NO_POSITION) return@setOnClickListener val oldPos = mExpandedPosition mExpandedPosition = if (clickPos == mExpandedPosition) -1 else clickPos // 只刷新发生状态变化的条目,其他无变化的条目完全不触发重绘 if (oldPos != -1) { notifyItemChanged(oldPos, PAYLOAD_EXPAND_STATE) } if (mExpandedPosition != -1) { notifyItemChanged(mExpandedPosition, PAYLOAD_EXPAND_STATE) } }
- 在Adapter里重写带payload的绑定方法,只更新和展开相关的UI:
override fun onBindViewHolder(holder: YourViewHolder, position: Int, payloads: MutableList<Any>) { if (payloads.contains(PAYLOAD_EXPAND_STATE)) { // 把过渡动画的作用域收敛到当前条目自己的视图上,不要波及全局root TransitionManager.beginDelayedTransition(holder.itemView) val expandVisible = position == mExpandedPosition holder.expandArea.visibility = if (expandVisible) View.VISIBLE else View.GONE // 其他和展开状态无关的控件逻辑完全不需要重复执行 return } // 非局部刷新场景,走原来的完整绑定逻辑即可 super.onBindViewHolder(holder, position, payloads) }
2. 修正动画的作用域
把原来写在点击回调里的TransitionManager.beginDelayedTransition(root)删掉,按上面的代码逻辑把动画启动的位置移到单个条目的视图上,让过渡动画的计算范围只覆盖当前条目内部的控件变化,不会触发整个页面的重算。
改完的实际效果
- 只有点击的条目、之前处于展开状态的旧条目会被更新,其他无状态变化的条目完全不会触发重新绑定、重绘
- 展开/收起的过渡动画只在单个条目内部生效,不会出现整个页面跟着闪、整体刷新的观感
- 相比原来全量刷新的实现,列表滚动、点击的流畅度会有明显提升,不会因为每次点击都遍历全量条目引发卡顿。
内容的提问来源于stack exchange,提问作者CrackCode
相关产品推荐
相关产品推荐

