arc_progress_widget.dart
6.63 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
import 'dart:math';
import 'package:flutter/material.dart';
/// 弧形进度单环配置
class ArcProgressItem {
/// 进度比例 (0.0 ~ 1.0,支持超过 1.0,<= 0 或 null 表示无数据/未开始)
final double? ratio;
/// 高亮进度颜色
final Color color;
/// 底轨背景色
final Color backgroundColor;
/// 无数据时的底轨背景色 (若为空则使用 [backgroundColor])
final Color? noDataBackgroundColor;
const ArcProgressItem({
required this.ratio,
required this.color,
required this.backgroundColor,
this.noDataBackgroundColor,
});
}
/// 华为版开口朝下的彩虹弧形多环进度 Widget(支持平滑动画,无中心文字)
class ArcMultiProgressWidget extends StatefulWidget {
/// 尺寸宽高 (正方形)
final double size;
/// 环形线宽 (如果为 null,将根据环数自适应计算)
final double? strokeWidth;
/// 环与环之间的间距 (如果为 null,将根据环数自适应计算)
final double? gap;
/// 起始角度 (以度为单位,150° 表示从左下方开始)
final double startAngleDeg;
/// 总扫过角度 (以度为单位,240° 表示从左下顺时针扫至右下,底部开口 120°)
final double sweepTotalDeg;
/// 环项列表 (从最外环到最内环排序)
final List<ArcProgressItem> items;
/// 是否开启动画
final bool animate;
/// 动画时长
final Duration duration;
/// 动画曲线
final Curve curve;
const ArcMultiProgressWidget({
super.key,
required this.items,
this.size = 50,
this.strokeWidth,
this.gap,
this.startAngleDeg = 150,
this.sweepTotalDeg = 240,
this.animate = true,
this.duration = const Duration(milliseconds: 1000),
this.curve = Curves.easeOutCubic,
});
@override
State<ArcMultiProgressWidget> createState() => _ArcMultiProgressWidgetState();
}
class _ArcMultiProgressWidgetState extends State<ArcMultiProgressWidget>
with SingleTickerProviderStateMixin {
late AnimationController _controller;
late Animation<double> _animation;
late List<double> _currentProgressAngles;
List<double> get _targetProgressAngles {
final totalRad = widget.sweepTotalDeg * pi / 180.0;
return widget.items.map((item) {
if (item.ratio == null || item.ratio! <= 0) {
return 0.0;
}
final clampedRatio = min(item.ratio!, 1.0);
return clampedRatio * totalRad;
}).toList();
}
@override
void initState() {
super.initState();
_currentProgressAngles = List.filled(widget.items.length, 0.0);
_controller = AnimationController(
vsync: this,
duration: widget.duration,
);
final targets = _targetProgressAngles;
_updateAnimation(_currentProgressAngles, targets);
}
@override
void didUpdateWidget(ArcMultiProgressWidget oldWidget) {
super.didUpdateWidget(oldWidget);
final oldTargets = oldWidget.items.map((e) => e.ratio ?? 0.0).toList();
final newTargets = widget.items.map((e) => e.ratio ?? 0.0).toList();
if (oldTargets.toString() != newTargets.toString()) {
_updateAnimation(_currentProgressAngles, _targetProgressAngles);
}
}
void _updateAnimation(List<double> fromAngles, List<double> toAngles) {
if (!widget.animate) {
setState(() {
_currentProgressAngles = List.from(toAngles);
});
return;
}
final startList = List<double>.from(fromAngles);
// 确保长度一致
while (startList.length < toAngles.length) {
startList.add(0.0);
}
_controller.duration = widget.duration;
_animation = CurvedAnimation(parent: _controller, curve: widget.curve);
_controller.addListener(() {
final t = _animation.value;
setState(() {
_currentProgressAngles = List.generate(toAngles.length, (i) {
final start = i < startList.length ? startList[i] : 0.0;
final end = toAngles[i];
return start + (end - start) * t;
});
});
});
_controller.forward(from: 0);
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
final count = widget.items.length;
// 自动计算粗细和间距
final strokeWidth = widget.strokeWidth ?? (count <= 2 ? 4.5 : 3.6);
final gap = widget.gap ?? (count <= 2 ? 2.5 : 1.8);
final startAngleRad = widget.startAngleDeg * pi / 180.0;
final totalSweepRad = widget.sweepTotalDeg * pi / 180.0;
return SizedBox(
width: widget.size,
height: widget.size,
child: CustomPaint(
painter: _ArcMultiProgressPainter(
animatedProgresses: _currentProgressAngles,
items: widget.items,
strokeWidth: strokeWidth,
gap: gap,
startAngleRad: startAngleRad,
totalSweepRad: totalSweepRad,
),
),
);
}
}
class _ArcMultiProgressPainter extends CustomPainter {
final List<double> animatedProgresses;
final List<ArcProgressItem> items;
final double strokeWidth;
final double gap;
final double startAngleRad;
final double totalSweepRad;
_ArcMultiProgressPainter({
required this.animatedProgresses,
required this.items,
required this.strokeWidth,
required this.gap,
required this.startAngleRad,
required this.totalSweepRad,
});
@override
void paint(Canvas canvas, Size size) {
final center = Offset(size.width / 2, size.height / 2);
final count = items.length;
for (int i = 0; i < count; i++) {
final item = items[i];
final radius = (size.width - strokeWidth) / 2 - i * (strokeWidth + gap);
if (radius <= 0) continue;
final rect = Rect.fromCircle(center: center, radius: radius);
// 1. 绘制底轨
final hasData = item.ratio != null && item.ratio! > 0;
final bgColor = (!hasData && item.noDataBackgroundColor != null)
? item.noDataBackgroundColor!
: item.backgroundColor;
final trackPaint = Paint()
..color = bgColor
..strokeWidth = strokeWidth
..style = PaintingStyle.stroke
..strokeCap = StrokeCap.round;
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);
}
}
}
@override
bool shouldRepaint(covariant _ArcMultiProgressPainter oldDelegate) {
return true;
}
}