188 lines
4.4 KiB
Dart
188 lines
4.4 KiB
Dart
import 'dart:math';
|
|
|
|
import 'package:fl_clash/common/common.dart';
|
|
import 'package:flutter/material.dart';
|
|
|
|
@immutable
|
|
class DonutChartData {
|
|
final double _value;
|
|
final Color color;
|
|
|
|
const DonutChartData({
|
|
required double value,
|
|
required this.color,
|
|
}) : _value = value + 1;
|
|
|
|
double get value => _value;
|
|
|
|
@override
|
|
String toString() {
|
|
return 'DonutChartData{_value: $_value}';
|
|
}
|
|
|
|
@override
|
|
bool operator ==(Object other) =>
|
|
identical(this, other) ||
|
|
other is DonutChartData &&
|
|
runtimeType == other.runtimeType &&
|
|
_value == other._value &&
|
|
color == other.color;
|
|
|
|
@override
|
|
int get hashCode => _value.hashCode ^ color.hashCode;
|
|
}
|
|
|
|
class DonutChart extends StatefulWidget {
|
|
final List<DonutChartData> data;
|
|
final Duration duration;
|
|
|
|
const DonutChart({
|
|
super.key,
|
|
required this.data,
|
|
this.duration = commonDuration,
|
|
});
|
|
|
|
@override
|
|
State<DonutChart> createState() => _DonutChartState();
|
|
}
|
|
|
|
class _DonutChartState extends State<DonutChart>
|
|
with SingleTickerProviderStateMixin {
|
|
late AnimationController _animationController;
|
|
late List<DonutChartData> _oldData;
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
_oldData = widget.data;
|
|
_animationController = AnimationController(
|
|
vsync: this,
|
|
duration: widget.duration,
|
|
);
|
|
}
|
|
|
|
@override
|
|
void didUpdateWidget(DonutChart oldWidget) {
|
|
super.didUpdateWidget(oldWidget);
|
|
if (oldWidget.data != widget.data) {
|
|
_oldData = oldWidget.data;
|
|
_animationController.forward(from: 0);
|
|
}
|
|
}
|
|
|
|
@override
|
|
void dispose() {
|
|
_animationController.dispose();
|
|
super.dispose();
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return AnimatedBuilder(
|
|
animation: _animationController,
|
|
builder: (context, child) {
|
|
return CustomPaint(
|
|
painter: DonutChartPainter(
|
|
_oldData,
|
|
widget.data,
|
|
_animationController.value,
|
|
),
|
|
);
|
|
},
|
|
);
|
|
}
|
|
}
|
|
|
|
class DonutChartPainter extends CustomPainter {
|
|
final List<DonutChartData> oldData;
|
|
final List<DonutChartData> newData;
|
|
final double progress;
|
|
|
|
DonutChartPainter(this.oldData, this.newData, this.progress);
|
|
|
|
double _logTransform(double value) {
|
|
const base = 10.0;
|
|
const minValue = 0.1;
|
|
if (value < minValue) return 0;
|
|
return log(value) / log(base) + 1;
|
|
}
|
|
|
|
double _expTransform(double value) {
|
|
const base = 10.0;
|
|
if (value <= 0) return 0;
|
|
return pow(base, value - 1).toDouble();
|
|
}
|
|
|
|
List<DonutChartData> get interpolatedData {
|
|
if (oldData.length != newData.length) return newData;
|
|
|
|
final interpolatedData = List.generate(newData.length, (index) {
|
|
final oldValue = oldData[index].value;
|
|
final newValue = newData[index].value;
|
|
|
|
final logOldValue = _logTransform(oldValue);
|
|
final logNewValue = _logTransform(newValue);
|
|
final interpolatedLogValue =
|
|
logOldValue + (logNewValue - logOldValue) * progress;
|
|
|
|
final interpolatedValue = _expTransform(interpolatedLogValue);
|
|
|
|
return DonutChartData(
|
|
value: interpolatedValue,
|
|
color: newData[index].color,
|
|
);
|
|
});
|
|
|
|
return interpolatedData;
|
|
}
|
|
|
|
@override
|
|
void paint(Canvas canvas, Size size) {
|
|
final center = Offset(size.width / 2, size.height / 2);
|
|
const strokeWidth = 10.0;
|
|
final radius = min(size.width / 2, size.height / 2) - strokeWidth / 2;
|
|
|
|
final gapAngle = 2 * asin(strokeWidth * 1 / (2 * radius)) * 1.2;
|
|
|
|
final data = interpolatedData;
|
|
final total = data.fold<double>(
|
|
0,
|
|
(sum, item) => sum + item.value,
|
|
);
|
|
|
|
if (total <= 0) return;
|
|
|
|
final availableAngle = 2 * pi - (data.length * gapAngle);
|
|
double startAngle = -pi / 2 + gapAngle / 2;
|
|
|
|
for (final item in data) {
|
|
final sweepAngle = availableAngle * (item.value / total);
|
|
|
|
if (sweepAngle <= 0) continue;
|
|
|
|
final paint = Paint()
|
|
..style = PaintingStyle.stroke
|
|
..strokeWidth = strokeWidth
|
|
..strokeCap = StrokeCap.round
|
|
..color = item.color;
|
|
|
|
canvas.drawArc(
|
|
Rect.fromCircle(center: center, radius: radius),
|
|
startAngle,
|
|
sweepAngle,
|
|
false,
|
|
paint,
|
|
);
|
|
|
|
startAngle += sweepAngle + gapAngle;
|
|
}
|
|
}
|
|
|
|
@override
|
|
bool shouldRepaint(DonutChartPainter oldDelegate) {
|
|
return oldDelegate.progress != progress ||
|
|
oldDelegate.oldData != oldData ||
|
|
oldDelegate.newData != newData;
|
|
}
|
|
}
|