Flutter 绘制圆弧,绘制Image,canvas.drawImageRect,圆弧刻度条

194 阅读2分钟

微信截图_20241204100704.png

效果如上图

ArcProgressPainter

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

class ArcProgressPainter extends CustomPainter {
  final double progress;
  final Color backgroundColor;
  final double strokeWidth;
  // final TextStyle textStyle;
  final ui.Image sliderImage;
  final double minValue; // 最小值
  final double maxValue; // 最大值

  ArcProgressPainter({
    required this.progress,
    required this.backgroundColor,
    required this.strokeWidth,
    // required this.textStyle,
    required this.sliderImage,
    this.minValue = 0.0, // 默认最小值为 0
    this.maxValue = 35.0, // 默认最大值为 100
  });

  @override
  void paint(Canvas canvas, Size size) {
    // 限制进度值
    double clampedProgress = progress.clamp(minValue, maxValue);

    final gradientColors = [
      const Color(0xFFFFFFD9),
      const Color(0x77B798FC),
      const Color(0xF9D75E2A),
    ];

    Offset center = Offset(size.width / 2, size.height / 2);
    double radius = math.min(size.width / 2, size.height / 2);

    Rect rect = Rect.fromCircle(center: center, radius: radius).inflate(-strokeWidth / 2);

    double degreesToRadians(num deg) => deg * (math.pi / 180.0);
    double startAngle = degreesToRadians(90 + 75); // 起始角度
    double sweepAngle = degreesToRadians(360 - 150); // 圆弧扫过的角度

    // 绘制圆弧
    for (double i = 0; i < sweepAngle; i += 0.01) {
      double angle = startAngle + i;
      double colorPosition = i / sweepAngle;
      Color color = _calculateGradientColor(gradientColors, colorPosition);

      Paint segmentPaint = Paint()
        ..color = color
        ..strokeWidth = strokeWidth
        ..strokeCap = StrokeCap.round
        ..style = PaintingStyle.stroke;

      canvas.drawArc(
        rect,
        angle,
        0.01, // 绘制小段的角度
        false,
        segmentPaint,
      );
    }

    // 绘制刻度
    int tickCount = 50; // 刻度数量
    double tickSpacing = sweepAngle / tickCount; // 每个刻度之间的间隔
    for (int i = 0; i <= tickCount; i++) {
      double angle = startAngle + i * tickSpacing;
      double tickX = center.dx + (radius - strokeWidth) * math.cos(angle);
      double tickY = center.dy + (radius - strokeWidth) * math.sin(angle);

      // 绘制刻度
      Paint tickPaint = Paint()
        ..color = Colors.grey
        ..style = PaintingStyle.fill;

      // 在每个刻度位置绘制短线或点
      double tickLength = -10; // 刻度的长度
      canvas.drawLine(
        Offset(tickX, tickY),
        Offset(tickX + tickLength * math.cos(angle), tickY + tickLength * math.sin(angle)),
        tickPaint,
      );
    }

    // 计算滑块位置
    double progressAngle = startAngle + ((clampedProgress - minValue) / (maxValue - minValue)) * sweepAngle;

    // 计算滑块的位置
    Offset sliderPosition = Offset(
      center.dx + (radius - strokeWidth) * math.cos(progressAngle),
      center.dy + (radius - strokeWidth) * math.sin(progressAngle),
    );

    // 绘制滑块
    double sliderSize = 10.0; // 滑块的大小
    final imageWidth = sliderImage.width.toDouble();
    final imageHeight = sliderImage.height.toDouble();
    final imageRect = Rect.fromCircle(center: sliderPosition, radius: sliderSize);

    // 使用 canvas.drawImageRect 绘制滑块图片
    canvas.drawImageRect(
      sliderImage, // 使用 ui.Image
      Rect.fromLTWH(0, 0, imageWidth, imageHeight), // 图片的源区域
      imageRect, // 目标区域
      Paint(),
    );

    // 绘制进度值文字
    String progressText = "${clampedProgress.toStringAsFixed(0)}℃"; // 显示进度值
    TextSpan span = TextSpan(
      style: const TextStyle(fontSize: 15, color: Colors.black),
      text: progressText,
    );
    TextPainter textPainter = TextPainter(
      text: span,
      textAlign: TextAlign.center,
      textDirection: TextDirection.ltr,
    );
    textPainter.layout();
    Offset textOffset = Offset(
      sliderPosition.dx - textPainter.width / 2 - 10,
      sliderPosition.dy - sliderSize - textPainter.height - 5, // 滑块上方位置
    );
    textPainter.paint(canvas, textOffset);
  }

  // 计算渐变颜色
  Color _calculateGradientColor(List<Color> colors, double position) {
    int index = (position * (colors.length - 1)).floor();
    double localPosition = (position * (colors.length - 1)) - index;
    return Color.lerp(colors[index], colors[index + 1], localPosition) ?? colors.last;
  }

  @override
  bool shouldRepaint(covariant CustomPainter oldDelegate) => true;
}

ArcProgressBar

1.这个是重点

  // 加载图片并转换为 ui.Image
  Future loadImage(String path) async {
    // 加载资源文件
    final data = await rootBundle.load(path);
    // 把资源文件转换成Uint8List类型
    final bytes = data.buffer.asUint8List();
    // 解析Uint8List类型的数据图片
    final image = await decodeImageFromList(bytes);
    _sliderImage = image;
    setState(() {});
  }
import 'package:flutter/material.dart';
import 'dart:ui' as ui;

import 'package:flutter/services.dart';

import 'arc_progress_painter .dart'; // 导入 ui 库

class ArcProgressBar extends StatefulWidget {
  final double progress;

  const ArcProgressBar({super.key, required this.progress});

  @override
  _ArcProgressBarState createState() => _ArcProgressBarState();
}

class _ArcProgressBarState extends State<ArcProgressBar> {
  ui.Image? _sliderImage;

  @override
  void initState() {
    super.initState();
    loadImage("images/bgi (8).webp"); // 加载图片
  }

  // 加载图片并转换为 ui.Image
  Future loadImage(String path) async {
    // 加载资源文件
    final data = await rootBundle.load(path);
    // 把资源文件转换成Uint8List类型
    final bytes = data.buffer.asUint8List();
    // 解析Uint8List类型的数据图片
    final image = await decodeImageFromList(bytes);
    _sliderImage = image;
    setState(() {});
  }

  @override
  Widget build(BuildContext context) {
    return _sliderImage == null
        ? const Center(child: CircularProgressIndicator()) // 加载中
        : CustomPaint(
            size: const Size(450, 320),
            painter: ArcProgressPainter(
              progress: widget.progress,
              backgroundColor: Colors.grey,
              strokeWidth: 5.0,
              // textStyle: const TextStyle(color: Colors.black, fontSize: 12),
              sliderImage: _sliderImage!, // 传递 ui.Image
            ),
          );
  }
}