Status bar: part lamps (CAN, OBD, IMU, CAM), drawn signal and battery icons
/ crosscheck (push) Successful in 1s
CI / Analyze & Test (push) Successful in 21s
CI / Debug APK (push) Successful in 40s

WiFi and LTE show signal in four steps with Android's traffic arrows; the battery is
filled to its level in good (charging), caution (under 40 %) or alarm (under 20 %).
OBD, IMU and CAM are lamps by CAN's rule; CAM reads orin-status' new cam_up and vision_up.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-15 23:01:40 +09:00
parent 31a844bb94
commit 5d0d2c0007
4 changed files with 385 additions and 114 deletions
+207
View File
@@ -0,0 +1,207 @@
// The status bar's drawn icons, Android-like: a battery whose fill shows level and
// colour, WiFi and cellular signal in four steps, and the traffic arrow pair. Drawn
// rather than taken from the icon font, which has no four-step cellular icon and no
// separately coloured battery fill.
import 'dart:math' as math;
import 'package:flutter/material.dart';
/// A horizontal battery: [outline] for the case, the inside filled to [level]
/// (0–1, null unknown) in [fill].
class BatteryIcon extends StatelessWidget {
const BatteryIcon({
super.key,
required this.level,
required this.fill,
required this.outline,
});
final double? level;
final Color fill;
final Color outline;
@override
Widget build(BuildContext context) => CustomPaint(
size: const Size(24, 12),
painter: _Battery(level, fill, outline),
);
}
class _Battery extends CustomPainter {
_Battery(this.level, this.fill, this.outline);
final double? level;
final Color fill;
final Color outline;
@override
void paint(Canvas c, Size s) {
const nub = 2.0;
final body = Rect.fromLTWH(0.75, 0.75, s.width - nub - 1.5, s.height - 1.5);
c.drawRRect(
RRect.fromRectAndRadius(body, const Radius.circular(2.5)),
Paint()
..style = PaintingStyle.stroke
..strokeWidth = 1.5
..color = outline,
);
c.drawRRect(
RRect.fromRectAndRadius(
Rect.fromLTWH(s.width - nub, s.height * 0.3, nub, s.height * 0.4),
const Radius.circular(1),
),
Paint()..color = outline,
);
final l = level;
if (l == null) return;
final inner = body.deflate(2);
c.drawRRect(
RRect.fromRectAndRadius(
Rect.fromLTWH(
inner.left,
inner.top,
inner.width * l.clamp(0, 1),
inner.height,
),
const Radius.circular(1),
),
Paint()..color = fill,
);
}
@override
bool shouldRepaint(_Battery o) =>
o.level != level || o.fill != fill || o.outline != outline;
}
/// Signal in [bars] of four (null: off, drawn crossed), WiFi's fan or cellular's
/// triangle; the unlit part faint.
class SignalIcon extends StatelessWidget {
const SignalIcon.wifi({super.key, required this.bars, required this.color})
: wifi = true;
const SignalIcon.cellular({
super.key,
required this.bars,
required this.color,
}) : wifi = false;
final int? bars;
final Color color;
final bool wifi;
@override
Widget build(BuildContext context) => CustomPaint(
size: const Size(18, 16),
painter: _Signal(bars, color, wifi),
);
}
class _Signal extends CustomPainter {
_Signal(this.bars, this.color, this.wifi);
final int? bars;
final Color color;
final bool wifi;
@override
void paint(Canvas c, Size s) {
final faint = Paint()..color = color.withValues(alpha: 0.3);
final lit = Paint()..color = color;
final n = bars ?? 0;
if (wifi) {
// A quarter-circle fan standing on its point; lit from the point outwards.
final apex = Offset(s.width / 2, s.height);
Path fan(double r) => Path()
..moveTo(apex.dx, apex.dy)
..arcTo(
Rect.fromCircle(center: apex, radius: r),
-3 * math.pi / 4,
math.pi / 2,
false,
)
..close();
final full = s.height;
c.drawPath(fan(full), faint);
if (n > 0) c.drawPath(fan(full * (0.25 + 0.75 * n / 4)), lit);
} else {
// A right triangle rising to the right; lit from the left.
final tri = Path()
..moveTo(0, s.height)
..lineTo(s.width, s.height)
..lineTo(s.width, 0)
..close();
c.drawPath(tri, faint);
if (n > 0) {
c.save();
c.clipRect(
Rect.fromLTWH(0, 0, s.width * (0.2 + 0.8 * n / 4), s.height),
);
c.drawPath(tri, lit);
c.restore();
}
}
if (bars == null) {
// Off: a stroke through it.
c.drawLine(
Offset(s.width * 0.15, s.height * 0.15),
Offset(s.width * 0.85, s.height * 0.95),
Paint()
..color = color
..strokeWidth = 1.5,
);
}
}
@override
bool shouldRepaint(_Signal o) =>
o.bars != bars || o.color != color || o.wifi != wifi;
}
/// Android's traffic pair beside a signal icon: up lit while sending, down while
/// receiving.
class TrafficArrows extends StatelessWidget {
const TrafficArrows({
super.key,
required this.up,
required this.down,
required this.color,
});
final bool up;
final bool down;
final Color color;
@override
Widget build(BuildContext context) =>
CustomPaint(size: const Size(6, 14), painter: _Arrows(up, down, color));
}
class _Arrows extends CustomPainter {
_Arrows(this.up, this.down, this.color);
final bool up;
final bool down;
final Color color;
@override
void paint(Canvas c, Size s) {
Paint p(bool on) =>
Paint()..color = on ? color : color.withValues(alpha: 0.25);
final h = s.height / 2 - 1;
c.drawPath(
Path()
..moveTo(0, h)
..lineTo(s.width, h)
..lineTo(s.width / 2, 0)
..close(),
p(up),
);
c.drawPath(
Path()
..moveTo(0, s.height - h)
..lineTo(s.width, s.height - h)
..lineTo(s.width / 2, s.height)
..close(),
p(down),
);
}
@override
bool shouldRepaint(_Arrows o) =>
o.up != up || o.down != down || o.color != color;
}