|
...
|
...
|
@@ -52,6 +52,27 @@ class ArcMultiProgressWidget extends StatefulWidget { |
|
|
|
/// 动画曲线
|
|
|
|
final Curve curve;
|
|
|
|
|
|
|
|
/// 超过 1 圈时端头阴影颜色 (默认半透明黑)
|
|
|
|
final Color shadowColor;
|
|
|
|
|
|
|
|
/// 阴影模糊半径 (越大越发散,越小越清晰,默认 2.2)
|
|
|
|
final double shadowBlur;
|
|
|
|
|
|
|
|
/// 阴影顺时针偏移距离 (像素,默认 0.8)
|
|
|
|
final double shadowOffset;
|
|
|
|
|
|
|
|
/// 合环三角形宽度占 size 的比例 (默认 15.27 / 50 ≈ 0.3054)
|
|
|
|
final double triangleWidthRatio;
|
|
|
|
|
|
|
|
/// 合环三角形高度占 size 的比例 (默认 8.27 / 50 ≈ 0.1654)
|
|
|
|
final double triangleHeightRatio;
|
|
|
|
|
|
|
|
/// 合环三角形向下溢出底部的比例 (占 size 的比例,默认 0.0)
|
|
|
|
final double triangleBottomOverflowRatio;
|
|
|
|
|
|
|
|
/// 合环三角形圆角半径占 size 的比例 (默认 1.4 / 50 ≈ 0.028)
|
|
|
|
final double triangleCornerRadiusRatio;
|
|
|
|
|
|
|
|
const ArcMultiProgressWidget({
|
|
|
|
super.key,
|
|
|
|
required this.items,
|
|
...
|
...
|
@@ -63,6 +84,13 @@ class ArcMultiProgressWidget extends StatefulWidget { |
|
|
|
this.animate = true,
|
|
|
|
this.duration = const Duration(milliseconds: 1000),
|
|
|
|
this.curve = Curves.easeOutCubic,
|
|
|
|
this.shadowColor = const Color.fromARGB(173, 0, 0, 0), // alpha ~ 0.35
|
|
|
|
this.shadowBlur = 2.2,
|
|
|
|
this.shadowOffset = 0.8,
|
|
|
|
this.triangleWidthRatio = 0.42,
|
|
|
|
this.triangleHeightRatio = 0.22,
|
|
|
|
this.triangleBottomOverflowRatio = 0.04,
|
|
|
|
this.triangleCornerRadiusRatio = 0.02,
|
|
|
|
});
|
|
|
|
|
|
|
|
@override
|
|
...
|
...
|
@@ -81,8 +109,7 @@ class _ArcMultiProgressWidgetState extends State<ArcMultiProgressWidget> |
|
|
|
if (item.ratio == null || item.ratio! <= 0) {
|
|
|
|
return 0.0;
|
|
|
|
}
|
|
|
|
final clampedRatio = min(item.ratio!, 1.0);
|
|
|
|
return clampedRatio * totalRad;
|
|
|
|
return item.ratio! * totalRad;
|
|
|
|
}).toList();
|
|
|
|
}
|
|
|
|
|
|
...
|
...
|
@@ -118,7 +145,6 @@ class _ArcMultiProgressWidgetState extends State<ArcMultiProgressWidget> |
|
|
|
}
|
|
|
|
|
|
|
|
final startList = List<double>.from(fromAngles);
|
|
|
|
// 确保长度一致
|
|
|
|
while (startList.length < toAngles.length) {
|
|
|
|
startList.add(0.0);
|
|
|
|
}
|
|
...
|
...
|
@@ -167,6 +193,13 @@ class _ArcMultiProgressWidgetState extends State<ArcMultiProgressWidget> |
|
|
|
gap: gap,
|
|
|
|
startAngleRad: startAngleRad,
|
|
|
|
totalSweepRad: totalSweepRad,
|
|
|
|
shadowColor: widget.shadowColor,
|
|
|
|
shadowBlur: widget.shadowBlur,
|
|
|
|
shadowOffset: widget.shadowOffset,
|
|
|
|
triangleWidthRatio: widget.triangleWidthRatio,
|
|
|
|
triangleHeightRatio: widget.triangleHeightRatio,
|
|
|
|
triangleBottomOverflowRatio: widget.triangleBottomOverflowRatio,
|
|
|
|
triangleCornerRadiusRatio: widget.triangleCornerRadiusRatio,
|
|
|
|
),
|
|
|
|
),
|
|
|
|
);
|
|
...
|
...
|
@@ -180,6 +213,13 @@ class _ArcMultiProgressPainter extends CustomPainter { |
|
|
|
final double gap;
|
|
|
|
final double startAngleRad;
|
|
|
|
final double totalSweepRad;
|
|
|
|
final Color shadowColor;
|
|
|
|
final double shadowBlur;
|
|
|
|
final double shadowOffset;
|
|
|
|
final double triangleWidthRatio;
|
|
|
|
final double triangleHeightRatio;
|
|
|
|
final double triangleBottomOverflowRatio;
|
|
|
|
final double triangleCornerRadiusRatio;
|
|
|
|
|
|
|
|
_ArcMultiProgressPainter({
|
|
|
|
required this.animatedProgresses,
|
|
...
|
...
|
@@ -188,6 +228,13 @@ class _ArcMultiProgressPainter extends CustomPainter { |
|
|
|
required this.gap,
|
|
|
|
required this.startAngleRad,
|
|
|
|
required this.totalSweepRad,
|
|
|
|
required this.shadowColor,
|
|
|
|
required this.shadowBlur,
|
|
|
|
required this.shadowOffset,
|
|
|
|
required this.triangleWidthRatio,
|
|
|
|
required this.triangleHeightRatio,
|
|
|
|
required this.triangleBottomOverflowRatio,
|
|
|
|
required this.triangleCornerRadiusRatio,
|
|
|
|
});
|
|
|
|
|
|
|
|
@override
|
|
...
|
...
|
@@ -202,7 +249,7 @@ class _ArcMultiProgressPainter extends CustomPainter { |
|
|
|
|
|
|
|
final rect = Rect.fromCircle(center: center, radius: radius);
|
|
|
|
|
|
|
|
// 1. 绘制底轨
|
|
|
|
// 1. 绘制底轨 (Track)
|
|
|
|
final hasData = item.ratio != null && item.ratio! > 0;
|
|
|
|
final bgColor = (!hasData && item.noDataBackgroundColor != null)
|
|
|
|
? item.noDataBackgroundColor!
|
|
...
|
...
|
@@ -216,19 +263,170 @@ class _ArcMultiProgressPainter extends CustomPainter { |
|
|
|
|
|
|
|
canvas.drawArc(rect, startAngleRad, totalSweepRad, false, trackPaint);
|
|
|
|
|
|
|
|
// 2. 绘制进度
|
|
|
|
final progressSweep =
|
|
|
|
i < animatedProgresses.length ? animatedProgresses[i] : 0.0;
|
|
|
|
if (progressSweep > 0.001) {
|
|
|
|
final progressPaint = Paint()
|
|
|
|
..color = item.color
|
|
|
|
..strokeWidth = strokeWidth
|
|
|
|
..style = PaintingStyle.stroke
|
|
|
|
..strokeCap = StrokeCap.round;
|
|
|
|
|
|
|
|
canvas.drawArc(rect, startAngleRad, progressSweep, false, progressPaint);
|
|
|
|
// 2. 绘制进度 (Progress)
|
|
|
|
final sweep = i < animatedProgresses.length ? animatedProgresses[i] : 0.0;
|
|
|
|
if (sweep <= 0.001) continue;
|
|
|
|
|
|
|
|
final fullLaps = (sweep / totalSweepRad).floor();
|
|
|
|
final remainingSweep = sweep - fullLaps * totalSweepRad;
|
|
|
|
|
|
|
|
final progressPaint = Paint()
|
|
|
|
..color = item.color
|
|
|
|
..strokeWidth = strokeWidth
|
|
|
|
..style = PaintingStyle.stroke
|
|
|
|
..strokeCap = StrokeCap.round;
|
|
|
|
|
|
|
|
// ① 如果至少满一圈,先绘制完整的底层圆弧
|
|
|
|
if (fullLaps >= 1) {
|
|
|
|
canvas.drawArc(
|
|
|
|
rect, startAngleRad, totalSweepRad, false, progressPaint);
|
|
|
|
}
|
|
|
|
|
|
|
|
// ② 绘制超出的部分(或第一圈未满的部分)
|
|
|
|
if (fullLaps >= 1) {
|
|
|
|
if (remainingSweep > 0.01) {
|
|
|
|
final currentEndAngle = startAngleRad + remainingSweep;
|
|
|
|
|
|
|
|
// 沿切线前进方向微小偏移
|
|
|
|
final tangentOffsetX = -shadowOffset * sin(currentEndAngle);
|
|
|
|
final tangentOffsetY = shadowOffset * cos(currentEndAngle);
|
|
|
|
|
|
|
|
final shadowCenter = Offset(
|
|
|
|
center.dx + radius * cos(currentEndAngle) + tangentOffsetX,
|
|
|
|
center.dy + radius * sin(currentEndAngle) + tangentOffsetY,
|
|
|
|
);
|
|
|
|
|
|
|
|
final capCenter = Offset(
|
|
|
|
center.dx + radius * cos(currentEndAngle),
|
|
|
|
center.dy + radius * sin(currentEndAngle),
|
|
|
|
);
|
|
|
|
|
|
|
|
// 构造当前圆环轨道的裁剪区域,严格限制阴影只在环槽内显示,绝不溢出到外圈或内圈
|
|
|
|
final trackClipPath = Path()
|
|
|
|
..addOval(Rect.fromCircle(
|
|
|
|
center: center, radius: radius + strokeWidth / 2))
|
|
|
|
..addOval(Rect.fromCircle(
|
|
|
|
center: center, radius: radius - strokeWidth / 2))
|
|
|
|
..fillType = PathFillType.evenOdd;
|
|
|
|
|
|
|
|
// 绘制被精准裁剪的自然阴影
|
|
|
|
canvas.save();
|
|
|
|
canvas.clipPath(trackClipPath);
|
|
|
|
final shadowPaint = Paint()
|
|
|
|
..color = shadowColor
|
|
|
|
..style = PaintingStyle.fill
|
|
|
|
..maskFilter = MaskFilter.blur(BlurStyle.normal, shadowBlur);
|
|
|
|
canvas.drawCircle(shadowCenter, strokeWidth / 2, shadowPaint);
|
|
|
|
canvas.restore();
|
|
|
|
|
|
|
|
// 绘制第二层圆弧(带圆角端头,压在阴影上)
|
|
|
|
canvas.drawArc(
|
|
|
|
rect, startAngleRad, remainingSweep, false, progressPaint);
|
|
|
|
|
|
|
|
// 显式绘制顶层端帽实心圆
|
|
|
|
final capFillPaint = Paint()
|
|
|
|
..color = item.color
|
|
|
|
..style = PaintingStyle.fill;
|
|
|
|
canvas.drawCircle(capCenter, strokeWidth / 2, capFillPaint);
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
// 第一圈未满,直接绘制
|
|
|
|
canvas.drawArc(rect, startAngleRad, sweep, false, progressPaint);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// 3. 当所有环都合环(>= 100%)时,在底部中央开口处绘制圆润的合环指示三角形
|
|
|
|
final isAllCompleted = items.isNotEmpty &&
|
|
|
|
List.generate(items.length, (idx) {
|
|
|
|
final sw =
|
|
|
|
idx < animatedProgresses.length ? animatedProgresses[idx] : 0.0;
|
|
|
|
return sw >= totalSweepRad - 0.01;
|
|
|
|
}).every((completed) => completed);
|
|
|
|
|
|
|
|
if (isAllCompleted && count > 0) {
|
|
|
|
// 按照 size 的比例计算宽度、高度、溢出与圆角
|
|
|
|
final triangleWidth = size.width * triangleWidthRatio;
|
|
|
|
final triangleHeight = size.height * triangleHeightRatio;
|
|
|
|
final halfWidth = triangleWidth / 2;
|
|
|
|
|
|
|
|
final outerRadius = (size.width - strokeWidth) / 2;
|
|
|
|
// 以最外环端点下边缘(合环底部最低点)为 0 基准
|
|
|
|
final ringBottomY =
|
|
|
|
center.dy + outerRadius * sin(startAngleRad) + strokeWidth / 2;
|
|
|
|
|
|
|
|
final overflow = size.height * triangleBottomOverflowRatio;
|
|
|
|
final bottomY = ringBottomY + overflow;
|
|
|
|
final topY = bottomY - triangleHeight;
|
|
|
|
|
|
|
|
final topPoint = Offset(center.dx, topY);
|
|
|
|
final rightPoint = Offset(center.dx + halfWidth, bottomY);
|
|
|
|
final leftPoint = Offset(center.dx - halfWidth, bottomY);
|
|
|
|
|
|
|
|
final cornerRadius =
|
|
|
|
size.width * (triangleCornerRadiusRatio ?? (1.4 / 50.0));
|
|
|
|
|
|
|
|
final roundedTrianglePath = _createRoundedTrianglePath(
|
|
|
|
top: topPoint,
|
|
|
|
right: rightPoint,
|
|
|
|
left: leftPoint,
|
|
|
|
cornerRadius: cornerRadius,
|
|
|
|
);
|
|
|
|
|
|
|
|
final trianglePaint = Paint()
|
|
|
|
..color = items.first.color
|
|
|
|
..style = PaintingStyle.fill;
|
|
|
|
|
|
|
|
canvas.drawPath(roundedTrianglePath, trianglePaint);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/// 绘制平滑圆角三角形
|
|
|
|
Path _createRoundedTrianglePath({
|
|
|
|
required Offset top,
|
|
|
|
required Offset right,
|
|
|
|
required Offset left,
|
|
|
|
required double cornerRadius,
|
|
|
|
}) {
|
|
|
|
final path = Path();
|
|
|
|
final points = [top, right, left];
|
|
|
|
final n = points.length;
|
|
|
|
|
|
|
|
for (int i = 0; i < n; i++) {
|
|
|
|
final p0 = points[(i - 1 + n) % n];
|
|
|
|
final p1 = points[i];
|
|
|
|
final p2 = points[(i + 1) % n];
|
|
|
|
|
|
|
|
final v1 = Offset(p0.dx - p1.dx, p0.dy - p1.dy);
|
|
|
|
final v2 = Offset(p2.dx - p1.dx, p2.dy - p1.dy);
|
|
|
|
|
|
|
|
final d1 = sqrt(v1.dx * v1.dx + v1.dy * v1.dy);
|
|
|
|
final d2 = sqrt(v2.dx * v2.dx + v2.dy * v2.dy);
|
|
|
|
|
|
|
|
final u1 = Offset(v1.dx / d1, v1.dy / d1);
|
|
|
|
final u2 = Offset(v2.dx / d2, v2.dy / d2);
|
|
|
|
|
|
|
|
final dot = (u1.dx * u2.dx + u1.dy * u2.dy).clamp(-1.0, 1.0);
|
|
|
|
final angle = acos(dot);
|
|
|
|
final cutDist = cornerRadius / tan(angle / 2);
|
|
|
|
|
|
|
|
final cut1 = Offset(p1.dx + u1.dx * cutDist, p1.dy + u1.dy * cutDist);
|
|
|
|
final cut2 = Offset(p1.dx + u2.dx * cutDist, p1.dy + u2.dy * cutDist);
|
|
|
|
|
|
|
|
if (i == 0) {
|
|
|
|
path.moveTo(cut1.dx, cut1.dy);
|
|
|
|
} else {
|
|
|
|
path.lineTo(cut1.dx, cut1.dy);
|
|
|
|
}
|
|
|
|
|
|
|
|
path.arcToPoint(
|
|
|
|
cut2,
|
|
|
|
radius: Radius.circular(cornerRadius),
|
|
|
|
clockwise: true,
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
path.close();
|
|
|
|
return path;
|
|
|
|
}
|
|
|
|
|
|
|
|
@override
|
...
|
...
|
|