图标样式

This commit is contained in:
mmc
2026-03-04 11:21:12 +08:00
parent b6ce5c4986
commit 0309011742

View File

@@ -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<RunningStatusPage> {
bool _isCardView = true;
bool _chipGaugeMode = true;
// 图表历史数据
final List<FlSpot> _leftMeasureHistory = [];
final List<FlSpot> _rightMeasureHistory = [];
final List<FlSpot> _leftTargetHistory = [];
@@ -40,26 +40,29 @@ class _RunningStatusPageState extends State<RunningStatusPage> {
double _timeIndex = 0;
// 限制列表长度,防止数据过多
// 兼容低版本的 takeLast 方法
List<T> _takeLast<T>(List<T> list, int count) {
if (list.length <= count) {
return List.from(list);
}
return list.sublist(list.length - count);
}
void _limit(List<FlSpot> list, {int max = 30}) {
if (list.length > max) list.removeAt(0);
}
// 刷新数据方法(移到正确位置)
void _refreshDeviceData() {
final deviceState = context.read<DevicesCubit>().state;
final currentDevice = deviceState.selectedDevice;
if (currentDevice != null) {
// 触发Bloc刷新数据
// context.read<DeviceStatusBloc>().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<RunningStatusPage> {
}
}
// 追加图表数据
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<RunningStatusPage> {
_limit(_voltageHistory);
}
// 构建间距组件
Widget _gap({double height = 20}) {
return SizedBox(height: height);
}
// 构建折线图
Widget _buildLineChart({required String title, required List<LineChartBarData> 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<double> 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<LineChartBarData> lines,
required double yAxisMax,
double yAxisMin = 0,
List<String> 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<RunningStatusPage> {
);
}
// 构建折线
LineChartBarData _line(List<FlSpot> 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<FlSpot> 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<RunningStatusPage> {
);
}
// 其他参数表格
Widget _buildOtherParamsTable(DeviceStatusUpdated state) {
final status = state.status;
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
@@ -370,7 +483,6 @@ class _RunningStatusPageState extends State<RunningStatusPage> {
);
}
// 表格单元格
Widget _buildTableCell(String text, {bool isHeader = false}) {
return TableCell(
child: Padding(
@@ -388,46 +500,16 @@ class _RunningStatusPageState extends State<RunningStatusPage> {
);
}
// 卡片视图
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<RunningStatusPage> {
),
),
// 顶部状态信息
BlocBuilder<DeviceStatusBloc, DeviceStatusState>(
builder: (context, state) {
String qual = '--';
@@ -522,7 +603,6 @@ class _RunningStatusPageState extends State<RunningStatusPage> {
},
),
// 切换选项卡
SliverToBoxAdapter(
child: Container(
color: Colors.white,
@@ -559,15 +639,12 @@ class _RunningStatusPageState extends State<RunningStatusPage> {
),
),
// 主要内容区域(修复Sliver嵌套问题)
SliverToBoxAdapter(
child: BlocBuilder<DeviceStatusBloc, DeviceStatusState>(
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<RunningStatusPage> {
);
}
}
// ====================== 半圆仪表盘绘制器 ======================
class SemiCircleGaugePainter extends CustomPainter {
final double value;
final double maxValue;
final List<double> 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;
}