Android类似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

