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

Flutter:如何监听ListView.Builder滚动结束的最后一帧事件

解决ListView滚动结束后再更新列表项文本的问题

问题描述

现有一个可创建10000行的ListView.Builder,每行包含图标和显示“index - N”的容器。需求为:在滑动或滚动操作时,仅在滚动结束的最后一帧(ListView.Builder的最后一次屏幕更新)更新容器中的文本,而非滚动过程中实时更新。

解决方案

要实现该需求,需结合NotificationListener监听滚动状态,将组件改为StatefulWidget维护滚动状态,并控制文本更新时机:

  • 将原StatelessWidget改为StatefulWidget,添加滚动控制器和状态变量。
  • 用NotificationListener包裹ListView,监听滚动开始/结束事件,更新滚动状态。
  • 滚动过程中使用缓存的文本内容,滚动结束后更新可见项的文本并触发组件重建。

修改后的完整代码

import 'package:flutter/material.dart';

void main() => runApp(const MyApp());

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return const MaterialApp(
      home: ListViewBuilder(),
    );
  }
}

class ListViewBuilder extends StatefulWidget {
  const ListViewBuilder({super.key});

  @override
  State<ListViewBuilder> createState() => _ListViewBuilderState();
}

class _ListViewBuilderState extends State<ListViewBuilder> {
  final ScrollController _scrollController = ScrollController();
  bool _isScrolling = false;
  // 缓存列表项文本,滚动过程中使用缓存值
  final Map<int, String> _cachedTexts = {};

  @override
  void initState() {
    super.initState();
    // 初始化可见区域的文本缓存,避免初始显示占位符
    WidgetsBinding.instance.addPostFrameCallback((_) {
      _updateVisibleItemTexts();
    });
  }

  @override
  void dispose() {
    _scrollController.dispose();
    super.dispose();
  }

  // 更新可见区域的文本缓存
  void _updateVisibleItemTexts() {
    if (!mounted) return;
    final renderObject = context.findRenderObject() as RenderBox?;
    if (renderObject == null) return;

    final viewport = RenderAbstractViewport.of(renderObject);
    final offset = _scrollController.offset;
    final visibleStart = viewport.getOffsetToReveal(renderObject, 0.0).offset;
    final visibleEnd = viewport.getOffsetToReveal(renderObject, 1.0).offset;

    final itemExtent = _scrollController.position.itemExtent;
    final startIndex = ((offset - visibleStart) / itemExtent).floor().clamp(0, 9999);
    final endIndex = ((offset - visibleEnd) / itemExtent).ceil().clamp(0, 9999);

    for (int i = startIndex; i <= endIndex; i++) {
      _cachedTexts[i] = '  index - $i';
    }
    setState(() {});
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: NotificationListener<ScrollNotification>(
        onNotification: (notification) {
          if (notification is ScrollStartNotification) {
            setState(() {
              _isScrolling = true;
            });
          } else if (notification is ScrollEndNotification) {
            setState(() {
              _isScrolling = false;
            });
            // 滚动结束后更新可见项的文本缓存
            _updateVisibleItemTexts();
          }
          return false;
        },
        child: ListView.builder(
          controller: _scrollController,
          itemCount: 10000,
          itemBuilder: (BuildContext context, int index) {
            return Row(
              children: [
                const Icon(Icons.list),
                SizedBox(
                  height: 30,
                  width: 200,
                  // 滚动时显示缓存文本,滚动结束后显示最新文本
                  child: Text(_isScrolling ? _cachedTexts[index] ?? '  index - ...' : '  index - $index'),
                ),
              ],
            );
          },
        ),
      ),
    );
  }
}

代码说明

  • _isScrolling:标记当前是否处于滚动状态,控制文本显示逻辑。
  • _cachedTexts:缓存每个列表项的文本内容,滚动过程中保持文本不变。
  • _updateVisibleItemTexts:滚动结束后计算可见区域的列表项索引,更新对应缓存文本并触发组件重建。
  • NotificationListener:监听ScrollStartNotification和ScrollEndNotification事件,更新滚动状态并触发缓存更新。

这样就能实现需求:滚动过程中列表项文本保持原有内容,仅在滚动结束的最后一帧更新为当前索引对应的文本。

内容的提问来源于stack exchange,提问作者Alex

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.02 12:22:41