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

如何在Flutter的Wrap组件中显示剩余子组件数量

实现最多两行、超出显示剩余数量的Wrap布局

要实现这个需求,Wrap组件本身没法直接获取可见子项数量,得通过测量子项尺寸+模拟换行逻辑来实现,以下是具体方案:

核心思路

  1. 先测量所有子组件的宽度,结合可用布局宽度,计算出2行内能容纳的最大子项数量
  2. 渲染前N个可见子项,最后用一个Container显示剩余数量(+${总数-N})
  3. 用LayoutBuilder获取父容器的可用宽度,确保计算准确

代码实现

import 'package:flutter/material.dart';

class TwoLineWrap extends StatefulWidget {
  final List<Widget> children;
  final double spacing;
  final double runSpacing;
  final Widget Function(int remainingCount) trailingBuilder;

  const TwoLineWrap({
    super.key,
    required this.children,
    this.spacing = 8.0,
    this.runSpacing = 8.0,
    required this.trailingBuilder,
  });

  @override
  State<TwoLineWrap> createState() => _TwoLineWrapState();
}

class _TwoLineWrapState extends State<TwoLineWrap> {
  final List<GlobalKey> _childKeys = [];
  int _visibleCount = 0;

  @override
  void initState() {
    super.initState();
    _childKeys.addAll(List.generate(widget.children.length, (_) => GlobalKey()));
    WidgetsBinding.instance.addPostFrameCallback((_) => _calculateVisibleCount());
  }

  @override
  void didUpdateWidget(covariant TwoLineWrap oldWidget) {
    super.didUpdateWidget(oldWidget);
    if (widget.children.length != oldWidget.children.length) {
      _childKeys.clear();
      _childKeys.addAll(List.generate(widget.children.length, (_) => GlobalKey()));
      WidgetsBinding.instance.addPostFrameCallback((_) => _calculateVisibleCount());
    }
  }

  void _calculateVisibleCount() {
    if (widget.children.isEmpty) return;

    final renderBox = context.findRenderObject() as RenderBox;
    final availableWidth = renderBox.size.width;
    double currentLineWidth = 0;
    int lineCount = 1;
    int visibleCount = 0;

    for (int i = 0; i < widget.children.length; i++) {
      final childRenderBox = _childKeys[i].currentContext?.findRenderObject() as RenderBox?;
      if (childRenderBox == null) continue;

      final childWidth = childRenderBox.size.width;
      final requiredWidth = currentLineWidth + childWidth + (currentLineWidth > 0 ? widget.spacing : 0);

      if (requiredWidth > availableWidth) {
        lineCount++;
        if (lineCount > 2) break;
        currentLineWidth = childWidth;
      } else {
        currentLineWidth = requiredWidth;
      }
      visibleCount++;
    }

    if (mounted) {
      setState(() => _visibleCount = visibleCount);
    }
  }

  @override
  Widget build(BuildContext context) {
    return LayoutBuilder(
      builder: (context, constraints) {
        final hasRemaining = widget.children.length > _visibleCount;
        final displayChildren = hasRemaining
            ? widget.children.take(_visibleCount).toList()
            : widget.children;

        if (hasRemaining) {
          displayChildren.add(widget.trailingBuilder(widget.children.length - _visibleCount));
        }

        return Wrap(
          spacing: widget.spacing,
          runSpacing: widget.runSpacing,
          children: displayChildren.asMap().entries.map((entry) {
            final index = entry.key;
            final child = entry.value;
            return index < widget.children.length
                ? KeyedSubtree(key: _childKeys[index], child: child)
                : child;
          }).toList(),
        );
      },
    );
  }
}

// 使用示例
void main() {
  runApp(const MaterialApp(home: DemoPage()));
}

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

  @override
  Widget build(BuildContext context) {
    final tags = List.generate(15, (index) => 'Tag $index');
    return Scaffold(
      padding: const EdgeInsets.all(16),
      body: TwoLineWrap(
        spacing: 8,
        runSpacing: 8,
        trailingBuilder: (count) => Container(
          padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
          decoration: BoxDecoration(
            color: Colors.grey[200],
            borderRadius: BorderRadius.circular(12),
          ),
          child: Text('+$count'),
        ),
        children: tags.map((tag) => Container(
          padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
          decoration: BoxDecoration(
            color: Colors.blue[100],
            borderRadius: BorderRadius.circular(12),
          ),
          child: Text(tag),
        )).toList(),
      ),
    );
  }
}

关键说明

  • 用GlobalKey测量每个子项的实际宽度,确保换行计算准确
  • LayoutBuilder获取父容器的可用宽度,适配不同屏幕尺寸
  • addPostFrameCallback确保在组件渲染完成后再计算可见数量,避免尺寸未初始化的问题
  • 支持自定义尾部组件样式,通过trailingBuilder传入你需要的“+N”样式

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.01 19:38:38