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

Android类似Trello的横向嵌套纵向RecyclerView实现方案咨询

实现类似Trello的横向嵌套纵向RecyclerView布局

嘿,要做这种Trello风格的布局其实不难,但性能优化是重中之重——毕竟嵌套RecyclerView很容易出现预加载过度、视图复用差的问题。下面我给你一步步拆解实现方案,重点讲怎么保证不可见项的复用,避免不必要的性能消耗:

1. 先搭好基础布局结构

父容器:横向RecyclerView

首先是外层的横向滑动列表,直接用RecyclerView+横向LinearLayoutManager就行:

<!-- activity_main.xml -->
<androidx.recyclerview.widget.RecyclerView
    android:id="@+id/horizontal_recycler"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
    app:orientation="horizontal" />

子Item:纵向RecyclerView(每一列)

每个横向Item就是一个完整的任务列,包含标题和纵向滑动的任务列表:

<!-- item_vertical_list.xml -->
<LinearLayout
    android:layout_width="280dp"
    android:layout_height="match_parent"
    android:orientation="vertical"
    android:padding="8dp">

    <TextView
        android:id="@+id/tv_list_title"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:textSize="18sp"
        android:textStyle="bold"/>

    <androidx.recyclerview.widget.RecyclerView
        android:id="@+id/vertical_recycler"
        android:layout_width="match_parent"
        android:layout_height="0dp"
        android:layout_weight="1"
        android:nestedScrollingEnabled="true"/>
</LinearLayout>

这里nestedScrollingEnabled保持默认的true,确保纵向列表能独立滑动,和父横向滑动互不干扰。

2. 适配器的实现(重点在复用缓存)

父横向适配器:缓存子RecyclerView的配置

父适配器的核心是不要每次绑定都重新创建子纵向RecyclerView的适配器和布局管理器,要在ViewHolder里缓存这些对象,减少重复初始化的开销:

class HorizontalListAdapter(private val listData: List<ListColumnData>) :
    RecyclerView.Adapter<HorizontalListAdapter.HorizontalViewHolder>() {

    inner class HorizontalViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
        val tvTitle: TextView = itemView.findViewById(R.id.tv_list_title)
        val verticalRecycler: RecyclerView = itemView.findViewById(R.id.vertical_recycler)
        // 缓存子适配器,避免每次绑定都新建
        private var verticalAdapter: VerticalItemAdapter? = null

        init {
            // 初始化子RecyclerView的布局管理器,只做一次
            verticalRecycler.layoutManager = LinearLayoutManager(itemView.context)
            verticalRecycler.setHasFixedSize(true)
            // 给子RecyclerView设置回收池,提升Item复用率
            verticalRecycler.recycledViewPool.setMaxRecycledViews(0, 20)
        }

        fun bind(data: ListColumnData) {
            tvTitle.text = data.title
            // 适配器不存在就创建,存在就直接更新数据
            if (verticalAdapter == null) {
                verticalAdapter = VerticalItemAdapter(data.items)
                verticalRecycler.adapter = verticalAdapter
            } else {
                verticalAdapter?.updateData(data.items)
            }
        }
    }

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): HorizontalViewHolder {
        val view = LayoutInflater.from(parent.context)
            .inflate(R.layout.item_vertical_list, parent, false)
        return HorizontalViewHolder(view)
    }

    override fun onBindViewHolder(holder: HorizontalViewHolder, position: Int) {
        holder.bind(listData[position])
    }

    override fun getItemCount() = listData.size
}

子纵向适配器:支持局部数据更新

子适配器要能高效更新数据,尽量用局部刷新代替全局刷新,减少视图重绘:

class VerticalItemAdapter(private var itemList: List<ItemData>) :
    RecyclerView.Adapter<VerticalItemAdapter.VerticalViewHolder>() {

    inner class VerticalViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
        val tvItemContent: TextView = itemView.findViewById(R.id.tv_item_content)
    }

    fun updateData(newList: List<ItemData>) {
        val oldSize = itemList.size
        itemList = newList
        // 局部刷新,避免全局重绘
        when {
            oldSize < newList.size -> notifyItemRangeInserted(oldSize, newList.size - oldSize)
            oldSize > newList.size -> notifyItemRangeRemoved(newList.size, oldSize - newList.size)
        }
        notifyItemRangeChanged(0, min(oldSize, newList.size))
    }

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): VerticalViewHolder {
        val view = LayoutInflater.from(parent.context)
            .inflate(R.layout.item_single_task, parent, false)
        return VerticalViewHolder(view)
    }

    override fun onBindViewHolder(holder: VerticalViewHolder, position: Int) {
        holder.tvItemContent.text = itemList[position].content
    }

    override fun getItemCount() = itemList.size
}

3. 性能优化的核心手段(必做!)

要实现不可见项的复用,避免预绘制所有列,必须做好这几点:

3.1 共享RecycledViewPool

让所有子纵向RecyclerView共享同一个回收池,这样不同列的任务Item可以互相复用,大大减少View的创建次数:

// 在Activity/Fragment初始化横向RecyclerView时
val sharedPool = RecyclerView.RecycledViewPool()
// 设置每种ViewType的最大缓存数量,根据你的Item数量调整
sharedPool.setMaxRecycledViews(0, 20)
horizontal_recycler.recycledViewPool = sharedPool

子RecyclerView会自动继承父RecyclerView的回收池,不用额外设置。

3.2 限制父RecyclerView的预加载

RecyclerView默认会预加载可见区域外的Item,这会导致还没滑到的列也提前初始化纵向RecyclerView并加载Item。我们可以自定义LayoutManager来关闭预加载:

class LimitedPreloadLinearLayoutManager(context: Context) : LinearLayoutManager(context, HORIZONTAL, false) {
    override fun getExtraLayoutSpace(state: RecyclerView.State?): Int {
        // 设置为0,只加载当前可见的列
        return 0
    }
}

然后替换父RecyclerView的LayoutManager:

horizontal_recycler.layoutManager = LimitedPreloadLinearLayoutManager(this)

这样只有当列滑入可见区域时,才会初始化它的纵向RecyclerView。

3.3 减少过度绘制

  • 给子纵向RecyclerView设置android:overScrollMode="never",去掉滚动时的边缘光晕效果;
  • 简化子Item的布局,尽量减少嵌套层级;
  • 如果子Item有复杂的非必要视图,用ViewStub延迟加载。

3.4 避免不必要的视图重绘

确保所有View的layout_width、layout_height设置合理,比如子纵向RecyclerView用layout_weight占满剩余空间,避免频繁的布局计算;同时尽量用notifyItemXXX系列的局部刷新方法,代替notifyDataSetChanged()。

4. 滑动冲突处理

一般来说,RecyclerView会自动根据滑动方向判断交给哪个View处理,横向滑动时父RecyclerView响应,纵向滑动时子RecyclerView响应,不需要额外处理。如果你的子Item里有横向滑动的视图(比如横向滚动的TextView),可以重写子RecyclerView的onInterceptTouchEvent方法,手动判断滑动方向来处理冲突。

这样实现后,就能完美复刻Trello的效果:横向滑动整个列组,每个列独立纵向滑动,同时通过共享回收池、限制预加载等手段,保证了应用性能,实现了不可见项的复用。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.05.25 03:40:11