import 'dart:math'; import 'package:flutter/material.dart'; import 'package:fl_chart/fl_chart.dart'; void main() { runApp(const MaterialApp( home: Scaffold( backgroundColor: Colors.white, body: Center(child: MySolidPieChart()), ), )); } class MySolidPieChart extends StatefulWidget { const MySolidPieChart({super.key}); @override State createState() => _MySolidPieChartState(); } class _MySolidPieChartState extends State { int? touchedIndex; final double baseRadius = 60.0; final List<_PieData> data = [ _PieData(label: "运行", value: 45, color: Colors.teal), _PieData(label: "移动", value: 14, color: Colors.blue), _PieData(label: "停留", value: 25, color: Colors.green), _PieData(label: "其他", value: 16, color: Colors.orange), ]; @override Widget build(BuildContext context) { return SizedBox( width: 320, height: 320, child: Stack( children: [ PieChart( PieChartData( sectionsSpace: 0, centerSpaceRadius: 0, // ✅ 实心饼图 startDegreeOffset: -90, pieTouchData: PieTouchData( touchCallback: (event, response) { setState(() { final index = response?.touchedSection?.touchedSectionIndex; touchedIndex = (index != null && index >= 0) ? index : null; }); }, ), sections: List.generate(data.length, (i) { final item = data[i]; final isTouched = i == touchedIndex; return PieChartSectionData( color: item.color, value: item.value.toDouble(), title: '${item.value}%', radius: isTouched ? baseRadius + 10 : baseRadius, titleStyle: const TextStyle( fontSize: 14, fontWeight: FontWeight.bold, color: Colors.white, ), titlePositionPercentageOffset: 0.6, ); }), ), ), IgnorePointer( child: CustomPaint( size: const Size(320, 320), painter: PieLineLabelPainter( center: const Offset(160, 160), baseRadius: baseRadius + 10, startAngleOffset: -90, data: data, touchedIndex: touchedIndex, ), ), ), ], ), ); } } class PieLineLabelPainter extends CustomPainter { final Offset center; final double baseRadius; final double startAngleOffset; final List<_PieData> data; final int? touchedIndex; PieLineLabelPainter({ required this.center, required this.baseRadius, required this.startAngleOffset, required this.data, required this.touchedIndex, }); @override void paint(Canvas canvas, Size size) { if (touchedIndex == null || touchedIndex! < 0 || touchedIndex! >= data.length) return; final paint = Paint() ..color = data[touchedIndex!].color ..strokeWidth = 1.5; final total = data.fold(0, (sum, e) => sum + e.value); double angle = startAngleOffset * pi / 180; for (int i = 0; i < data.length; i++) { final sweepAngle = (data[i].value / total) * 2 * pi; if (i == touchedIndex) { final midAngle = angle + sweepAngle / 2; final startR = baseRadius + 2; final bendR = startR + 25; final start = Offset( center.dx + cos(midAngle) * startR, center.dy + sin(midAngle) * startR, ); final bend = Offset( center.dx + cos(midAngle) * bendR, center.dy + sin(midAngle) * bendR, ); final isRight = cos(midAngle) >= 0; final end = Offset( bend.dx + (isRight ? 30 : -30), bend.dy, ); canvas.drawCircle(start, 3, paint); canvas.drawLine(start, bend, paint); canvas.drawLine(bend, end, paint); final textPainter = TextPainter( text: TextSpan( text: '${data[i].label} 11h30min', style: TextStyle(color: data[i].color, fontSize: 12), ), textDirection: TextDirection.ltr, )..layout(); textPainter.paint( canvas, Offset( isRight ? end.dx + 4 : end.dx - textPainter.width - 4, end.dy - 6, ), ); } angle += sweepAngle; } } @override bool shouldRepaint(covariant CustomPainter oldDelegate) => true; } class _PieData { final String label; final int value; final Color color; _PieData({required this.label, required this.value, required this.color}); }