- Implemented RawFrameDebugScreen for displaying ECU frame data and diagnostics. - Created DtcScreen to show active Diagnostic Trouble Codes (DTCs) with detailed descriptions. - Added DtcRow widget for individual DTC representation. - Introduced GraphScreen for visualizing sensor data over time with customizable time windows. - Developed SensorListScreen to display available sensors and their current values. - Enhanced SettingsScreen with options for Bluetooth connection, ECU protocol selection, polling interval, and theme settings. - Added AppColors and AppTheme for consistent theming across the application. - Implemented BtStatusChip to show Bluetooth connection status in the app bar. - Created RecordFab for starting and stopping data recording sessions.
264 lines
7.7 KiB
Dart
264 lines
7.7 KiB
Dart
import 'dart:math';
|
||
import 'package:flutter/material.dart';
|
||
|
||
class AnalogGauge extends StatefulWidget {
|
||
final double value;
|
||
final double minValue;
|
||
final double maxValue;
|
||
final String label;
|
||
final String unit;
|
||
final Color accentColor;
|
||
|
||
const AnalogGauge({
|
||
super.key,
|
||
required this.value,
|
||
required this.minValue,
|
||
required this.maxValue,
|
||
required this.label,
|
||
required this.unit,
|
||
required this.accentColor,
|
||
});
|
||
|
||
@override
|
||
State<AnalogGauge> createState() => _AnalogGaugeState();
|
||
}
|
||
|
||
class _AnalogGaugeState extends State<AnalogGauge>
|
||
with SingleTickerProviderStateMixin {
|
||
late AnimationController _ctrl;
|
||
late Animation<double> _anim;
|
||
|
||
double get _normalized =>
|
||
((widget.value - widget.minValue) /
|
||
(widget.maxValue - widget.minValue))
|
||
.clamp(0.0, 1.0);
|
||
|
||
@override
|
||
void initState() {
|
||
super.initState();
|
||
_ctrl = AnimationController(
|
||
vsync: this,
|
||
duration: const Duration(milliseconds: 150),
|
||
);
|
||
_anim = Tween<double>(begin: 0, end: _normalized)
|
||
.animate(CurvedAnimation(parent: _ctrl, curve: Curves.easeOut));
|
||
_ctrl.forward();
|
||
}
|
||
|
||
@override
|
||
void didUpdateWidget(AnalogGauge old) {
|
||
super.didUpdateWidget(old);
|
||
if (old.value != widget.value) {
|
||
_anim = Tween<double>(begin: _anim.value, end: _normalized)
|
||
.animate(CurvedAnimation(parent: _ctrl, curve: Curves.easeOut));
|
||
_ctrl.forward(from: 0);
|
||
}
|
||
}
|
||
|
||
@override
|
||
void dispose() {
|
||
_ctrl.dispose();
|
||
super.dispose();
|
||
}
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
return AspectRatio(
|
||
aspectRatio: 1,
|
||
child: AnimatedBuilder(
|
||
animation: _anim,
|
||
builder: (_, __) => CustomPaint(
|
||
painter: _GaugePainter(
|
||
normalizedValue: _anim.value,
|
||
accentColor: widget.accentColor,
|
||
displayValue: widget.value,
|
||
unit: widget.unit,
|
||
label: widget.label,
|
||
),
|
||
),
|
||
),
|
||
);
|
||
}
|
||
}
|
||
|
||
class _GaugePainter extends CustomPainter {
|
||
final double normalizedValue; // 0.0–1.0
|
||
final Color accentColor;
|
||
final double displayValue;
|
||
final String unit;
|
||
final String label;
|
||
|
||
static const double _startAngle = 135 * pi / 180; // 7:30 position
|
||
static const double _sweepAngle = 270 * pi / 180; // 270° sweep
|
||
|
||
_GaugePainter({
|
||
required this.normalizedValue,
|
||
required this.accentColor,
|
||
required this.displayValue,
|
||
required this.unit,
|
||
required this.label,
|
||
});
|
||
|
||
@override
|
||
void paint(Canvas canvas, Size size) {
|
||
final center = Offset(size.width / 2, size.height / 2);
|
||
final radius = size.width * 0.42;
|
||
final rect = Rect.fromCircle(center: center, radius: radius);
|
||
|
||
// ── Background arc ──────────────────────────────────────────────
|
||
final bgPaint = Paint()
|
||
..color = const Color(0xFF2A2A2A)
|
||
..style = PaintingStyle.stroke
|
||
..strokeWidth = size.width * 0.06
|
||
..strokeCap = StrokeCap.round;
|
||
canvas.drawArc(rect, _startAngle, _sweepAngle, false, bgPaint);
|
||
|
||
// ── Red zone (last 20%) ──────────────────────────────────────────
|
||
final redPaint = Paint()
|
||
..color = Colors.red.withAlpha(120)
|
||
..style = PaintingStyle.stroke
|
||
..strokeWidth = size.width * 0.06
|
||
..strokeCap = StrokeCap.round;
|
||
canvas.drawArc(
|
||
rect,
|
||
_startAngle + _sweepAngle * 0.8,
|
||
_sweepAngle * 0.2,
|
||
false,
|
||
redPaint,
|
||
);
|
||
|
||
// ── Value arc ────────────────────────────────────────────────────
|
||
if (normalizedValue > 0) {
|
||
final isRedZone = normalizedValue > 0.8;
|
||
final valPaint = Paint()
|
||
..color = isRedZone ? Colors.red : accentColor
|
||
..style = PaintingStyle.stroke
|
||
..strokeWidth = size.width * 0.06
|
||
..strokeCap = StrokeCap.round;
|
||
canvas.drawArc(
|
||
rect,
|
||
_startAngle,
|
||
_sweepAngle * normalizedValue,
|
||
false,
|
||
valPaint,
|
||
);
|
||
}
|
||
|
||
// ── Tick marks ───────────────────────────────────────────────────
|
||
_drawTicks(canvas, center, radius, size);
|
||
|
||
// ── Needle ────────────────────────────────────────────────────────
|
||
_drawNeedle(canvas, center, radius, size);
|
||
|
||
// ── Center text ──────────────────────────────────────────────────
|
||
_drawCenterText(canvas, center, size);
|
||
}
|
||
|
||
void _drawTicks(Canvas canvas, Offset center, double radius, Size size) {
|
||
final majorPaint = Paint()
|
||
..color = Colors.white54
|
||
..strokeWidth = size.width * 0.012
|
||
..strokeCap = StrokeCap.round;
|
||
final minorPaint = Paint()
|
||
..color = Colors.white24
|
||
..strokeWidth = size.width * 0.006
|
||
..strokeCap = StrokeCap.round;
|
||
|
||
const totalTicks = 50; // 2% each
|
||
for (int i = 0; i <= totalTicks; i++) {
|
||
final t = i / totalTicks;
|
||
final angle = _startAngle + _sweepAngle * t;
|
||
final isMajor = i % 5 == 0;
|
||
final paint = isMajor ? majorPaint : minorPaint;
|
||
final outerR = radius - size.width * 0.01;
|
||
final innerR = outerR - (isMajor ? size.width * 0.06 : size.width * 0.03);
|
||
canvas.drawLine(
|
||
center + Offset(cos(angle) * innerR, sin(angle) * innerR),
|
||
center + Offset(cos(angle) * outerR, sin(angle) * outerR),
|
||
paint,
|
||
);
|
||
}
|
||
}
|
||
|
||
void _drawNeedle(Canvas canvas, Offset center, double radius, Size size) {
|
||
final angle = _startAngle + _sweepAngle * normalizedValue;
|
||
final tipR = radius - size.width * 0.08;
|
||
final tailR = size.width * 0.08;
|
||
|
||
final needlePaint = Paint()
|
||
..color = Colors.white
|
||
..strokeWidth = size.width * 0.018
|
||
..strokeCap = StrokeCap.round;
|
||
|
||
canvas.drawLine(
|
||
center - Offset(cos(angle) * tailR, sin(angle) * tailR),
|
||
center + Offset(cos(angle) * tipR, sin(angle) * tipR),
|
||
needlePaint,
|
||
);
|
||
|
||
// Center dot
|
||
canvas.drawCircle(
|
||
center,
|
||
size.width * 0.04,
|
||
Paint()..color = Colors.white,
|
||
);
|
||
canvas.drawCircle(
|
||
center,
|
||
size.width * 0.025,
|
||
Paint()..color = const Color(0xFF0A0A0A),
|
||
);
|
||
}
|
||
|
||
void _drawCenterText(Canvas canvas, Offset center, Size size) {
|
||
// Value
|
||
final valSpan = TextSpan(
|
||
text: displayValue.toStringAsFixed(0),
|
||
style: TextStyle(
|
||
color: Colors.white,
|
||
fontSize: size.width * 0.14,
|
||
fontWeight: FontWeight.bold,
|
||
letterSpacing: -1,
|
||
),
|
||
);
|
||
final valPainter = TextPainter(
|
||
text: valSpan,
|
||
textDirection: TextDirection.ltr,
|
||
)..layout();
|
||
valPainter.paint(
|
||
canvas,
|
||
center +
|
||
Offset(
|
||
-valPainter.width / 2,
|
||
size.width * 0.1,
|
||
),
|
||
);
|
||
|
||
// Unit
|
||
final unitSpan = TextSpan(
|
||
text: unit,
|
||
style: TextStyle(
|
||
color: Colors.white54,
|
||
fontSize: size.width * 0.07,
|
||
),
|
||
);
|
||
final unitPainter = TextPainter(
|
||
text: unitSpan,
|
||
textDirection: TextDirection.ltr,
|
||
)..layout();
|
||
unitPainter.paint(
|
||
canvas,
|
||
center +
|
||
Offset(
|
||
-unitPainter.width / 2,
|
||
size.width * 0.1 + valPainter.height + 2,
|
||
),
|
||
);
|
||
}
|
||
|
||
@override
|
||
bool shouldRepaint(_GaugePainter old) =>
|
||
old.normalizedValue != normalizedValue ||
|
||
old.accentColor != accentColor ||
|
||
old.displayValue != displayValue;
|
||
}
|