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

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.30 00:45:30