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

如何在Flutter中创建带对齐圆点的自定义时间线组件

Flutter 实现时间线竖线与标题精准对齐的圆点

要给已有的渐变竖线时间线添加和标题精准对齐的圆点,核心是让圆点的垂直中心与标题文本的中心保持一致,下面提供两种实用实现方案:

方案一:基于字体大小估算快速实现

如果标题字体大小固定,直接通过硬编码数值让圆点中心与标题中心对齐,适合快速开发:

Row(
  crossAxisAlignment: CrossAxisAlignment.start,
  children: [
    IntrinsicHeight(
      child: Stack(
        children: [
          // 渐变竖线
          Align(
            alignment: Alignment.center,
            child: Container(
              width: 4,
              decoration: BoxDecoration(
                gradient: LinearGradient(
                  colors: [Colors.blue, Colors.purple],
                  begin: Alignment.topCenter,
                  end: Alignment.bottomCenter,
                ),
              ),
            ),
          ),
          // 与标题对齐的圆点
          Positioned(
            left: 0,
            right: 0,
            // 计算逻辑:标题字体大小16,行高默认1.2,总高度约20;圆点半径6,所以top = (20/2) -6 =4
            top: 4,
            child: Container(
              width: 12,
              height: 12,
              decoration: BoxDecoration(
                color: Colors.white,
                border: Border.all(color: Colors.blue, width: 2),
                borderRadius: BorderRadius.circular(6),
              ),
            ),
          ),
        ],
      ),
    ),
    SizedBox(width: 16),
    Expanded(
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          Text("标题文本", style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold)),
          SizedBox(height: 8),
          Text("详细内容..."),
        ],
      ),
    ),
  ],
)

方案二:动态获取标题位置实现精准对齐

如果标题字体大小不固定,或者需要绝对精准的对齐,可以通过GlobalKey获取标题的渲染位置,动态计算圆点的top值:

// 先定义全局key和状态变量
final _titleKey = GlobalKey();
double _dotTop = 0;

@override
Widget build(BuildContext context) {
  return Row(
    crossAxisAlignment: CrossAxisAlignment.start,
    children: [
      IntrinsicHeight(
        child: Stack(
          children: [
            // 渐变竖线
            Align(
              alignment: Alignment.center,
              child: Container(
                width: 4,
                decoration: BoxDecoration(
                  gradient: LinearGradient(
                    colors: [Colors.blue, Colors.purple],
                    begin: Alignment.topCenter,
                    end: Alignment.bottomCenter,
                  ),
                ),
              ),
            ),
            // 动态计算位置的圆点
            LayoutBuilder(
              builder: (context, constraints) {
                WidgetsBinding.instance.addPostFrameCallback((_) {
                  final titleRender = _titleKey.currentContext?.findRenderObject() as RenderBox?;
                  final parentRender = context.findRenderObject() as RenderBox?;
                  if (titleRender != null && parentRender != null) {
                    // 转换标题的全局坐标为父容器的局部坐标
                    final localOffset = parentRender.globalToLocal(titleRender.localToGlobal(Offset.zero));
                    // 计算圆点top值:标题顶部偏移 + 标题高度/2 - 圆点半径
                    setState(() {
                      _dotTop = localOffset.dy + (titleRender.size.height / 2) - 6;
                    });
                  }
                });
                return Positioned(
                  left: 0,
                  right: 0,
                  top: _dotTop,
                  child: Container(
                    width: 12,
                    height: 12,
                    decoration: BoxDecoration(
                      color: Colors.white,
                      border: Border.all(color: Colors.blue, width: 2),
                      borderRadius: BorderRadius.circular(6),
                    ),
                  ),
                );
              },
            ),
          ],
        ),
      ),
      SizedBox(width: 16),
      Expanded(
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            Text(
              "标题文本",
              key: _titleKey,
              style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
            ),
            SizedBox(height: 8),
            Text("详细内容..."),
          ],
        ),
      ),
    ],
  );
}

额外优化建议

  • 把时间线条目封装成自定义TimelineItem组件,传入标题、内容、渐变颜色、圆点样式等参数,方便在列表中复用
  • 如果是时间线列表,注意第一条和最后一条的竖线可以截断(比如第一条只显示下半部分,最后一条只显示上半部分),提升视觉效果
  • 圆点样式可根据需求调整:比如填充渐变颜色、添加阴影,或者用Icon替代容器

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.24 23:11:03