Flutter自定义饼图UI布局实现:添加环绕费用图标及连线方法
在Flutter中实现饼图外围带图标及连接线的UI

我拥有一组费用模型数据,需要将对应的图标放置在饼图周围,并添加从饼图指向该费用图标的直线。如何在Flutter中实现这类UI?
实现方案
可以结合第三方图表库+自定义绘制的方式快速实现,下面是具体步骤和代码示例:
1. 定义数据模型
先封装费用数据,包含类别、占比、图标、颜色等信息:
class ExpenseModel { final String category; final double percentage; final IconData icon; final Color color; ExpenseModel({ required this.category, required this.percentage, required this.icon, required this.color, }); }
2. 用第三方库绘制基础饼图
这里选用charts_flutter库(需先在pubspec.yaml中添加依赖),绘制带厚度的饼图,留出空间给连接线:
import 'package:charts_flutter/flutter.dart' as charts; Widget buildBasePieChart(List<ExpenseModel> expenses) { final series = [ charts.Series<ExpenseModel, String>( id: 'Expenses', domainFn: (expense, _) => expense.category, measureFn: (expense, _) => expense.percentage, colorFn: (expense, _) => charts.ColorUtil.fromDartColor(expense.color), data: expenses, ) ]; return charts.PieChart( series, animate: true, defaultRenderer: charts.ArcRendererConfig( arcWidth: 60, // 调整饼图厚度 ), ); }
3. 叠加自定义连接线和图标
通过Stack组件将饼图、自定义绘制的连接线、图标叠加,用三角函数计算每个图标和连接线的位置:
import 'dart:math' as math; class PieChartWithIcons extends StatelessWidget { final List<ExpenseModel> expenses; final double chartSize; const PieChartWithIcons({ super.key, required this.expenses, this.chartSize = 250, }); @override Widget build(BuildContext context) { return Stack( alignment: Alignment.center, children: [ SizedBox(width: chartSize, height: chartSize, child: buildBasePieChart(expenses)), // 绘制连接线 CustomPaint( size: Size(chartSize + 80, chartSize + 80), painter: _LinePainter(expenses: expenses, chartRadius: chartSize / 2), ), // 放置外围图标 ..._buildIconWidgets(expenses), ], ); } List<Widget> _buildIconWidgets(List<ExpenseModel> expenses) { final icons = <Widget>[]; final totalPercentage = expenses.fold(0.0, (sum, item) => sum + item.percentage); var currentAngle = -90.0; // 饼图默认起始角度为顶部(-90度) for (final expense in expenses) { final sectorAngle = (expense.percentage / totalPercentage) * 360; final midAngle = currentAngle + sectorAngle / 2; final iconDistance = chartSize / 2 + 60; // 图标到饼图中心的距离 // 计算图标坐标 final x = iconDistance * math.cos(midAngle * math.pi / 180); final y = iconDistance * math.sin(midAngle * math.pi / 180); icons.add( Positioned( left: chartSize / 2 + x - 20, // 20是图标尺寸的一半,居中用 top: chartSize / 2 + y - 20, child: Icon(expense.icon, color: expense.color, size: 40), ), ); currentAngle += sectorAngle; } return icons; } } // 自定义绘制连接线的Painter class _LinePainter extends CustomPainter { final List<ExpenseModel> expenses; final double chartRadius; _LinePainter({required this.expenses, required this.chartRadius}); @override void paint(Canvas canvas, Size size) { final linePaint = Paint() ..color = Colors.grey.shade400 ..strokeWidth = 2 ..style = PaintingStyle.stroke; final totalPercentage = expenses.fold(0.0, (sum, item) => sum + item.percentage); var currentAngle = -90.0; final iconDistance = chartRadius + 60; for (final expense in expenses) { final sectorAngle = (expense.percentage / totalPercentage) * 360; final midAngle = currentAngle + sectorAngle / 2; // 饼图边缘的起点坐标 final startX = chartRadius * math.cos(midAngle * math.pi / 180); final startY = chartRadius * math.sin(midAngle * math.pi / 180); // 图标位置的终点坐标 final endX = iconDistance * math.cos(midAngle * math.pi / 180); final endY = iconDistance * math.sin(midAngle * math.pi / 180); canvas.drawLine( Offset(size.width / 2 + startX, size.height / 2 + startY), Offset(size.width / 2 + endX, size.height / 2 + endY), linePaint, ); currentAngle += sectorAngle; } } @override bool shouldRepaint(covariant CustomPainter oldDelegate) => true; }
4. 页面中使用组件
final sampleExpenses = [ ExpenseModel( category: '餐饮', percentage: 30, icon: Icons.restaurant, color: Colors.redAccent, ), ExpenseModel( category: '交通', percentage: 20, icon: Icons.train, color: Colors.blueAccent, ), ExpenseModel( category: '购物', percentage: 25, icon: Icons.shopping_bag, color: Colors.greenAccent, ), ExpenseModel( category: '娱乐', percentage: 25, icon: Icons.movie, color: Colors.yellowAccent, ), ]; @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: const Text('费用统计')), body: Center(child: PieChartWithIcons(expenses: sampleExpenses)), ); }
优化提示
- 可以调整
chartSize、arcWidth、图标尺寸、连接线长度等参数适配你的UI; - 若需要交互(比如点击图标高亮对应扇区),可以给图标添加
GestureDetector,结合饼图的交互事件实现; - 不想依赖第三方库的话,也可以完全用
CustomPaint绘制饼图、连接线和图标,只是开发成本会更高。
内容的提问来源于stack exchange,提问作者Tanvir Ibn Mizan
相关产品推荐
相关产品推荐

