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

基于Flutter实现带指示点的百分比弧形Widget开发求助

实现渐变弧形百分比指示器Widget

核心思路

通过自定义CustomPainter绘制渐变弧形、指示点,结合Stack布局放置中间百分比文本。核心是根据传入的百分比计算指示点的坐标,并提取对应位置的渐变颜色,保证文字与指示点颜色匹配。

完整代码实现

import 'package:flutter/material.dart';

class GradientArcIndicator extends StatefulWidget {
  final double percentage; // 传入0-100之间的数值
  final double strokeWidth;
  final Size size;

  const GradientArcIndicator({
    super.key,
    required this.percentage,
    this.strokeWidth = 16,
    this.size = const Size(200, 200),
  });

  @override
  State<GradientArcIndicator> createState() => _GradientArcIndicatorState();
}

class _GradientArcIndicatorState extends State<GradientArcIndicator> {
  // 根据百分比获取对应位置的渐变颜色
  Color getMatchColor() {
    final gradient = LinearGradient(
      colors: [Colors.blue, Colors.green, Colors.yellow, Colors.red],
      stops: const [0.0, 0.33, 0.66, 1.0],
      begin: Alignment.centerLeft,
      end: Alignment.centerRight,
    );
    final fraction = widget.percentage / 100;
    return gradient.transform(Alignment(fraction * 2 - 1, 0)).color;
  }

  @override
  Widget build(BuildContext context) {
    final matchColor = getMatchColor();
    return SizedBox.fromSize(
      size: widget.size,
      child: Stack(
        alignment: Alignment.center,
        children: [
          // 背景灰色弧形
          CustomPaint(
            painter: _BackgroundArcPainter(
              strokeWidth: widget.strokeWidth,
              color: Colors.grey.shade200,
            ),
          ),
          // 渐变前景弧形
          CustomPaint(
            painter: _ForegroundArcPainter(
              percentage: widget.percentage,
              strokeWidth: widget.strokeWidth,
              gradient: LinearGradient(
                colors: [Colors.blue, Colors.green, Colors.yellow, Colors.red],
                stops: const [0.0, 0.33, 0.66, 1.0],
              ),
            ),
          ),
          // 指示点
          CustomPaint(
            painter: _IndicatorDotPainter(
              percentage: widget.percentage,
              dotRadius: widget.strokeWidth / 2 + 4,
              color: matchColor,
            ),
          ),
          // 中间百分比文本
          Text(
            '${widget.percentage.toInt()}%',
            style: TextStyle(
              fontSize: widget.size.width * 0.2,
              fontWeight: FontWeight.bold,
              color: matchColor,
            ),
          ),
        ],
      ),
    );
  }
}

// 绘制背景弧形
class _BackgroundArcPainter extends CustomPainter {
  final double strokeWidth;
  final Color color;

  _BackgroundArcPainter({required this.strokeWidth, required this.color});

  @override
  void paint(Canvas canvas, Size size) {
    final rect = Rect.fromCircle(
      center: Offset(size.width / 2, size.height / 2),
      radius: (size.width - strokeWidth) / 2,
    );
    final paint = Paint()
      ..color = color
      ..style = PaintingStyle.stroke
      ..strokeWidth = strokeWidth
      ..strokeCap = StrokeCap.round;

    canvas.drawArc(rect, 0, 2 * 3.1415926, false, paint);
  }

  @override
  bool shouldRepaint(covariant _BackgroundArcPainter oldDelegate) {
    return oldDelegate.strokeWidth != strokeWidth || oldDelegate.color != color;
  }
}

// 绘制渐变前景弧形
class _ForegroundArcPainter extends CustomPainter {
  final double percentage;
  final double strokeWidth;
  final Gradient gradient;

  _ForegroundArcPainter({
    required this.percentage,
    required this.strokeWidth,
    required this.gradient,
  });

  @override
  void paint(Canvas canvas, Size size) {
    final rect = Rect.fromCircle(
      center: Offset(size.width / 2, size.height / 2),
      radius: (size.width - strokeWidth) / 2,
    );
    final sweepAngle = 2 * 3.1415926 * (percentage / 100);
    final paint = Paint()
      ..style = PaintingStyle.stroke
      ..strokeWidth = strokeWidth
      ..strokeCap = StrokeCap.round
      ..shader = gradient.createShader(rect);

    canvas.drawArc(rect, -0.5 * 3.1415926, sweepAngle, false, paint);
  }

  @override
  bool shouldRepaint(covariant _ForegroundArcPainter oldDelegate) {
    return oldDelegate.percentage != percentage ||
        oldDelegate.strokeWidth != strokeWidth ||
        oldDelegate.gradient != gradient;
  }
}

// 绘制指示点
class _IndicatorDotPainter extends CustomPainter {
  final double percentage;
  final double dotRadius;
  final Color color;

  _IndicatorDotPainter({
    required this.percentage,
    required this.dotRadius,
    required this.color,
  });

  @override
  void paint(Canvas canvas, Size size) {
    final center = Offset(size.width / 2, size.height / 2);
    final radius = (size.width / 2) - dotRadius;
    // 从顶部开始顺时针计算指示点角度
    final angle = -0.5 * 3.1415926 + 2 * 3.1415926 * (percentage / 100);
    final dotCenter = Offset(
      center.dx + radius * cos(angle),
      center.dy + radius * sin(angle),
    );

    final paint = Paint()..color = color;
    canvas.drawCircle(dotCenter, dotRadius, paint);
  }

  @override
  bool shouldRepaint(covariant _IndicatorDotPainter oldDelegate) {
    return oldDelegate.percentage != percentage ||
        oldDelegate.dotRadius != dotRadius ||
        oldDelegate.color != color;
  }
}

// 使用示例
void main() {
  runApp(const MaterialApp(
    home: Scaffold(
      body: Center(
        child: GradientArcIndicator(percentage: 75),
      ),
    ),
  ));
}

关键说明

  • 颜色匹配逻辑:通过LinearGradient.transform方法,将百分比转换为渐变坐标,精准提取对应位置的颜色,保证指示点和文本颜色与弧形位置一致。
  • 指示点定位:基于百分比计算对应的弧度,通过三角函数算出点在圆上的坐标,确保指示点落在弧形的对应百分比位置。
  • 可定制性:支持调整弧形宽度、组件尺寸、渐变颜色组等参数,适配不同UI需求。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.30 10:30:39