5d0d2c0007
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>
208 lines
5.4 KiB
Dart
208 lines
5.4 KiB
Dart
// 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;
|
||
}
|