From 0309011742fcf68bb20dcad968a6718ed4f6fd89 Mon Sep 17 00:00:00 2001 From: mmc <1556375442@qq.com> Date: Wed, 4 Mar 2026 11:21:12 +0800 Subject: [PATCH] =?UTF-8?q?=E5=9B=BE=E6=A0=87=E6=A0=B7=E5=BC=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../pages/running_status_page.dart | 654 ++++++++++++------ 1 file changed, 455 insertions(+), 199 deletions(-) diff --git a/lib/features/home/presentation/pages/running_status_page.dart b/lib/features/home/presentation/pages/running_status_page.dart index baadaa01..6f0aac4f 100644 --- a/lib/features/home/presentation/pages/running_status_page.dart +++ b/lib/features/home/presentation/pages/running_status_page.dart @@ -1,5 +1,6 @@ import 'dart:async'; import 'dart:convert'; +import 'dart:math' as math; import 'package:fl_chart/fl_chart.dart'; import 'package:flutter/material.dart'; @@ -25,7 +26,6 @@ class _RunningStatusPageState extends State { bool _isCardView = true; bool _chipGaugeMode = true; - // 图表历史数据 final List _leftMeasureHistory = []; final List _rightMeasureHistory = []; final List _leftTargetHistory = []; @@ -40,26 +40,29 @@ class _RunningStatusPageState extends State { double _timeIndex = 0; - // 限制列表长度,防止数据过多 + // 兼容低版本的 takeLast 方法 + List _takeLast(List list, int count) { + if (list.length <= count) { + return List.from(list); + } + return list.sublist(list.length - count); + } + void _limit(List list, {int max = 30}) { if (list.length > max) list.removeAt(0); } - // 刷新数据方法(移到正确位置) void _refreshDeviceData() { final deviceState = context.read().state; final currentDevice = deviceState.selectedDevice; if (currentDevice != null) { - // 触发Bloc刷新数据 - // context.read().add(RefreshDeviceStatus(deviceId: currentDevice.id)); ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text("数据刷新成功!"), duration: Duration(seconds: 1))); } else { ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text("暂无设备,无法刷新"), duration: Duration(seconds: 1))); } } - // 解析定位质量数值为文字描述 String _parseLocationQuality(int locationValue) { switch (locationValue) { case 0: @@ -77,34 +80,23 @@ class _RunningStatusPageState extends State { } } - // 追加图表数据 void _appendChartData(DeviceStatusUpdated state) { final status = state.status; - // 添加速度数据 _leftTargetHistory.add(FlSpot(_timeIndex, status.leftTargetSpeed)); _rightTargetHistory.add(FlSpot(_timeIndex, status.rightTargetSpeed)); _leftMeasureHistory.add(FlSpot(_timeIndex, status.leftMeasureSpeed)); _rightMeasureHistory.add(FlSpot(_timeIndex, status.rightMeasureSpeed)); - - // 添加电流数据 _leftCurrentHistory.add(FlSpot(_timeIndex, status.leftCurrent)); _rightCurrentHistory.add(FlSpot(_timeIndex, status.rightCurrent)); - - // 添加温度数据 _leftTempHistory.add(FlSpot(_timeIndex, status.leftMotorTemp)); _rightTempHistory.add(FlSpot(_timeIndex, status.rightMotorTemp)); - - // 添加割刀速度 _knifeHistory.add(FlSpot(_timeIndex, double.tryParse(status.knifeCuttingSpeed) ?? 0)); - - // 添加芯片温度和电压 _chipTempHistory.add(FlSpot(_timeIndex, status.chipTemp)); _voltageHistory.add(FlSpot(_timeIndex, double.tryParse(status.battery) ?? 0)); _timeIndex += 1; - // 限制数据长度 _limit(_leftTargetHistory); _limit(_rightTargetHistory); _limit(_leftMeasureHistory); @@ -118,33 +110,142 @@ class _RunningStatusPageState extends State { _limit(_voltageHistory); } - // 构建间距组件 Widget _gap({double height = 20}) { return SizedBox(height: height); } - // 构建折线图 - Widget _buildLineChart({required String title, required List lines, double? minY, double? maxY}) { + // ====================== 图表卡片(统一样式) ====================== + Widget _chartCard({required String title, required Widget child}) { + return Container( + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(12), + boxShadow: [BoxShadow(color: Colors.grey.withOpacity(0.1), blurRadius: 4, offset: const Offset(0, 2))], + ), + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + title, + style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold, color: Color(0xFF1677FF)), + ), + const SizedBox(height: 16), + child, + ], + ), + ); + } + + // ====================== 半圆仪表盘(电压/芯片温度) ====================== + Widget _semiCircleGauge({ + required double value, + required double maxValue, + required String unit, + required String label, + List majorTicks = const [0, 25, 50, 75, 100, 125, 150, 175, 200, 225, 250], + }) { + return SizedBox( + height: 200, + child: Stack( + alignment: Alignment.center, + children: [ + CustomPaint( + painter: SemiCircleGaugePainter(value: value / maxValue, maxValue: maxValue, majorTicks: majorTicks), + size: const Size(280, 140), + ), + Positioned( + bottom: 10, + child: Text( + "${value.toStringAsFixed(2)} $unit", + style: const TextStyle(fontSize: 24, fontWeight: FontWeight.bold, color: Colors.grey), + ), + ), + ], + ), + ); + } + + // ====================== 罗盘式航向角仪表盘 ====================== + Widget _compassGauge({required double yaw}) { + return SizedBox( + height: 250, + child: Stack( + alignment: Alignment.center, + children: [ + CustomPaint( + painter: CompassGaugePainter(yaw: yaw), + size: const Size(280, 280), + ), + Text( + "${yaw.toStringAsFixed(0)}°", + style: const TextStyle(fontSize: 24, fontWeight: FontWeight.bold, color: Colors.black), + ), + ], + ), + ); + } + + // ====================== 折线图(统一样式) ====================== + Widget _styledLineChart({ + required String title, + required List lines, + required double yAxisMax, + double yAxisMin = 0, + List bottomLabels = const ["t", "t+1", "t+2", "t+3", "t+4", "t+5"], + }) { return Column( - crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text(title, style: const TextStyle(fontSize: 16, fontWeight: FontWeight.bold)), - const SizedBox(height: 12), + if (lines.length > 1) + Padding( + padding: const EdgeInsets.only(bottom: 8), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + _legendItem(color: const Color(0xFF3F51B5), label: "左轮"), + const SizedBox(width: 24), + _legendItem(color: const Color(0xFF8BC34A), label: "右轮"), + ], + ), + ), SizedBox( - height: 220, + height: 200, child: LineChart( LineChartData( - minY: minY, - maxY: maxY, - lineBarsData: lines, - gridData: const FlGridData(show: true), - borderData:FlBorderData(show: true), - titlesData: const FlTitlesData( - leftTitles: AxisTitles(sideTitles: SideTitles(showTitles: true)), - bottomTitles: AxisTitles(sideTitles: SideTitles(showTitles: true)), - rightTitles: AxisTitles(sideTitles: SideTitles(showTitles: false)), - topTitles: AxisTitles(sideTitles: SideTitles(showTitles: false)), + minY: yAxisMin, + maxY: yAxisMax, + gridData: FlGridData( + show: true, + horizontalInterval: (yAxisMax - yAxisMin) / 6, + getDrawingHorizontalLine: (value) => const FlLine(color: Color(0xFFE5E5E5), strokeWidth: 1), + drawVerticalLine: false, ), + titlesData: FlTitlesData( + leftTitles: AxisTitles( + sideTitles: SideTitles( + showTitles: true, + interval: (yAxisMax - yAxisMin) / 6, + getTitlesWidget: (value, meta) => Text(value.toStringAsFixed(0), style: const TextStyle(fontSize: 12, color: Colors.grey)), + ), + ), + bottomTitles: AxisTitles( + sideTitles: SideTitles( + showTitles: true, + interval: 1, + getTitlesWidget: (value, meta) { + int idx = value.toInt(); + if (idx >= 0 && idx < bottomLabels.length) { + return Text(bottomLabels[idx], style: const TextStyle(fontSize: 12, color: Colors.grey)); + } + return const Text(""); + }, + ), + ), + rightTitles: const AxisTitles(sideTitles: SideTitles(showTitles: false)), + topTitles: const AxisTitles(sideTitles: SideTitles(showTitles: false)), + ), + borderData: FlBorderData(show: false), + lineBarsData: lines, ), ), ), @@ -152,142 +253,156 @@ class _RunningStatusPageState extends State { ); } - // 构建折线 - LineChartBarData _line(List data, Color color) { - return LineChartBarData( - spots: data, - color: color, - isCurved: true, - barWidth: 2, - dotData: const FlDotData(show: false), - belowBarData: BarAreaData(show: false), - ); - } - - // 左右轮测量速度图表 - Widget _buildSpeedMeasureChart() { - return _buildLineChart( - title: "左右轮测量速度 (rpm)", - minY: -3000, - maxY: 3000, - lines: [_line(_leftMeasureHistory, Colors.blue), _line(_rightMeasureHistory, Colors.red)], - ); - } - - // 左右轮目标速度图表 - Widget _buildSpeedTargetChart() { - return _buildLineChart( - title: "左右轮目标速度 (rpm)", - minY: -3000, - maxY: 3000, - lines: [_line(_leftTargetHistory, Colors.green), _line(_rightTargetHistory, Colors.orange)], - ); - } - - // 电流图表 - Widget _buildCurrentChart() { - return _buildLineChart( - title: "左右轮电流 (A)", - minY: 0, - maxY: 100, - lines: [_line(_leftCurrentHistory, Colors.purple), _line(_rightCurrentHistory, Colors.teal)], - ); - } - - // 电机温度图表 - Widget _buildMotorTempChart() { - return _buildLineChart( - title: "左右轮电机温度 (°C)", - minY: 0, - maxY: 120, - lines: [_line(_leftTempHistory, Colors.deepOrange), _line(_rightTempHistory, Colors.brown)], - ); - } - - // 割刀速度图表 - Widget _buildKnifeSpeedChart() { - return _buildLineChart(title: "割刀速度 (rpm)", minY: 0, maxY: 3000, lines: [_line(_knifeHistory, Colors.indigo)]); - } - - // 芯片温度/电压面板 - Widget _buildChipTempVoltagePanel(DeviceStatusUpdated state) { - return Column( - crossAxisAlignment: CrossAxisAlignment.start, + Widget _legendItem({required Color color, required String label}) { + return Row( children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - const Text("芯片温度 / 电压", style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold)), - Switch(value: _chipGaugeMode, onChanged: (v) => setState(() => _chipGaugeMode = v)), - ], + Container( + width: 16, + height: 16, + decoration: BoxDecoration( + shape: BoxShape.circle, + color: Colors.white, + border: Border.all(color: color, width: 2), + ), ), - const SizedBox(height: 16), - _chipGaugeMode - ? Row( - mainAxisAlignment: MainAxisAlignment.spaceEvenly, - children: [_buildGauge(state.status.chipTemp, 0, 120, "芯片温度 (°C)"), _buildGauge(double.parse(state.status.battery), 0, 100, "电量 (%)")], - ) - : _buildLineChart(title: "最近30次数据", lines: [_line(_chipTempHistory, Colors.red), _line(_voltageHistory, Colors.blue)]), + const SizedBox(width: 4), + Text(label, style: const TextStyle(fontSize: 14, color: Colors.black87)), ], ); } - // 航向角仪表盘 - Widget _buildHeadingGauge(DeviceStatusUpdated state) { - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const Text("航向角", style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold)), - const SizedBox(height: 16), - Center(child: _buildGauge(state.status.yaw, 0, 360, "航向角 (°)")), - ], + LineChartBarData _lineData(List data, Color color) { + // 替换 takeLast 为自定义的 _takeLast 方法 + return LineChartBarData( + spots: _takeLast(data, 6), // 兼容低版本 Dart + isCurved: false, + color: color, + barWidth: 2, + isStrokeCapRound: true, + dotData: FlDotData(show: true, getDotPainter: (spot, percent, barData, index) => FlDotCirclePainter(radius: 4, color: Colors.white, strokeWidth: 2)), + belowBarData: BarAreaData(show: false), ); } - // 构建仪表盘 - Widget _buildGauge(double value, double min, double max, String label) { - return SizedBox( - width: 150, - height: 150, - child: SfRadialGauge( - title: GaugeTitle(text: label), - axes: [ - RadialAxis( - minimum: min, - maximum: max, - pointers: [NeedlePointer(value: value)], - annotations: [ - GaugeAnnotation( - widget: Text(value.toStringAsFixed(1), style: const TextStyle(fontSize: 16, fontWeight: FontWeight.bold)), - angle: 90, - positionFactor: 0.5, - ), - ], + // ====================== 图表视图 ====================== + Widget _buildChartContentView(DeviceStatusState state) { + if (state is! DeviceStatusUpdated) { + return const Center(child: CircularProgressIndicator()); + } + + final status = state.status; + + return SingleChildScrollView( + padding: const EdgeInsets.all(16), + child: Column( + children: [ + // 电压仪表盘 + _chartCard( + title: "电压 (V)", + child: _semiCircleGauge( + value: status.voltage, + maxValue: 250, + unit: "V", + label: "电压", + majorTicks: const [0, 25, 50, 75, 100, 125, 150, 175, 200, 225, 250], + ), + ), + _gap(), + + // 航向角仪表盘 + _chartCard( + title: "航向角 (°)", + child: _compassGauge(yaw: status.yaw), + ), + _gap(), + + // 芯片温度仪表盘 + _chartCard( + title: "芯片温度 (°C)", + child: _semiCircleGauge( + value: status.chipTemp, + maxValue: 100, + unit: "°C", + label: "芯片温度", + majorTicks: const [0, 10, 20, 30, 40, 50, 60, 70, 80, 90, 100], + ), + ), + _gap(), + + // 割刀速度折线图 + _chartCard( + title: "割刀速度 (rpm)", + child: _styledLineChart(title: "割刀速度", lines: [_lineData(_knifeHistory, const Color(0xFF4CAF50))], yAxisMax: 3000, yAxisMin: -3000), + ), + _gap(), + + // 电流对比折线图 + _chartCard( + title: "电流对比 (A)", + child: _styledLineChart( + title: "电流对比", + lines: [_lineData(_leftCurrentHistory, const Color(0xFF3F51B5)), _lineData(_rightCurrentHistory, const Color(0xFF8BC34A))], + yAxisMax: 100, + ), + ), + _gap(), + + // 电机温度对比折线图 + _chartCard( + title: "电机温度对比 (°C)", + child: _styledLineChart( + title: "电机温度对比", + lines: [_lineData(_leftTempHistory, const Color(0xFF3F51B5)), _lineData(_rightTempHistory, const Color(0xFF8BC34A))], + yAxisMax: 100, + ), + ), + _gap(), + + // 右轮速度对比折线图 + _chartCard( + title: "右轮速度对比 (rpm)", + child: _styledLineChart( + title: "右轮速度对比", + lines: [_lineData(_rightTargetHistory, const Color(0xFF3F51B5)), _lineData(_rightMeasureHistory, const Color(0xFF8BC34A))], + yAxisMax: 3000, + yAxisMin: -3000, + ), + ), + _gap(), + + // 左轮速度对比折线图 + _chartCard( + title: "左轮速度对比 (rpm)", + child: _styledLineChart( + title: "左轮速度对比", + lines: [_lineData(_leftTargetHistory, const Color(0xFF3F51B5)), _lineData(_leftMeasureHistory, const Color(0xFF8BC34A))], + yAxisMax: 3000, + yAxisMin: -3000, + ), ), ], ), ); } - // 构建选项卡 - Widget _buildTab(String title, {required bool isActive}) { - return Text( - title, - style: TextStyle( - color: isActive ? const Color(0xFF1677FF) : const Color(0xFF666666), - fontSize: 16, - fontWeight: isActive ? FontWeight.bold : FontWeight.normal, - decoration: isActive ? TextDecoration.underline : null, - decorationColor: const Color(0xFF1677FF), - decorationThickness: 2, - ), - ); + // ====================== 卡片视图 ====================== + Widget _buildCardContentView(DeviceStatusState state) { + if (state is DeviceStatusUpdated) { + return SingleChildScrollView( + padding: const EdgeInsets.all(16), + child: Column(children: [_buildMotorTable(state), const SizedBox(height: 20), _buildOtherParamsTable(state)]), + ); + } else if (state is DeviceStatusError) { + return const Center( + child: Text("数据加载失败", style: TextStyle(color: Colors.red, fontSize: 16)), + ); + } else { + return const Center(child: CircularProgressIndicator()); + } } - // 电机参数表格 Widget _buildMotorTable(DeviceStatusUpdated state) { final status = state.status; - return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ @@ -337,10 +452,8 @@ class _RunningStatusPageState extends State { ); } - // 其他参数表格 Widget _buildOtherParamsTable(DeviceStatusUpdated state) { final status = state.status; - return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ @@ -370,7 +483,6 @@ class _RunningStatusPageState extends State { ); } - // 表格单元格 Widget _buildTableCell(String text, {bool isHeader = false}) { return TableCell( child: Padding( @@ -388,46 +500,16 @@ class _RunningStatusPageState extends State { ); } - // 卡片视图 - Widget _buildCardContentView(DeviceStatusState state) { - if (state is DeviceStatusUpdated) { - return SingleChildScrollView( - padding: const EdgeInsets.all(16), - child: Column(children: [_buildMotorTable(state), const SizedBox(height: 20), _buildOtherParamsTable(state)]), - ); - } else if (state is DeviceStatusError) { - return const Center( - child: Text("数据加载失败", style: TextStyle(color: Colors.red, fontSize: 16)), - ); - } else { - return const Center(child: CircularProgressIndicator()); - } - } - - // 图表视图 - Widget _buildChartContentView(DeviceStatusState state) { - if (state is! DeviceStatusUpdated) { - return const Center(child: CircularProgressIndicator()); - } - - return SingleChildScrollView( - padding: const EdgeInsets.all(16), - child: Column( - children: [ - _buildSpeedMeasureChart(), - _gap(), - _buildSpeedTargetChart(), - _gap(), - _buildCurrentChart(), - _gap(), - _buildMotorTempChart(), - _gap(), - _buildKnifeSpeedChart(), - _gap(), - _buildChipTempVoltagePanel(state), - _gap(), - _buildHeadingGauge(state), - ], + Widget _buildTab(String title, {required bool isActive}) { + return Text( + title, + style: TextStyle( + color: isActive ? const Color(0xFF1677FF) : const Color(0xFF666666), + fontSize: 16, + fontWeight: isActive ? FontWeight.bold : FontWeight.normal, + decoration: isActive ? TextDecoration.underline : null, + decorationColor: const Color(0xFF1677FF), + decorationThickness: 2, ), ); } @@ -461,7 +543,6 @@ class _RunningStatusPageState extends State { ), ), - // 顶部状态信息 BlocBuilder( builder: (context, state) { String qual = '--'; @@ -522,7 +603,6 @@ class _RunningStatusPageState extends State { }, ), - // 切换选项卡 SliverToBoxAdapter( child: Container( color: Colors.white, @@ -559,15 +639,12 @@ class _RunningStatusPageState extends State { ), ), - // 主要内容区域(修复Sliver嵌套问题) SliverToBoxAdapter( child: BlocBuilder( builder: (context, state) { - // 追加图表数据 if (state is DeviceStatusUpdated) { _appendChartData(state); } - return Container( margin: const EdgeInsets.all(16), decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(8)), @@ -581,3 +658,182 @@ class _RunningStatusPageState extends State { ); } } + +// ====================== 半圆仪表盘绘制器 ====================== +class SemiCircleGaugePainter extends CustomPainter { + final double value; + final double maxValue; + final List majorTicks; + + SemiCircleGaugePainter({required this.value, required this.maxValue, required this.majorTicks}); + + @override + void paint(Canvas canvas, Size size) { + final center = Offset(size.width / 2, size.height); + final radius = size.width / 2 - 20; + + // 背景弧 + final bgPaint = Paint() + ..color = const Color(0xFFE3F2FD) + ..style = PaintingStyle.stroke + ..strokeWidth = 20 + ..strokeCap = StrokeCap.round; + + canvas.drawArc(Rect.fromCircle(center: center, radius: radius), math.pi, math.pi, false, bgPaint); + + // 进度弧 + final progressPaint = Paint() + ..color = const Color(0xFFBBDEFB) + ..style = PaintingStyle.stroke + ..strokeWidth = 20 + ..strokeCap = StrokeCap.round; + + canvas.drawArc(Rect.fromCircle(center: center, radius: radius), math.pi, math.pi * value, false, progressPaint); + + // 刻度 + final tickPaint = Paint() + ..color = Colors.black54 + ..strokeWidth = 2; + + for (int i = 0; i <= 40; i++) { + final angle = math.pi + (math.pi / 40) * i; + final x1 = center.dx + (radius - 15) * math.cos(angle); + final y1 = center.dy + (radius - 15) * math.sin(angle); + final x2 = center.dx + radius * math.cos(angle); + final y2 = center.dy + radius * math.sin(angle); + canvas.drawLine(Offset(x1, y1), Offset(x2, y2), tickPaint); + + // 主刻度 + if (majorTicks.contains(i * (maxValue / 40))) { + final x3 = center.dx + (radius - 25) * math.cos(angle); + final y3 = center.dy + (radius - 25) * math.sin(angle); + canvas.drawLine(Offset(x1, y1), Offset(x3, y3), tickPaint..strokeWidth = 4); + + // 刻度值 + final textValue = (i * (maxValue / 40)).toStringAsFixed(0); + final textPainter = TextPainter( + text: TextSpan( + text: textValue, + style: const TextStyle(fontSize: 16, color: Colors.black54), + ), + textDirection: TextDirection.ltr, + )..layout(); + final textX = center.dx + (radius - 40) * math.cos(angle) - textPainter.width / 2; + final textY = center.dy + (radius - 40) * math.sin(angle) - textPainter.height / 2; + textPainter.paint(canvas, Offset(textX, textY)); + } + } + + // 指针 + final needleAngle = math.pi + math.pi * value; + final needlePaint = Paint() + ..color = const Color(0xFF90A4AE) + ..style = PaintingStyle.fill; + + final needlePath = Path() + ..moveTo(center.dx, center.dy) + ..lineTo(center.dx + radius * 0.8 * math.cos(needleAngle - 0.05), center.dy + radius * 0.8 * math.sin(needleAngle - 0.05)) + ..lineTo(center.dx + radius * 0.8 * math.cos(needleAngle + 0.05), center.dy + radius * 0.8 * math.sin(needleAngle + 0.05)) + ..close(); + + canvas.drawPath(needlePath, needlePaint); + } + + @override + bool shouldRepaint(covariant CustomPainter oldDelegate) => true; +} + +// ====================== 罗盘式航向角仪表盘绘制器 ====================== +class CompassGaugePainter extends CustomPainter { + final double yaw; + + CompassGaugePainter({required this.yaw}); + + @override + void paint(Canvas canvas, Size size) { + final center = Offset(size.width / 2, size.height / 2); + final radius = size.width / 2 - 20; + + // 外圈 + final outerPaint = Paint() + ..color = const Color(0xFF1677FF) + ..style = PaintingStyle.stroke + ..strokeWidth = 12; + + canvas.drawCircle(center, radius, outerPaint); + + // 刻度 + final tickPaint = Paint() + ..color = Colors.grey + ..strokeWidth = 2; + + for (int i = 0; i < 36; i++) { + final angle = (i * 10 - 90) * math.pi / 180; + final x1 = center.dx + (radius - 10) * math.cos(angle); + final y1 = center.dy + (radius - 10) * math.sin(angle); + final x2 = center.dx + radius * math.cos(angle); + final y2 = center.dy + radius * math.sin(angle); + canvas.drawLine(Offset(x1, y1), Offset(x2, y2), tickPaint); + + // 主刻度(45°间隔) + if (i % 9 == 0) { + final x3 = center.dx + (radius - 20) * math.cos(angle); + final y3 = center.dy + (radius - 20) * math.sin(angle); + canvas.drawLine(Offset(x1, y1), Offset(x3, y3), tickPaint..strokeWidth = 4); + + // 方向文字 + final direction = i == 0 + ? "N" + : i == 9 + ? "E" + : i == 18 + ? "S" + : i == 27 + ? "W" + : ""; + if (direction.isNotEmpty) { + final textPainter = TextPainter( + text: TextSpan( + text: direction, + style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold, color: Colors.black), + ), + textDirection: TextDirection.ltr, + )..layout(); + final textX = center.dx + (radius - 40) * math.cos(angle) - textPainter.width / 2; + final textY = center.dy + (radius - 40) * math.sin(angle) - textPainter.height / 2; + textPainter.paint(canvas, Offset(textX, textY)); + } + + // 角度值 + final angleValue = (i * 10).toString(); + final textPainter = TextPainter( + text: TextSpan( + text: angleValue, + style: const TextStyle(fontSize: 14, color: Colors.black54), + ), + textDirection: TextDirection.ltr, + )..layout(); + final textX = center.dx + (radius - 60) * math.cos(angle) - textPainter.width / 2; + final textY = center.dy + (radius - 60) * math.sin(angle) - textPainter.height / 2; + textPainter.paint(canvas, Offset(textX, textY)); + } + } + + // 指针 + final needleAngle = (yaw - 90) * math.pi / 180; + final needlePaint = Paint() + ..color = Colors.red + ..style = PaintingStyle.fill; + + final needlePath = Path() + ..moveTo(center.dx, center.dy) + ..lineTo(center.dx + radius * 0.7 * math.cos(needleAngle - 0.03), center.dy + radius * 0.7 * math.sin(needleAngle - 0.03)) + ..lineTo(center.dx + radius * 0.7 * math.cos(needleAngle + 0.03), center.dy + radius * 0.7 * math.sin(needleAngle + 0.03)) + ..close(); + + canvas.drawPath(needlePath, needlePaint); + } + + @override + bool shouldRepaint(covariant CustomPainter oldDelegate) => true; +}