Flutter实现双列表滚动联动 可控制关联列表速度与方向
Flutter 双列表滚动联动轻量实现方案
不需要引入任何第三方依赖,仅基于Flutter自带的ScrollController就能实现所有需求,核心逻辑代码量不到30行。
核心实现逻辑
- 用
ScrollController监听驱动列表(示例中为List1)的滚动事件 - 拿到驱动列表的实时滚动偏移后,按照自定义规则计算被驱动列表(List2)的目标偏移
- 加同步锁标记避免两个列表滚动事件互相触发导致死循环
- 对目标偏移做边界校验,避免被驱动列表滚动越界报错
支持的可配置项
- 自定义滚动速率:通过速率系数控制两个列表的滚动速度差,比如系数为0.5时List2滚动速度是List1的一半,系数为2时是List1的2倍
- 反向滚动:开关打开后两个列表滚动方向完全相反
- 可选双向联动:如果需要滚动任意一个列表都带动另一个,只需要加对称的监听逻辑即可
- 可选平滑滚动:替换滚动调用方法即可实现带动画的同步滚动效果
改造后完整代码
class ConectLists extends StatefulWidget { const ConectLists({Key? key}) : super(key: key); @override State<ConectLists> createState() => _ConectListsState(); } class _ConectListsState extends State<ConectLists> { final ScrollController scrollConroller1 = ScrollController(); final ScrollController scrollConroller2 = ScrollController(); // 同步锁,防止循环触发滚动 bool _isSyncing = false; // ========== 可自定义配置 ========== final double speedRatio = 0.5; // List2相对于List1的滚动速率 final bool reverseScroll = false; // 是否开启反向滚动 final bool enableTwoWaySync = false; // 是否开启双向联动 // ================================= @override void initState() { super.initState(); // 监听List1滚动,驱动List2 scrollConroller1.addListener(() { if (_isSyncing) return; _isSyncing = true; final double currentOffset1 = scrollConroller1.offset; double targetOffset2 = currentOffset1 * speedRatio; // 反向滚动计算 if (reverseScroll) { targetOffset2 = scrollConroller2.position.maxScrollExtent - targetOffset2; } // 边界校验,防止越界 targetOffset2 = targetOffset2.clamp(0.0, scrollConroller2.position.maxScrollExtent); // 同步滚动,需要平滑效果可替换为animateTo scrollConroller2.jumpTo(targetOffset2); _isSyncing = false; }); // 开启双向联动时,监听List2滚动驱动List1 if (enableTwoWaySync) { scrollConroller2.addListener(() { if (_isSyncing) return; _isSyncing = true; final double currentOffset2 = scrollConroller2.offset; double targetOffset1 = reverseScroll ? (scrollConroller2.position.maxScrollExtent - currentOffset2) / speedRatio : currentOffset2 / speedRatio; targetOffset1 = targetOffset1.clamp(0.0, scrollConroller1.position.maxScrollExtent); scrollConroller1.jumpTo(targetOffset1); _isSyncing = false; }); } } @override void dispose() { scrollConroller1.dispose(); scrollConroller2.dispose(); super.dispose(); } @override Widget build(BuildContext context) { return Scaffold( body: Column( children: [ const SizedBox(height: 8), Row( mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: const [ Text('List 1'), Text('List 2'), ], ), const SizedBox(height: 8), Container( color: Colors.black.withOpacity(0.5), width: double.infinity, height: 4, ), Expanded( child: Row( children: [ Expanded( flex: 1, child: ListView.builder( controller: scrollConroller1, itemBuilder: (context, index) => Card( elevation: 3, child: SizedBox( height: 40, child: Center(child: Text('First list item $index')))), itemCount: 50, ), ), Container( color: Colors.black.withOpacity(0.5), width: 4, height: double.infinity, ), Expanded( child: ListView.builder( controller: scrollConroller2, itemBuilder: (context, index) => Card( elevation: 3, child: SizedBox( height: 40, child: Center( child: Text('Second list item $index')))), itemCount: 25, ), ), ], ), ), ], ), ); } }
补充说明
- 如果两个列表item高度不一致,需要实现滚动进度百分比同步,只需要把偏移计算逻辑改成「驱动列表偏移/驱动列表最大可滚动距离 * 被驱动列表最大可滚动距离」即可
- 如果需要滚动动画,把
jumpTo方法替换为animateTo,传入自定义的滚动时长和动画曲线就能实现平滑同步效果 - 该方案没有任何依赖,性能和原生列表滚动完全一致,没有额外性能开销
效果参考:
内容的提问来源于stack exchange,提问作者Jaafar Melhem
相关产品推荐
相关产品推荐

