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

Flutter实现上下两个ListView Builder按索引同步滚动问询

实现不同高度Item的ListView按索引同步滚动

因为两个ListView的Item高度不一致,直接同步滚动偏移量会导致索引错位,核心思路是跟踪当前列表的可见索引,计算另一个列表对应索引的滚动偏移量并跳转,具体实现步骤如下:

1. 维护Item高度记录列表

在_WeatherCityitemState中添加两个列表,用于存储每个Item的高度:

List<double> _topItemHeights = [];
List<double> _bottomItemHeights = [];
ScrollController _controllerTop = ScrollController();
ScrollController _controllerBottom = ScrollController();
ScrollingList scrollingList = ScrollingList.none;

2. 初始化高度列表

在initState中初始化高度列表,同时监听字体大小变化(如果有动态调整字体的需求):

@override
void initState() {
  super.initState();
  _controllerTop = ScrollController();
  _controllerBottom = ScrollController();
  // 初始化高度列表,长度与列表Item数一致
  _topItemHeights = List.filled(widget.verseTamil.length, 0.0);
  _bottomItemHeights = List.filled(widget.verseEnglish.length, 0.0);
  
  // 监听字体大小变化,重置高度记录
  ever(Get.find<StateController>().fontSize, (newSize) {
    setState(() {
      _topItemHeights = List.filled(widget.verseTamil.length, 0.0);
      _bottomItemHeights = List.filled(widget.verseEnglish.length, 0.0);
    });
  });
}

3. 记录每个Item的高度

在两个ListView的itemBuilder中,用LayoutBuilder获取Item实际高度并更新记录列表:

顶部ListView的ItemBuilder示例:

itemBuilder: (context, index) {
  return LayoutBuilder(
    builder: (context, constraints) {
      WidgetsBinding.instance.addPostFrameCallback((_) {
        // 仅当高度变化或未记录时更新
        if (_topItemHeights[index] != constraints.maxHeight) {
          setState(() {
            _topItemHeights[index] = constraints.maxHeight;
          });
        }
      });
      // 替换为你的实际Item Widget
      return YourTopVerseItem(
        text: widget.verseTamil[index],
        fontSize: fontSized,
        color: _fontcolor,
      );
    },
  );
},

底部ListView的ItemBuilder示例:

itemBuilder: (context, index) {
  return LayoutBuilder(
    builder: (context, constraints) {
      WidgetsBinding.instance.addPostFrameCallback((_) {
        if (_bottomItemHeights[index] != constraints.maxHeight) {
          setState(() {
            _bottomItemHeights[index] = constraints.maxHeight;
          });
        }
      });
      // 替换为你的实际Item Widget
      return YourBottomVerseItem(
        text: widget.verseEnglish[index],
        fontSize: fontSized,
        color: _fontcolor,
      );
    },
  );
},

4. 实现按索引同步滚动

修改NotificationListener的逻辑,不再直接同步偏移量,而是通过可见索引计算目标偏移量:

顶部列表的滚动监听:

onNotification: (notification) {
  if (notification is ScrollStartNotification) {
    if (scrollingList == ScrollingList.none) {
      scrollingList = ScrollingList.top;
    }
  } else if (notification is ScrollEndNotification) {
    if (scrollingList == ScrollingList.top) {
      scrollingList = ScrollingList.none;
    }
  }

  if (scrollingList == ScrollingList.top && _bottomItemHeights.isNotEmpty) {
    // 获取顶部列表第一个可见索引
    final visibleIndex = _getFirstVisibleIndex(_controllerTop);
    // 计算底部列表到该索引的总偏移量
    double targetOffset = 0;
    for (int i = 0; i < visibleIndex; i++) {
      if (i < _bottomItemHeights.length) {
        targetOffset += _bottomItemHeights[i];
      }
    }
    // 底部列表跳转到目标偏移量
    _controllerBottom.jumpTo(targetOffset);
  }
  return true;
},

底部列表的滚动监听:

onNotification: (notification) {
  if (notification is ScrollStartNotification) {
    if (scrollingList == ScrollingList.none) {
      scrollingList = ScrollingList.bottom;
    }
  } else if (notification is ScrollEndNotification) {
    if (scrollingList == ScrollingList.bottom) {
      scrollingList = ScrollingList.none;
    }
  }

  if (scrollingList == ScrollingList.bottom && _topItemHeights.isNotEmpty) {
    // 获取底部列表第一个可见索引
    final visibleIndex = _getFirstVisibleIndex(_controllerBottom);
    // 计算顶部列表到该索引的总偏移量
    double targetOffset = 0;
    for (int i = 0; i < visibleIndex; i++) {
      if (i < _topItemHeights.length) {
        targetOffset += _topItemHeights[i];
      }
    }
    // 顶部列表跳转到目标偏移量
    _controllerTop.jumpTo(targetOffset);
  }
  return true;
},

5. 实现获取第一个可见索引的方法

在_WeatherCityitemState中添加工具方法:

int _getFirstVisibleIndex(ScrollController controller) {
  final renderObject = controller.position.context.findRenderObject();
  if (renderObject is RenderSliverList && renderObject.children.isNotEmpty) {
    return renderObject.indexOf(renderObject.children.first);
  }
  return 0;
}

关键注意事项

  • 确保两个ListView的itemCount一致,否则索引同步会出现越界问题;
  • 如果Item高度会动态变化(比如字体大小调整),必须重置高度记录列表,否则同步会失效;
  • 使用jumpTo会有瞬间跳转的效果,如果需要平滑滚动,可以替换为animateTo并设置动画参数。

内容的提问来源于stack exchange,提问作者Immanuvel Blessanraj Mariappan

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.24 20:06:33