import 'dart:math' as math; import 'package:flutter/material.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; /// ============================================================ /// 【SemicircleProgress】半圆正确率进度环 /// 1:1 对标 Vue components/lProgressCircle/lProgressCircle.vue /// (report.vue 用法:width 308rpx / height 168rpx / strokeWidth 14 / /// bgColor #D8DFEB / progressColor #1578FF,上半圆从左到右) /// /// 尺寸说明(Vue canvas 逻辑像素 → Flutter dp): /// 308rpx = 154.w,168rpx = 84.h,描边 14px = 7.w, /// paddingTop 默认 8px = 4.h; /// radius = min(宽/2 - 描边, 高 - 描边 - paddingTop) /// ============================================================ class SemicircleProgress extends StatelessWidget { const SemicircleProgress({ super.key, required this.percent, this.width = 154, this.height = 84, this.strokeWidth = 10, this.paddingTop = 4, this.trackColor = const Color(0xFFD8DFEB), this.progressColor = const Color(0xFF1578FF), }); /// 0 ~ 100 final num percent; final double width; final double height; final double strokeWidth; final double paddingTop; final Color trackColor; final Color progressColor; @override Widget build(BuildContext context) { final w = width.w; final h = height.h; final sw = strokeWidth.w; return SizedBox( width: w, height: h, child: CustomPaint( size: Size(w, h), painter: _SemicirclePainter( percent: percent.toDouble().clamp(0, 100), strokeWidth: sw, paddingTop: paddingTop.h, trackColor: trackColor, progressColor: progressColor, ), // 对标 Vue slot:progress-content top 43rpx,居中展示数值 child: Padding( padding: EdgeInsets.only(top: 31.5.h), child: Column( mainAxisSize: MainAxisSize.min, children: [ // 大数字 + %(font-50 粗体标题色 + font-24 副色 %,底 10rpx) Padding( padding: EdgeInsets.only(bottom: 5.h), child: RichText( text: TextSpan( children: [ TextSpan( text: '${percent % 1 == 0 ? percent.toInt() : percent}', style: TextStyle( fontSize: 25.sp, fontWeight: FontWeight.w600, color: const Color(0xFF1D2B47), ), ), TextSpan( text: '%', style: TextStyle( fontSize: 12.sp, color: const Color(0xFF495A7A), ), ), ], ), ), ), Text( '本次正确率', style: TextStyle( fontSize: 12.sp, color: const Color(0xFF687FAD), ), ), ], ), ), ), ); } } class _SemicirclePainter extends CustomPainter { const _SemicirclePainter({ required this.percent, required this.strokeWidth, required this.paddingTop, required this.trackColor, required this.progressColor, }); final double percent; final double strokeWidth; final double paddingTop; final Color trackColor; final Color progressColor; @override void paint(Canvas canvas, Size size) { final halfStroke = strokeWidth / 2; final maxRadiusByWidth = size.width / 2 - strokeWidth; final maxRadiusByHeight = size.height - strokeWidth - paddingTop; final radius = math.max(0.0, math.min(maxRadiusByWidth, maxRadiusByHeight)); final centerX = size.width / 2; final centerY = size.height - halfStroke; final trackPaint = Paint() ..style = PaintingStyle.stroke ..strokeWidth = strokeWidth ..strokeCap = StrokeCap.round ..color = trackColor; // 背景半圆环:π(左)→ 0(右) canvas.drawArc( Rect.fromCircle(center: Offset(centerX, centerY), radius: radius), math.pi, math.pi, false, trackPaint, ); // 进度弧 if (percent > 0) { final progressPaint = Paint() ..style = PaintingStyle.stroke ..strokeWidth = strokeWidth ..strokeCap = StrokeCap.round ..color = progressColor; final sweep = math.pi * (percent / 100); canvas.drawArc( Rect.fromCircle(center: Offset(centerX, centerY), radius: radius), math.pi, sweep, false, progressPaint, ); } } @override bool shouldRepaint(covariant _SemicirclePainter oldDelegate) { return oldDelegate.percent != percent || oldDelegate.strokeWidth != strokeWidth || oldDelegate.trackColor != trackColor || oldDelegate.progressColor != progressColor; } }