Flutter:如何在外围圆形与中心圆形间绘制连接线
Flutter 实现外围圆形到中心圆形的连接线方案
先修复你的LinePainterWidget,原组件存在几个问题:CustomPaint没有尺寸约束导致线条无法渲染、shouldRepaint一直返回true造成不必要重绘、直接使用全局坐标容易受布局影响。修改后的代码如下:
import 'package:flutter/material.dart'; class LinePainterWidget extends StatelessWidget { final Offset start; final Offset end; final Color color; final double strokeWidth; const LinePainterWidget({ super.key, required this.start, required this.end, this.color = Colors.white, this.strokeWidth = 2.0, }); @override Widget build(BuildContext context) { return CustomPaint( size: Size.infinite, // 让CustomPaint占满父容器,确保线条能渲染 painter: MyLinePainter( start: start, end: end, color: color, strokeWidth: strokeWidth, ), ); } } class MyLinePainter extends CustomPainter { final Offset start; final Offset end; final Color color; final double strokeWidth; MyLinePainter({ required this.start, required this.end, required this.color, required this.strokeWidth, }); @override void paint(Canvas canvas, Size size) { final paint = Paint() ..color = color ..strokeWidth = strokeWidth ..strokeCap = StrokeCap.round; // 线条两端圆润,视觉效果更好 canvas.drawLine(start, end, paint); } @override bool shouldRepaint(covariant MyLinePainter oldDelegate) { // 仅当参数变化时触发重绘,优化性能 return oldDelegate.start != start || oldDelegate.end != end || oldDelegate.color != color || oldDelegate.strokeWidth != strokeWidth; } }
接下来重构HomePage布局,原布局用Column和Row排列圆形,无法直接获取相对位置,改用Stack布局承载线条和圆形,线条放在底层,圆形在上层。通过GlobalKey获取每个圆形的位置,计算中心坐标后绘制连接线:
class HomePage extends StatefulWidget { @override _HomePageState createState() => _HomePageState(); } class _HomePageState extends State<HomePage> { // 定义每个圆形组件的GlobalKey,用于获取布局位置 final GlobalKey _centerCircleKey = GlobalKey(); final GlobalKey _topCircleKey = GlobalKey(); final GlobalKey _topLeftCircleKey = GlobalKey(); final GlobalKey _topRightCircleKey = GlobalKey(); final GlobalKey _bottomLeftCircleKey = GlobalKey(); final GlobalKey _bottomRightCircleKey = GlobalKey(); final GlobalKey _bottomCircleKey = GlobalKey(); Offset? _centerOffset; // 中心圆的局部中心坐标 final Map<GlobalKey, Offset?> _outerOffsets = {}; // 外围圆的坐标存储 @override void initState() { super.initState(); // 初始化外围圆坐标存储 _outerOffsets[_topCircleKey] = null; _outerOffsets[_topLeftCircleKey] = null; _outerOffsets[_topRightCircleKey] = null; _outerOffsets[_bottomLeftCircleKey] = null; _outerOffsets[_bottomRightCircleKey] = null; _outerOffsets[_bottomCircleKey] = null; // 布局完成后计算所有圆形的中心坐标 WidgetsBinding.instance.addPostFrameCallback((_) { _calculateCircleCenters(); }); } // 计算所有圆形的中心局部坐标 void _calculateCircleCenters() { setState(() { _centerOffset = _getCircleCenter(_centerCircleKey); for (var key in _outerOffsets.keys) { _outerOffsets[key] = _getCircleCenter(key); } }); } // 根据GlobalKey获取组件的局部中心坐标 Offset? _getCircleCenter(GlobalKey key) { final renderBox = key.currentContext?.findRenderObject() as RenderBox?; if (renderBox == null) return null; final size = renderBox.size; return Offset(size.width / 2, size.height / 2); } @override Widget build(BuildContext context) { return WillPopScope( onWillPop: () async => false, child: Scaffold( drawer: AppDrawer(), body: Container( color: const Color(0xFF191D3F), child: SafeArea( child: Column( children: [ const SizedBox(height: 10), const HomeTopBar(username: "User001"), Flexible( child: SingleChildScrollView( child: SizedBox( width: double.infinity, child: Stack( alignment: Alignment.center, children: [ // 绘制连接线:放在Stack底层,避免被圆形遮挡 if (_centerOffset != null) ..._outerOffsets.entries.map((entry) { final outerOffset = entry.value; if (outerOffset != null) { return LinePainterWidget( start: _centerOffset!, end: outerOffset, ); } return const SizedBox.shrink(); }), // 所有圆形组件:放在线条上层 Column( mainAxisAlignment: MainAxisAlignment.center, children: [ const SizedBox(height: 20), GestureDetector( onTapDown: (details) { final tapPosition = details.globalPosition; print("Top"); print("x :${tapPosition.dx}"); print("y :${tapPosition.dy}"); }, child: CircleIconWidget( key: _topCircleKey, width: 100, imagePath: 'Assets/solarIcon-Live.png', color: Colors.yellow, labelText: '100 W', ), ), Padding( padding: const EdgeInsets.symmetric(horizontal: 20), child: Row( mainAxisAlignment: MainAxisAlignment.center, children: [ GestureDetector( onTapDown: (details) { final tapPosition = details.globalPosition; print("Top Left"); print("x :${tapPosition.dx}"); print("y :${tapPosition.dy}"); }, onTap: () => Navigator.of(context) .push(MaterialPageRoute( builder: (context) => const EVChargerView( number: 1, ))), child: CircleIconWidget( key: _topLeftCircleKey, width: 100, imagePath: 'Assets/EVChargerIcon-Live.png', color: Colors.green, labelText: '100 W', ), ), const Spacer(), GestureDetector( onTapDown: (details) { final tapPosition = details.globalPosition; print("Top Right"); print("x :${tapPosition.dx}"); print("y :${tapPosition.dy}"); }, onTap: () => Navigator.of(context) .push(MaterialPageRoute( builder: (context) => const EVChargerView( number: 2, ))), child: CircleIconWidget( key: _topRightCircleKey, width: 100, imagePath: 'Assets/EVChargerIcon-Live.png', color: Colors.green, labelText: '100 W', ), ), ], ), ), GestureDetector( onTapDown: (details) { final tapPosition = details.globalPosition; print("middle"); print("x :${tapPosition.dx}"); print("y :${tapPosition.dy}"); }, child: CircleIconWidget( key: _centerCircleKey, width: 100, imagePath: 'Assets/homeIcon-Live.png', color: Colors.white, labelText: '100 W', ), ), Padding( padding: const EdgeInsets.symmetric(horizontal: 20), child: Row( mainAxisAlignment: MainAxisAlignment.center, children: [ GestureDetector( onTapDown: (details) { final tapPosition = details.globalPosition; print("Bottom Left"); print("x :${tapPosition.dx}"); print("y :${tapPosition.dy}"); }, child: CircleIconWidget( key: _bottomLeftCircleKey, width: 100, imagePath: 'Assets/gridIcon-Live.png', color: Colors.red, labelText: '100 W', ), ), const Spacer(), GestureDetector( onTapDown: (details) { final tapPosition = details.globalPosition; print("Bottom Right"); print("x :${tapPosition.dx}"); print("y :${tapPosition.dy}"); }, child: CircleIconWidget( key: _bottomRightCircleKey, width: 100, imagePath: 'Assets/batteryIcon-Live.png', color: Colors.blue, labelText: '100 W', ), ), ], ), ), GestureDetector( onTapDown: (details) { final tapPosition = details.globalPosition; print("Bottom Middle"); print("x :${tapPosition.dx}"); print("y :${tapPosition.dy}"); }, child: CircleIconWidget( key: _bottomCircleKey, width: 100, imagePath: 'Assets/diverterIcon-Live.png', color: Colors.pink, labelText: '100 W', ), ), const SizedBox(height: 20), ], ), ], ), ), ), ), ], ), ), ), ), ); } }
关键说明
- 用
Stack布局实现线条和圆形的层叠,线条在底层,确保不会被圆形组件遮挡 - 通过
GlobalKey和RenderBox获取组件的局部中心坐标,避免全局坐标受屏幕尺寸、布局 padding 等因素影响 - 在
addPostFrameCallback中计算坐标,确保组件已经完成布局,能正确获取位置信息 - 优化
LinePainterWidget的重绘逻辑,只在必要时触发重绘,提升页面性能
内容的提问来源于stack exchange,提问作者Sean
相关产品推荐
相关产品推荐

