percent_widget.dart
4.67 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
import 'package:flutter/material.dart';
import 'paint.dart';
/// 环形百分比进度 Widget(专为单环百分比展示设计,保持平滑动画)
class CircularPercentProgressWidget extends StatefulWidget {
/// 尺寸宽高
final double size;
/// 圆环宽度
final double strokeWidth;
/// 进度百分比 (0.0 ~ 1.0+,例如 0.85 表示 85%,null 或 < 0 表示无数据)
final double? percent;
/// 渐变色或纯色列表
final List<Color> gradientColors;
/// 底环背景颜色
final Color backgroundColor;
/// 是否开启动画
final bool animate;
/// 动画时长
final Duration duration;
/// 动画曲线
final Curve curve;
/// 是否在中心显示文字
final bool showText;
/// 自定义中心文字样式
final TextStyle? textStyle;
/// 无数据时显示的文字,默认为 '-'
final String noDataText;
const CircularPercentProgressWidget({
super.key,
required this.gradientColors,
this.percent,
this.size = 50,
this.strokeWidth = 4.5,
this.backgroundColor = const Color(0xFFF3F3F3),
this.animate = true,
this.duration = const Duration(milliseconds: 1000),
this.curve = Curves.easeOutCubic,
this.showText = true,
this.textStyle,
this.noDataText = '-',
});
@override
State<CircularPercentProgressWidget> createState() =>
_CircularPercentProgressWidgetState();
}
class _CircularPercentProgressWidgetState
extends State<CircularPercentProgressWidget>
with SingleTickerProviderStateMixin {
late AnimationController _controller;
late Animation<double> _animation;
double _currentSweepAngle = 0;
double get _targetSweepAngle {
if (widget.percent == null || widget.percent! < 0) {
return -1;
}
return widget.percent! * 360;
}
@override
void initState() {
super.initState();
_controller = AnimationController(
vsync: this,
duration: widget.duration,
);
final targetAngle = _targetSweepAngle;
_updateAnimation(0, targetAngle);
}
@override
void didUpdateWidget(CircularPercentProgressWidget oldWidget) {
super.didUpdateWidget(oldWidget);
if (widget.percent != oldWidget.percent) {
_updateAnimation(_currentSweepAngle, _targetSweepAngle);
}
}
void _updateAnimation(double fromAngle, double toAngle) {
if (toAngle < 0) {
setState(() {
_currentSweepAngle = -1;
});
return;
}
final begin = fromAngle < 0 ? 0.0 : fromAngle;
if (!widget.animate) {
setState(() {
_currentSweepAngle = toAngle;
});
return;
}
_controller.duration = widget.duration;
_animation = Tween<double>(begin: begin, end: toAngle).animate(
CurvedAnimation(parent: _controller, curve: widget.curve),
)..addListener(() {
setState(() {
_currentSweepAngle = _animation.value;
});
});
_controller.forward(from: 0);
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
final colors = widget.gradientColors.length == 1
? [widget.gradientColors.first, widget.gradientColors.first]
: widget.gradientColors;
final hasData = widget.percent != null && widget.percent! >= 0;
String displayText;
if (!hasData) {
displayText = widget.noDataText;
} else {
final currentPercentInt = ((_currentSweepAngle / 360) * 100).round();
displayText = '$currentPercentInt%';
}
final defaultTextStyle = TextStyle(
fontSize: 10,
fontWeight: FontWeight.w600,
color: const Color(0xFF0F0F11),
letterSpacing: -0.5,
);
return SizedBox(
width: widget.size,
height: widget.size,
child: Stack(
alignment: Alignment.center,
children: [
CustomPaint(
painter: ProgressPainter(
sweepAngle: _currentSweepAngle,
progressColor: [colors],
strokeWidth: widget.strokeWidth,
backgroundColor: widget.backgroundColor,
reverse: false,
),
size: Size(widget.size, widget.size),
),
if (widget.showText)
Container(
width: widget.size - widget.strokeWidth * 2 - 4,
height: widget.size - widget.strokeWidth * 2 - 4,
alignment: Alignment.center,
child: FittedBox(
fit: BoxFit.scaleDown,
child: Text(
displayText,
textAlign: TextAlign.center,
maxLines: 1,
style: widget.textStyle ?? defaultTextStyle,
),
),
),
],
),
);
}
}