Files
MWClash/lib/widgets/donut_chart.dart
chen08209 ef5f6dbd59 Add rule override
Update core

Optimize more details
2025-04-08 15:35:14 +08:00

186 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;
}
}