嵌套垂直RecyclerView内部ViewHolder无法回收的解决方案咨询
这种嵌套RecyclerView的坑我之前踩过好几次!核心问题就是内层RecyclerView完全依赖外层滚动,自身没有触发滚动事件,导致它的LayoutManager认为所有条目都在可视范围内,根本不会去detach和回收ViewHolder,结果就是一次性加载所有内层条目,性能拉胯。
下面给你几个可行的解决方案,按推荐程度排序:
方案1:彻底抛弃嵌套,用单一RecyclerView实现多类型布局
这是最优解,完全规避嵌套RecyclerView的性能问题,还能完美复用原生的视图回收机制。思路是把外层Grid项和内层瀑布流条目都作为同一个RecyclerView的不同类型条目,通过GridLayoutManager的SpanSizeLookup来控制不同类型的占列数,模拟出嵌套的视觉效果。
代码示例:
首先定义多类型Adapter:
class CombinedAdapter(private val dataList: List<Any>) : RecyclerView.Adapter<RecyclerView.ViewHolder>() { companion object { private const val TYPE_PARENT_GRID = 0 private const val TYPE_CHILD_STAGGERED = 1 } override fun getItemViewType(position: Int): Int { return when (dataList[position]) { is ParentGridItem -> TYPE_PARENT_GRID is ChildStaggeredItem -> TYPE_CHILD_STAGGERED else -> throw IllegalArgumentException("Unknown item type") } } override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { val inflater = LayoutInflater.from(parent.context) return when (viewType) { TYPE_PARENT_GRID -> ParentViewHolder(inflater.inflate(R.layout.item_parent_grid, parent, false)) TYPE_CHILD_STAGGERED -> ChildViewHolder(inflater.inflate(R.layout.item_child_staggered, parent, false)) else -> throw IllegalArgumentException("Unknown view type") } } override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { when (holder) { is ParentViewHolder -> holder.bind(dataList[position] as ParentGridItem) is ChildViewHolder -> holder.bind(dataList[position] as ChildStaggeredItem) } } override fun getItemCount(): Int = dataList.size // 父Grid项ViewHolder inner class ParentViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { fun bind(item: ParentGridItem) { // 绑定父项数据,比如标题、分类等 itemView.tv_title.text = item.title } } // 子瀑布流项ViewHolder inner class ChildViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { fun bind(item: ChildStaggeredItem) { // 绑定子项数据,比如图片、描述等 Glide.with(itemView).load(item.imageUrl).into(itemView.iv_image) itemView.tv_desc.text = item.desc } } }
然后配置GridLayoutManager,控制不同类型的占列数:
val spanCount = 2 // 外层Grid的列数 val layoutManager = GridLayoutManager(context, spanCount) layoutManager.spanSizeLookup = object : GridLayoutManager.SpanSizeLookup() { override fun getSpanSize(position: Int): Int { return when (combinedAdapter.getItemViewType(position)) { CombinedAdapter.TYPE_PARENT_GRID -> spanCount // 父项占满所有列 CombinedAdapter.TYPE_CHILD_STAGGERED -> 1 // 子项占1列 else -> 1 } } } recyclerView.apply { this.layoutManager = layoutManager adapter = combinedAdapter }
这种方式完全利用RecyclerView原生的回收机制,没有嵌套的性能损耗,维护起来也更简单,强烈推荐!
方案2:让内层RecyclerView感知外层滚动,触发自身回收
如果必须保留嵌套结构,那就要手动让内层RecyclerView知道自己什么时候不在可视范围内,从而触发ViewHolder回收。可以通过两种方式实现:
方式A:自定义内层LayoutManager
重写StaggeredLayoutManager,让它判断自身是否在父RecyclerView的可视区域,不在的话直接回收所有子View;在可视区域内时,只布局可见范围内的条目。
class NonScrollStaggeredLayoutManager(spanCount: Int) : StaggeredGridLayoutManager(spanCount, VERTICAL) { override fun canScrollVertically(): Boolean { return false // 禁止内层滚动 } override fun onLayoutChildren(recycler: RecyclerView.Recycler, state: RecyclerView.State) { // 获取当前内层RV在屏幕上的可视区域 val globalVisibleRect = Rect() val isVisible = (parent as? View)?.getGlobalVisibleRect(globalVisibleRect) ?: false if (!isVisible) { // 不在可视区域,回收所有子View detachAndScrapAttachedViews(recycler) return } // 转换为内层RV的局部坐标 val localVisibleRect = Rect() globalVisibleRect.offset(-left, -top) // 先执行默认布局 super.onLayoutChildren(recycler, state) // 回收超出局部可视区域的子View for (i in 0 until childCount) { val child = getChildAt(i) ?: continue val childRect = Rect() child.getGlobalVisibleRect(childRect) if (!Rect.intersects(globalVisibleRect, childRect)) { detachAndScrapView(child, recycler) } } } }
然后给内层RecyclerView设置这个自定义LayoutManager即可。
方式B:外层滚动监听,触发内层重新计算
给外层RecyclerView添加滚动监听,遍历所有可见的父ViewHolder,通知内层RecyclerView重新布局,强制它触发回收逻辑。
outerRecyclerView.addOnScrollListener(object : RecyclerView.OnScrollListener() { override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) { super.onScrolled(recyclerView, dx, dy) val gridLayoutManager = recyclerView.layoutManager as GridLayoutManager val firstVisiblePos = gridLayoutManager.findFirstVisibleItemPosition() val lastVisiblePos = gridLayoutManager.findLastVisibleItemPosition() // 遍历所有可见的父ViewHolder for (pos in firstVisiblePos..lastVisiblePos) { val parentViewHolder = recyclerView.findViewHolderForAdapterPosition(pos) as? ParentViewHolder parentViewHolder?.innerRecyclerView?.run { // 强制重新布局,触发内层LayoutManager的回收逻辑 requestLayout() (layoutManager as? StaggeredGridLayoutManager)?.invalidateSpanAssignments() } } } })
方案3:手动管理内层RecyclerView的回收(不推荐)
如果上面两种方案都不适用,可以在父Adapter的onViewDetachedFromWindow和onViewAttachedToWindow方法中,手动绑定和解绑内层RecyclerView的数据,让它在不可见时清空条目,回收ViewHolder。
class ParentAdapter(private val dataList: List<ParentGridItem>) : RecyclerView.Adapter<ParentAdapter.ParentViewHolder>() { inner class ParentViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { private val innerRv: RecyclerView = itemView.findViewById(R.id.inner_rv) private lateinit var innerAdapter: InnerAdapter fun bind(item: ParentGridItem) { innerAdapter = InnerAdapter(item.childItems) innerRv.adapter = innerAdapter } fun clear() { // 清空数据,触发内层回收 innerAdapter.submitList(emptyList()) innerRv.adapter = null } } override fun onViewAttachedToWindow(holder: ParentViewHolder) { super.onViewAttachedToWindow(holder) val item = dataList[holder.adapterPosition] holder.bind(item) } override fun onViewDetachedFromWindow(holder: ParentViewHolder) { super.onViewDetachedFromWindow(holder) holder.clear() } // 其他基础方法省略 }
这种方式虽然能实现回收,但容易出现数据闪烁,维护成本高,只作为最后的备选。
内容的提问来源于stack exchange,提问作者deorez

