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

如何在Flutter中创建容器的随机堆叠布局?

Flutter 复刻交错胶囊标签布局方案

问题场景

我希望在Flutter中复刻以下两种标签布局效果:

  • 效果一:
    胶囊标签适配两行空隙
    其中“Linguistics”胶囊标签完美嵌入在“healthcare”与“data science”两行标签的空隙中
  • 效果二:
    另一组交错标签布局

尝试过的方案及问题:

  • 使用Row/Column组件:出现内容溢出错误
  • 使用Stack/Positioned组件:初期布局正常,但后续容器出现相互重叠

解决方案

通过动态计算标签宽度+Stack定位的方式实现,既能避免溢出,又能精准控制中间标签的位置,避免重叠。以下是完整实现代码:

import 'package:flutter/material.dart';

class StaggeredTagLayout extends StatelessWidget {
  final List<String> topTags;
  final String middleTag;
  final List<String> bottomTags;
  final TextStyle tagStyle;
  final Color tagColor;
  final Color middleTagColor;

  const StaggeredTagLayout({
    super.key,
    required this.topTags,
    required this.middleTag,
    required this.bottomTags,
    this.tagStyle = const TextStyle(color: Colors.white, fontSize: 12),
    this.tagColor = Colors.grey,
    this.middleTagColor = Colors.blue,
  });

  @override
  Widget build(BuildContext context) {
    return LayoutBuilder(
      builder: (context, constraints) {
        return Stack(
          children: [
            // 顶部标签行
            Positioned(
              top: 0,
              left: 0,
              right: 0,
              child: Wrap(
                spacing: 8,
                runSpacing: 8,
                children: topTags.map((tag) => _buildTag(tag, tagColor)).toList(),
              ),
            ),
            // 底部标签行
            Positioned(
              top: 32, // 根据标签高度调整垂直间距
              left: 0,
              right: 0,
              child: Wrap(
                spacing: 8,
                runSpacing: 8,
                children: bottomTags.map((tag) => _buildTag(tag, tagColor)).toList(),
              ),
            ),
            // 动态计算中间标签的位置
            FutureBuilder(
              future: _getMiddleTagOffset(constraints.maxWidth),
              builder: (context, snapshot) {
                if (!snapshot.hasData) return const SizedBox.shrink();
                final offset = snapshot.data!;
                return Positioned(
                  left: offset.dx,
                  top: 16, // 垂直居中在两行标签之间
                  child: _buildTag(middleTag, middleTagColor),
                );
              },
            ),
          ],
        );
      },
    );
  }

  // 构建单个胶囊标签
  Widget _buildTag(String text, Color color) {
    return Container(
      padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
      decoration: BoxDecoration(
        color: color,
        borderRadius: BorderRadius.circular(12),
      ),
      child: Text(text, style: tagStyle),
    );
  }

  // 计算中间标签的水平偏移量
  Future<Offset> _getMiddleTagOffset(double maxWidth) async {
    final spacing = 8.0;
    double totalWidth = 0;
    // 计算顶部每个标签的实际宽度
    final topTagWidths = await Future.wait(
      topTags.map((tag) => _calculateTagWidth(tag)),
    );

    for (var width in topTagWidths) {
      // 检查插入中间标签后是否超出容器宽度
      final requiredWidth = totalWidth + width + spacing + await _calculateTagWidth(middleTag);
      if (requiredWidth <= maxWidth) {
        totalWidth += width + spacing;
      } else {
        break;
      }
    }
    return Offset(totalWidth, 0);
  }

  // 计算单个标签的总宽度(文本宽度+padding)
  Future<double> _calculateTagWidth(String text) async {
    final textPainter = TextPainter(
      text: TextSpan(text: text, style: tagStyle),
      textDirection: TextDirection.ltr,
    )..layout();
    return textPainter.width + 24; // 24是左右padding的总和(12*2)
  }
}

// 使用示例
class TagDemoPage extends StatelessWidget {
  const TagDemoPage({super.key});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Padding(
        padding: const EdgeInsets.all(16.0),
        child: StaggeredTagLayout(
          topTags: ["Healthcare", "Data Science"],
          middleTag: "Linguistics",
          bottomTags: ["Machine Learning", "AI", "Neuroscience"],
        ),
      ),
    );
  }
}

方案说明

  1. 动态宽度计算:用TextPainter精准计算每个标签的实际宽度,确保中间标签插入后不会超出容器宽度
  2. Stack定位:将上下两行标签用Wrap布局(自动换行避免溢出),中间标签通过计算得到的偏移量定位在两行空隙中
  3. 自适应适配:通过LayoutBuilder获取容器宽度,适配不同屏幕尺寸

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.18 14:53:14