167 lines
5.0 KiB
Dart
167 lines
5.0 KiB
Dart
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;
|
||
}
|
||
}
|