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

如何在Flutter中实现支持无限滚动的瀑布流网格视图?

嘿,我刚好折腾过类似的Flutter瀑布流需求,给你一套完美适配的解决方案!完全满足你要的无限滚动、动态高度item(不用预先知道尺寸)、包含图片+可变长文本+图标/金额这些要求。

解决方案:动态高度的Pinterest风格瀑布流

核心思路

因为每个item的图片高度、文本长度都是不确定的,我们需要先加载图片拿到真实尺寸,再计算文本的高度,最终算出整个item的总高度,然后通知瀑布流布局更新——这样就不用预先硬写高度啦。

具体实现步骤

1. 依赖准备

你之前提到的flutter_staggered_grid_view其实现在已经支持动态更新item高度了,用它的StaggeredGridView.countBuilder可以轻松实现无限滚动,刚好匹配我们的需求。

2. 定义数据模型

先给每个瀑布流item定义一个数据类,用来存内容和计算后的高度:

class PinItem {
  final String imageUrl;
  final String title; // 可变长文本
  final String price; // 金额
  double? itemHeight; // 动态计算的item总高度

  PinItem({
    required this.imageUrl,
    required this.title,
    required this.price,
    this.itemHeight,
  });
}

3. 实现动态高度的Item Widget

这个widget会负责加载图片、计算文本高度,最终算出整个item的高度,并通知父组件刷新:

class PinItemWidget extends StatelessWidget {
  final PinItem item;
  final VoidCallback onHeightCalculated;
  final GlobalKey itemKey;

  const PinItemWidget({
    super.key,
    required this.item,
    required this.onHeightCalculated,
    required this.itemKey,
  });

  @override
  Widget build(BuildContext context) {
    final columnWidth = MediaQuery.of(context).size.width / 2; // 假设是2列布局

    return Container(
      key: itemKey,
      margin: const EdgeInsets.all(8),
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          // 图片部分:加载完成后获取真实尺寸
          Image.network(
            item.imageUrl,
            width: columnWidth,
            fit: BoxFit.fitWidth,
            frameBuilder: (context, child, frame, wasSynchronouslyLoaded) {
              if (frame != null) {
                // 计算图片高度(按列宽等比例缩放)
                final imageHeight = (frame.image.height / frame.image.width) * columnWidth;
                // 计算文本高度(支持多行)
                final textPainter = TextPainter(
                  text: TextSpan(
                    text: item.title,
                    style: Theme.of(context).textTheme.bodyMedium,
                  ),
                  maxLines: null,
                  textDirection: TextDirection.ltr,
                )..layout(maxWidth: columnWidth);
                final textHeight = textPainter.height;
                // 加上价格、内边距等固定高度
                final totalHeight = imageHeight + textHeight + 40;

                // 如果高度有变化,更新并通知父组件刷新
                if (item.itemHeight != totalHeight) {
                  item.itemHeight = totalHeight;
                  onHeightCalculated();
                }
              }
              return child;
            },
          ),
          // 文本+金额区域
          Padding(
            padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 4),
            child: Column(
              crossAxisAlignment: CrossAxisAlignment.start,
              children: [
                Text(
                  item.title,
                  style: Theme.of(context).textTheme.bodyMedium,
                ),
                const SizedBox(height: 4),
                Row(
                  children: [
                    const Icon(Icons.attach_money, size: 16),
                    Text(
                      item.price,
                      style: Theme.of(context).textTheme.titleSmall?.copyWith(
                            fontWeight: FontWeight.bold,
                          ),
                    ),
                  ],
                ),
              ],
            ),
          ),
        ],
      ),
    );
  }
}

4. 实现无限滚动的瀑布流主页面

这里用StaggeredGridView.countBuilder结合ScrollController来实现无限滚动,同时处理加载更多的逻辑:

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

  @override
  State<PinterestStyleGrid> createState() => _PinterestStyleGridState();
}

class _PinterestStyleGridState extends State<PinterestStyleGrid> {
  final List<PinItem> _items = [];
  bool _isLoading = false;
  final ScrollController _scrollController = ScrollController();
  final Map<int, GlobalKey> _itemKeys = {}; // 存每个item的key,方便布局更新

  @override
  void initState() {
    super.initState();
    _loadInitialData();
    _scrollController.addListener(_checkLoadMore);
  }

  // 加载初始数据(模拟API请求)
  Future<void> _loadInitialData() async {
    setState(() => _isLoading = true);
    await Future.delayed(const Duration(seconds: 1));
    final newItems = List.generate(10, (index) => PinItem(
      imageUrl: "https://picsum.photos/600/${700 + index * 60}", // 模拟不同高度的图片
      title: "这是一个可变长度的标题,可能会跨多行显示${index % 3 == 0 ? ',再加一些内容让文本更长' : ''}",
      price: "\$${(12 + index * 2.3).toStringAsFixed(2)}",
    ));
    setState(() {
      _items.addAll(newItems);
      _isLoading = false;
      // 给新item分配GlobalKey
      for (int i = _items.length - newItems.length; i < _items.length; i++) {
        _itemKeys[i] = GlobalKey();
      }
    });
  }

  // 加载更多数据
  Future<void> _loadMoreData() async {
    if (_isLoading) return;
    setState(() => _isLoading = true);
    await Future.delayed(const Duration(seconds: 1));
    final newItems = List.generate(10, (index) => PinItem(
      imageUrl: "https://picsum.photos/600/${650 + index * 80}",
      title: "加载更多的可变标题${index % 2 == 0 ? ',这里测试多行文本的高度计算' : ''}",
      price: "\$${(20 + index * 3.1).toStringAsFixed(2)}",
    ));
    setState(() {
      _items.addAll(newItems);
      _isLoading = false;
      // 给新item分配GlobalKey
      for (int i = _items.length - newItems.length; i < _items.length; i++) {
        _itemKeys[i] = GlobalKey();
      }
    });
  }

  // 监听滚动,触发加载更多
  void _checkLoadMore() {
    if (_scrollController.position.pixels >= _scrollController.position.maxScrollExtent - 200 && !_isLoading) {
      _loadMoreData();
    }
  }

  // 刷新item高度
  void _refreshItemHeight(int index) {
    setState(() {});
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text("Pinterest 风格瀑布流")),
      body: StaggeredGridView.countBuilder(
        controller: _scrollController,
        crossAxisCount: 2, // 2列布局,可按需修改
        itemCount: _items.length + (_isLoading ? 1 : 0),
        itemBuilder: (context, index) {
          if (index == _items.length) {
            // 加载更多的占位指示器
            return const Center(child: CircularProgressIndicator());
          }
          final item = _items[index];
          return PinItemWidget(
            item: item,
            onHeightCalculated: () => _refreshItemHeight(index),
            itemKey: _itemKeys[index]!,
          );
        },
        staggeredTileBuilder: (index) {
          if (index == _items.length) {
            return const StaggeredTile.fit(2); // 加载项占满两列
          }
          // 用计算后的高度,没计算出来时用默认高度占位
          return StaggeredTile.fit(1, item.itemHeight ?? 300);
        },
        mainAxisSpacing: 8,
        crossAxisSpacing: 8,
      ),
    );
  }

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

关键细节说明

  • 动态高度计算:通过Image.network的frameBuilder监听图片加载完成,获取真实尺寸后结合文本高度算出总高度,避免预先硬写高度。
  • 无限滚动:用ScrollController监听滚动位置,当距离底部200px时自动加载更多数据,和ListView.builder的逻辑一致。
  • 流畅性优化:首次加载用默认高度占位,图片加载完成后再更新真实高度,减少布局抖动。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.05.28 07:20:37