multi_segment_circle_indicator.dart 4.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136
  1. import 'dart:ui';
  2. import 'package:flutter/Material.dart';
  3. import 'package:flutter_screenutil/flutter_screenutil.dart';
  4. class PieData {
  5. final String label;
  6. final double value;
  7. final Color color;
  8. PieData(this.label, this.value, this.color);
  9. }
  10. class MultiSegmentCircleIndicator extends StatelessWidget {
  11. final List<PieData> segments;
  12. final double radius;
  13. final double strokeWidth;
  14. final Duration animationDuration;
  15. final String Function(double percent)? centerTextBuilder;
  16. final String? subtitle;
  17. final double centerValue;
  18. const MultiSegmentCircleIndicator({
  19. super.key,
  20. required this.segments,
  21. this.radius = 60,
  22. this.strokeWidth = 12,
  23. this.animationDuration = const Duration(seconds: 1),
  24. this.centerTextBuilder,
  25. required this.centerValue,
  26. this.subtitle,
  27. });
  28. @override
  29. Widget build(BuildContext context) {
  30. return TweenAnimationBuilder<double>(
  31. tween: Tween<double>(begin: 0, end: 1),
  32. duration: animationDuration,
  33. builder: (context, value, child) {
  34. final animatedSegments = segments
  35. .map((e) => PieData(e.label, e.value * value, e.color))
  36. .toList();
  37. return Column(
  38. mainAxisSize: MainAxisSize.min,
  39. children: [
  40. SizedBox(
  41. width: radius * 2,
  42. height: radius * 2,
  43. child: CustomPaint(
  44. painter: _CirclePainter(
  45. segments: animatedSegments,
  46. strokeWidth: strokeWidth,
  47. ),
  48. child: Center(
  49. child: Column(
  50. mainAxisSize: MainAxisSize.min,
  51. textBaseline: TextBaseline.alphabetic,
  52. children: [
  53. Text.rich(TextSpan(children: [
  54. TextSpan(
  55. text: centerTextBuilder?.call(centerValue * value) ??
  56. (centerValue * value).toStringAsFixed(0),
  57. style: TextStyle(
  58. color: Colors.white.withAlpha(229),
  59. fontSize: 30.sp,
  60. fontWeight: FontWeight.w400,
  61. ),
  62. ),
  63. TextSpan(
  64. text: '%',
  65. style: TextStyle(
  66. color: Colors.white.withAlpha(229),
  67. fontSize: 13.03.sp,
  68. fontWeight: FontWeight.w500,
  69. ),
  70. ),
  71. ])),
  72. if (subtitle != null)
  73. Text(
  74. subtitle!,
  75. style: TextStyle(
  76. fontSize: 14,
  77. color: Colors.white.withOpacity(0.6),
  78. ),
  79. ),
  80. ],
  81. ),
  82. ),
  83. ),
  84. ),
  85. const SizedBox(height: 12),
  86. ],
  87. );
  88. },
  89. );
  90. }
  91. }
  92. class _CirclePainter extends CustomPainter {
  93. final List<PieData> segments;
  94. final double strokeWidth;
  95. _CirclePainter({required this.segments, this.strokeWidth = 16});
  96. @override
  97. void paint(Canvas canvas, Size size) {
  98. final center = size.center(Offset.zero);
  99. final radius = (size.shortestSide - strokeWidth) / 2;
  100. final paint = Paint()
  101. ..style = PaintingStyle.stroke
  102. ..strokeWidth = strokeWidth
  103. ..strokeCap = StrokeCap.round;
  104. double startAngle = -90.0;
  105. for (final segment in segments) {
  106. final sweepAngle = 360 * (segment.value / 100);
  107. paint.color = segment.color;
  108. canvas.drawArc(
  109. Rect.fromCircle(center: center, radius: radius),
  110. _toRadians(startAngle),
  111. _toRadians(sweepAngle),
  112. false,
  113. paint,
  114. );
  115. startAngle += sweepAngle;
  116. }
  117. }
  118. double _toRadians(double degrees) => degrees * 3.1415926 / 180;
  119. @override
  120. bool shouldRepaint(covariant CustomPainter oldDelegate) => true;
  121. }