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

嵌套RecyclerView添加父项后ItemDecoration边距异常问题

实现逻辑

我用DelegateAdapters在父RecyclerView中展示不同分区,初始添加两个带标题和横向嵌套列表的分区(AuthorTracksDelegateItem和NewReleasesDelegateItem),同时通过观察LiveData,在有新曲目时添加第三个分区(PlayerDelegateItem),代码如下:

fun setAdapter(recyclerView: RecyclerView){
    //Add sections
    val testData = listOf(
        AuthorTracksDelegateItem(title = "Beatles", tracks = homeViewModel.getTracks("Beatles")),
        NewReleasesDelegateItem(title = "New releases", albums = homeViewModel.getNewReleases()),
    )
    compositeAdapter.setItems(testData)
    
    //Observe and add new section
    homeViewModel.getTracks("Imagine dragons").observe(viewLifecycleOwner) { tracks ->
        if(tracks.isNotEmpty()){
            compositeAdapter.addToEnd(PlayerDelegateItem(title = "Last play", content = tracks.first()))
        }
    }
    
    //other
    recyclerView.layoutManager = LinearLayoutManager(context)
    recyclerView.adapter = compositeAdapter
    val padding = resources.getDimensionPixelSize(R.dimen.section_margin)
    recyclerView.addItemDecoration(MarginItemDecoration(padding, padding*10, padding))
}

随后在ViewHolder中创建嵌套横向列表,代码如下:

inner class NewReleasesViewHolder(itemView: View):
    DelegateViewHolder(itemView)
{
    private val title = itemView.findViewById<TextView>(R.id.section_title)
    private val recyclerView = itemView.findViewById<RecyclerView>(R.id.rv_horizontal_tracks)
    private val adapter = NewReleasesAdapter()

    override fun bind(item: IDelegateAdapterItem) {
        title.text = item.id() as String
        (item.content() as LiveData<List<Album>>).observeForever { albums ->
            adapter.setItems(albums)
        }
        setupRecycleView()
    }

    private fun setupRecycleView(){
        val layout = LinearLayoutManager(itemView.context)
        layout.orientation = LinearLayoutManager.HORIZONTAL
        recyclerView.adapter = adapter
        recyclerView.layoutManager = layout
        recyclerView.addItemDecoration(
            MarginItemDecoration(
                startSpace = itemView.resources.getDimensionPixelSize(R.dimen.content_horizontal_margin),
                endSpace = itemView.resources.getDimensionPixelSize(R.dimen.content_horizontal_margin),
                betweenSpace = itemView.resources.getDimensionPixelSize(R.dimen.items_margin),
                isHorizontal = true
            )
        )
    }
}

override fun createViewHolder(binding: View): RecyclerView.ViewHolder {
    return NewReleasesViewHolder(binding)
}

override fun getLayoutId(): Int {
    return R.layout.section_nested_list
}

inner class TrackViewHolder(itemView: View): DelegateViewHolder(itemView){
    val title = itemView.findViewById<TextView>(R.id.section_title)
    val recyclerView = itemView.findViewById<RecyclerView>(R.id.rv_horizontal_tracks)
    val adapter = LargeTracksAdapter()

    override fun bind(item: IDelegateAdapterItem) {
        title.text = item.id() as String
        (item.content() as LiveData<List<Track>>).observeForever { tracks ->
            adapter.setItems(tracks)
        }
        setupRecycleView()
    }

    fun setupRecycleView(){
        val layout = LinearLayoutManager(itemView.context)
        layout.orientation = LinearLayoutManager.HORIZONTAL
        recyclerView.layoutManager = layout
        recyclerView.adapter = adapter
        recyclerView.addItemDecoration(MarginItemDecoration(
            startSpace = itemView.resources.getDimensionPixelSize(R.dimen.content_horizontal_margin),
            endSpace =  itemView.resources.getDimensionPixelSize(R.dimen.content_horizontal_margin),
            betweenSpace = itemView.resources.getDimensionPixelSize(R.dimen.items_margin),
            isHorizontal = true
        ))
        recyclerView.invalidateItemDecorations()
    }
}

我用MarginItemDecoration设置元素间边距,代码如下:

class MarginItemDecoration(
private val startSpace: Int = 0,
private val endSpace: Int = 0,
private var betweenSpace: Int = 0,
private val isHorizontal: Boolean = false): RecyclerView.ItemDecoration() {

init {
    betweenSpace /= 2
}

override fun getItemOffsets(
    outRect: Rect,
    view: View,
    parent: RecyclerView,
    state: RecyclerView.State
) {
    val size = parent.adapter?.itemCount ?: 0
    val itemPosition =  parent.getChildAdapterPosition(view)
    if (isHorizontal) {
        outRect.left = getStartSpace(itemPosition)
        outRect.right = getEndSpace(itemPosition, size)
    } else {
        outRect.top = getStartSpace(itemPosition)
        outRect.bottom = getEndSpace(itemPosition, size)
    }
}

private fun getStartSpace(
    itemPosition: Int
): Int = when (itemPosition) {
    0 -> startSpace
    else -> betweenSpace
}

private fun getEndSpace(
    itemPosition: Int,
    size: Int,
): Int = when (itemPosition) {
    size-1 -> endSpace
    else -> betweenSpace
}
}

