You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

嵌套垂直RecyclerView内部ViewHolder无法回收的解决方案咨询

解决嵌套RecyclerView的视图回收问题

这种嵌套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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.22 09:49:18