Flutter实现多个水平ListView横向同步滚动且分隔符不随动
Flutter 嵌套列表横向同步滚动实现方案
问题背景
实现外层垂直滚动、内层水平滚动的嵌套列表布局,需满足以下要求:
- 外层垂直列表支持正常上下滚动
- 横向滚动任意一行内容时,所有行同步横向滚动,始终保持列对齐
- 行与行之间的分隔符固定在垂直列表中,不跟随横向滚动位移,因此无法通过在外层包裹横向
SingleChildScrollView实现 - 受Flutter框架限制,同一个
ScrollController无法同时绑定多个可滚动组件,直接复用控制器会抛出异常
初始问题代码如下:
class ListViewTest extends StatelessWidget{ final ScrollController _horizontalScrollController = ScrollController(); @override Widget build(BuildContext context) { return ListView.separated( itemCount: 20, separatorBuilder: (BuildContext context, int i) => const Divider( thickness: 1, color: Colors.black, height: 41, ), itemBuilder: (BuildContext context, int i) => SizedBox( height: 40, child: ListView.separated( //controller: _horizontalScrollController, does not work shrinkWrap: true, scrollDirection: Axis.horizontal, itemCount: 10, separatorBuilder: (context, j) => SizedBox(width: 5,), itemBuilder: (context, j) => Container( height: 40, width: 60, color: Colors.green, child: Text( "cell $i - $j" ), ), ), ), ); } }
效果参考:
实现思路
核心逻辑是放弃「单控制器绑定多组件」的思路,改为多控制器+偏移同步的方案:
- 为每一行的水平列表单独创建专属
ScrollController,统一存放在列表中管理 - 给所有控制器添加滚动监听,当任意一行触发横向滚动时,记录当前滚动偏移
- 增加同步锁标记,避免滚动回调循环触发,遍历所有其他行的控制器,统一更新滚动位置到记录的偏移值
- 分隔符始终放在外层垂直列表的构建逻辑中,不进入横向滚动区域,自然不会产生横向位移
完整可运行代码
class SyncScrollTableList extends StatefulWidget { const SyncScrollTableList({super.key}); @override State<SyncScrollTableList> createState() => _SyncScrollTableListState(); } class _SyncScrollTableListState extends State<SyncScrollTableList> { // 存储所有行的横向滚动控制器 final List<ScrollController> _rowScrollControllers = []; // 滚动同步锁,防止回调循环触发 bool _isSyncing = false; // 全局统一的横向滚动偏移 double _syncOffset = 0; @override void dispose() { // 组件销毁时释放所有控制器,避免内存泄漏 for (final controller in _rowScrollControllers) { controller.dispose(); } super.dispose(); } /// 按行索引获取对应滚动控制器,懒初始化 ScrollController _getRowController(int rowIndex) { // 索引超过已存储长度时新建控制器 if (rowIndex >= _rowScrollControllers.length) { final controller = ScrollController(initialScrollOffset: _syncOffset); controller.addListener(() { // 同步过程中触发的回调直接跳过 if (_isSyncing) return; // 更新全局偏移值 _syncOffset = controller.offset; _isSyncing = true; // 同步所有其他行的滚动位置 for (final c in _rowScrollControllers) { if (c != controller && c.hasClients) { c.jumpTo(_syncOffset); } } _isSyncing = false; }); _rowScrollControllers.add(controller); } return _rowScrollControllers[rowIndex]; } @override Widget build(BuildContext context) { return ListView.separated( itemCount: 20, // 分隔符在外层构建,不属于横向滚动区域,固定不横向移动 separatorBuilder: (BuildContext context, int index) => const Divider( thickness: 1, color: Colors.black, height: 41, ), itemBuilder: (BuildContext context, int rowIndex) => SizedBox( height: 40, child: ListView.separated( controller: _getRowController(rowIndex), scrollDirection: Axis.horizontal, physics: const ClampingScrollPhysics(), itemCount: 10, separatorBuilder: (context, colIndex) => const SizedBox(width: 5), itemBuilder: (context, colIndex) => Container( height: 40, width: 60, color: Colors.green, alignment: Alignment.center, child: Text("cell $rowIndex - $colIndex"), ), ), ), ); } }
优化说明
- 如果需要同步滚动时带平滑动画效果,可将代码中的
jumpTo替换为animateTo,传入统一的滚动时长和动画曲线即可 - 如果列表行数极大(百级以上),可结合外层
ListView的懒加载生命周期,在row组件销毁时同步移除并销毁对应的ScrollController,降低内存占用 - 若需要固定首列/尾列不跟随横向滚动,可在每一行的横向列表外拼接固定位置的列组件即可,逻辑不受同步滚动影响
注意:水平ListView无需设置
shrinkWrap: true,外层SizedBox已经明确给出了高度约束,开启shrinkWrap会导致横向列表一次性加载全部子组件,长列表场景下会引发性能问题。
内容的提问来源于stack exchange,提问作者Haissem55
相关产品推荐
相关产品推荐