问题现象

尽管使用dimensions.xml中的固定值,部分分区的嵌套列表边距却翻倍。添加多个分区后出现更复杂的异常:

//Add sections
val testData = listOf(
    AuthorTracksDelegateItem(title = "Beatles", tracks = homeViewModel.getTracks("Beatles")),
    NewReleasesDelegateItem(title = "New releases", albums = homeViewModel.getNewReleases()),
    AuthorTracksDelegateItem(title = "Linkin Park", tracks = homeViewModel.getTracks("Linkin Park")),
    AuthorTracksDelegateItem(title = "21 pilots", tracks = homeViewModel.getTracks("21 pilots")),
)

实际表现:

  • 第一个分区(Beatles)显示正常
  • 第二个分区(New releases)嵌套列表边距翻倍
  • 第三个分区(Linkin Park)嵌套列表边距正常,但自身底部边距异常增大
  • 第四个分区(21 pilots)嵌套列表边距再次异常

已尝试方案

发现父RecyclerView添加新分区后边距异常,尝试延迟添加、调用invalidateItemDecorations()、更换布局和ID均无效。


问题原因与解决方法

核心问题:重复添加ItemDecoration

你的setupRecycleView()方法在每次bind()时都会被调用,而每次调用都会给嵌套RecyclerView添加一个新的MarginItemDecoration。RecyclerView的ItemDecoration是累加的,多次添加就会导致边距翻倍、异常增大的问题。另外,observeForever()未解绑,会触发重复绑定,加剧Decoration重复添加的问题。

具体修复步骤:

  1. 避免重复添加ItemDecoration
    把MarginItemDecoration的初始化移到ViewHolder的构造方法中,只添加一次:
inner class NewReleasesViewHolder(itemView: View): DelegateViewHolder(itemView) {
    private val title = itemView.findViewById<TextView>(R.id.section_title)
    private val recyclerView = itemView.findViewById<RecyclerView>(R.id.rv_horizontal_tracks)
    private val adapter = NewReleasesAdapter()

    init {
        // 初始化时只添加一次Decoration
        setupRecycleView()
    }

    override fun bind(item: IDelegateAdapterItem) {
        title.text = item.id() as String
        val liveData = item.content() as LiveData<List<Album>>
        liveData.observeForever { albums ->
            adapter.setItems(albums)
        }
        // 移除bind中的setupRecycleView调用
    }

    private fun setupRecycleView(){
        val layout = LinearLayoutManager(itemView.context)
        layout.orientation = LinearLayoutManager.HORIZONTAL
        recyclerView.adapter = adapter
        recyclerView.layoutManager = layout
        recyclerView.addItemDecoration(
            MarginItemDecoration(
                startSpace = itemView.resources.getDimensionPixelSize(R.dimen.content_horizontal_margin),
                endSpace = itemView.resources.getDimensionPixelSize(R.dimen.content_horizontal_margin),
                betweenSpace = itemView.resources.getDimensionPixelSize(R.dimen.items_margin),
                isHorizontal = true
            )
        )
    }
}

TrackViewHolder做同样修改,把setupRecycleView()移到构造方法的init块中,不在bind()里重复调用。

  1. 正确处理LiveData的观察
    使用observe()而非observeForever(),并在ViewHolder销毁时移除观察者,避免内存泄漏和重复回调:
// 在ViewHolder中添加观察者引用
private var albumObserver: Observer<List<Album>>? = null

override fun bind(item: IDelegateAdapterItem) {
    title.text = item.id() as String
    val liveData = item.content() as LiveData<List<Album>>
    
    // 先移除之前的观察者
    albumObserver?.let { liveData.removeObserver(it) }
    
    // 创建新的观察者
    albumObserver = Observer { albums ->
        adapter.setItems(albums)
    }
    liveData.observe(viewLifecycleOwner, albumObserver!!)
}

// 在ViewHolder的onViewDetachedFromWindow中移除观察者
override fun onViewDetachedFromWindow() {
    super.onViewDetachedFromWindow()
    val liveData = item.content() as LiveData<List<Album>>
    albumObserver?.let { liveData.removeObserver(it) }
}

若DelegateViewHolder没有onViewDetachedFromWindow方法,需重载该方法,或在合适的生命周期节点解绑观察者。

  1. 调整父RecyclerView的Decoration参数
    父RecyclerView的MarginItemDecoration设置了endSpace = padding*10,过大的值可能导致分区底部边距异常,建议统一使用padding作为endSpace:
recyclerView.addItemDecoration(MarginItemDecoration(padding, padding, padding))

验证修复

修改后重新运行,检查各分区边距是否恢复正常,同时确认内存泄漏问题是否解决。


内容的提问来源于stack exchange,提问作者Ikrom

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.30 10:05:53