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

Flutter开发:实现可控制分段长度的U形Widget

实现可自定义分段的U形时间线Widget(Flutter)

直接给你一个基于CustomPainter的可行实现,能精准控制每个彩色分段的长度:

1. 定义分段数据类

先创建数据类存储每个活动分段的颜色和时长:

class ActivitySegment {
  final Color color;
  final double duration; // 自定义单位,最终按总时长比例计算长度

  ActivitySegment({required this.color, required this.duration});
}

2. 自定义U形时间线Painter

这个Painter会按比例把各分段绘制到U形的左竖边、底部横边、右竖边:

import 'dart:math' as math;
import 'package:flutter/material.dart';

class UTimelinePainter extends CustomPainter {
  final List<ActivitySegment> segments;
  final double strokeWidth;
  final double cornerRadius;
  final double totalDuration;

  UTimelinePainter({
    required this.segments,
    this.strokeWidth = 20.0,
    this.cornerRadius = 10.0,
    required this.totalDuration,
  });

  @override
  void paint(Canvas canvas, Size size) {
    final paint = Paint()
      ..style = PaintingStyle.stroke
      ..strokeWidth = strokeWidth
      ..strokeCap = StrokeCap.round;

    // 计算U形各部分总长度
    final leftHeight = size.height / 2;
    final bottomWidth = size.width - strokeWidth; // 留边避免超出画布
    final rightHeight = size.height / 2;
    final totalPathLength = leftHeight + bottomWidth + rightHeight;

    // 转换每个分段的时长为实际路径长度
    final segmentLengths = segments.map((seg) {
      return (seg.duration / totalDuration) * totalPathLength;
    }).toList();

    double currentOffset = 0.0;

    for (int i = 0; i < segments.length; i++) {
      final segmentLength = segmentLengths[i];
      paint.color = segments[i].color;
      Path path = Path();

      // 处理左竖边(顶部中点到底部)
      if (currentOffset < leftHeight) {
        final startY = (size.height - leftHeight) + currentOffset;
        final endY = startY + segmentLength;
        if (endY <= size.height) {
          path.moveTo(strokeWidth / 2, startY);
          path.lineTo(strokeWidth / 2, endY);
          canvas.drawPath(path, paint);
          currentOffset += segmentLength;
          continue;
        } else {
          // 画到左竖边底部,绘制转角圆角
          path.moveTo(strokeWidth / 2, startY);
          path.lineTo(strokeWidth / 2, size.height - strokeWidth / 2);
          path.arcTo(
            Rect.fromLTWH(
              strokeWidth / 2,
              size.height - strokeWidth - cornerRadius,
              cornerRadius * 2,
              cornerRadius * 2,
            ),
            math.pi / 2,
            math.pi / 2,
            false,
          );
          canvas.drawPath(path, paint);
          currentOffset = leftHeight + cornerRadius;
        }
      }

      // 处理底部横边(左到右)
      if (currentOffset >= leftHeight && currentOffset < leftHeight + bottomWidth) {
        final startX = strokeWidth + (currentOffset - leftHeight);
        final endX = startX + segmentLength;
        if (endX <= size.width - strokeWidth) {
          path.moveTo(startX, size.height - strokeWidth / 2);
          path.lineTo(endX, size.height - strokeWidth / 2);
          canvas.drawPath(path, paint);
          currentOffset += segmentLength;
          continue;
        } else {
          // 画到横边右端,绘制转角圆角
          path.moveTo(startX, size.height - strokeWidth / 2);
          path.lineTo(size.width - strokeWidth / 2 - cornerRadius, size.height - strokeWidth / 2);
          path.arcTo(
            Rect.fromLTWH(
              size.width - strokeWidth - cornerRadius,
              size.height - strokeWidth - cornerRadius,
              cornerRadius * 2,
              cornerRadius * 2,
            ),
            0,
            math.pi / 2,
            false,
          );
          canvas.drawPath(path, paint);
          currentOffset = leftHeight + bottomWidth + cornerRadius;
        }
      }

      // 处理右竖边(底部到顶部中点)
      if (currentOffset >= leftHeight + bottomWidth) {
        final startY = size.height - strokeWidth / 2 - (currentOffset - leftHeight - bottomWidth);
        final endY = startY - segmentLength;
        if (endY >= size.height - rightHeight) {
          path.moveTo(size.width - strokeWidth / 2, startY);
          path.lineTo(size.width - strokeWidth / 2, endY);
          canvas.drawPath(path, paint);
          currentOffset += segmentLength;
        }
      }
    }
  }

  @override
  bool shouldRepaint(covariant UTimelinePainter oldDelegate) {
    return oldDelegate.segments != segments ||
        oldDelegate.strokeWidth != strokeWidth ||
        oldDelegate.cornerRadius != cornerRadius ||
        oldDelegate.totalDuration != totalDuration;
  }
}

3. 封装成可复用Widget

把Painter封装成Widget,方便页面调用:

class UTimelineWidget extends StatelessWidget {
  final List<ActivitySegment> segments;
  final double totalDuration;
  final double strokeWidth;
  final double cornerRadius;
  final double height;

  const UTimelineWidget({
    super.key,
    required this.segments,
    required this.totalDuration,
    this.strokeWidth = 20.0,
    this.cornerRadius = 10.0,
    this.height = 200.0,
  });

  @override
  Widget build(BuildContext context) {
    return SizedBox(
      height: height,
      width: double.infinity,
      child: CustomPaint(
        painter: UTimelinePainter(
          segments: segments,
          totalDuration: totalDuration,
          strokeWidth: strokeWidth,
          cornerRadius: cornerRadius,
        ),
      ),
    );
  }
}

4. 示例用法

假设总时长为一天1440分钟,各活动分段如下:

// 定义活动分段
final activitySegments = [
  ActivitySegment(color: Colors.blue.shade700, duration: 480), // 睡眠8小时
  ActivitySegment(color: Colors.green.shade600, duration: 360), // 工作6小时
  ActivitySegment(color: Colors.orange.shade500, duration: 120), // 休闲2小时
  ActivitySegment(color: Colors.purple.shade500, duration: 480), // 其他8小时
];

// 在页面中使用
UTimelineWidget(
  segments: activitySegments,
  totalDuration: 1440,
  strokeWidth: 25,
  cornerRadius: 12,
  height: 220,
)

关键控制逻辑

  • 分段长度由duration和totalDuration的比例决定,修改duration即可调整对应分段长度
  • 通过strokeWidth调整U形粗细,height控制整体高度
  • cornerRadius控制U形转角的圆角大小

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.14 21:14:55