App 2.36.0: Overview style Grid, after the Tron films

Neon lines on black over a perspective grid floor that moves with the
speed; the battery as a light wall, power on a disc whose inner ring
turns with it, and the last two minutes of power as a light-cycle trail
of right angles (the Cockpit's PowerTrace, now in ClusterValues). Cyan,
or the accent colour; in cyan the app turns cyan too. In THEMES.md with
two screenshots. Emulator only; not tried on the car.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LhzAVaUyqcw5PkGjz4mWW6
This commit is contained in:
2026-10-26 18:33:53 +01:00
co-authored by Claude Opus 5.5
parent 1e0fd28eb2
commit cba46504a6
14 changed files with 472 additions and 15 deletions
+2 -2
View File
@@ -75,11 +75,11 @@ Repository: `ssh://git@git.stinnesbeck.com:2222/nils/mg4.git` (remote `origin`),
| Tab | Contents | | Tab | Contents |
|---|---| |---|---|
| Overview | value tiles (battery, range, range/usage/energy since start, 5-min averages of range, usage, power, speed), vehicle info (odometer, lease allowance and the odometer against it), tyre pressures; read every 0.2 s on a background thread. Styles (Settings → Overview style): Tiles, Gauges (`GaugeView` needles), Cockpit (`RingView` rings, `TraceView` power trace), Retro (`RetroView`, seven-segment VFD), Futuristic (`FlowView`, animated energy flow), Sci-fi (`SciFiView`, starship console), Cartoon (`CartoonView`), MG (`OctagonView`, octagonal dials after the classic MG dashboards), and on `ClusterCanvasView`: Minimal, Terminal, Nixie tubes, Chronograph; the drawn styles share `ClusterValues`. A style shown in its own palette (Sci-fi, Cartoon, Nixie; Retro and Terminal with "Original") gives the whole app its colours (`StylePalette`, `res/values/style_palettes.xml`) instead of the accent colour. A button in the navigation's header hides the navigation for a full-width Overview (`PREF_OVERVIEW_FULLSCREEN`); a swipe in from the left edge (`EdgeSwipe`) brings it back; without the app's own full screen the car's sidebar takes that edge, so a tab at the left edge (`edge_tab`) is tapped or swiped instead. Pages change only by the navigation (the pager's own swiping is off) | | Overview | value tiles (battery, range, range/usage/energy since start, 5-min averages of range, usage, power, speed), vehicle info (odometer, lease allowance and the odometer against it), tyre pressures; read every 0.2 s on a background thread. Styles (Settings → Overview style): Tiles, Gauges (`GaugeView` needles), Cockpit (`RingView` rings, `TraceView` power trace), Retro (`RetroView`, seven-segment VFD), Futuristic (`FlowView`, animated energy flow), Sci-fi (`SciFiView`, starship console), Cartoon (`CartoonView`), MG (`OctagonView`, octagonal dials after the classic MG dashboards), and on `ClusterCanvasView`: Minimal, Terminal, Nixie tubes, Chronograph, Grid (`GridView`, neon lines over a moving grid floor, power as a light-cycle trail from `ClusterValues.powerTrail`); the drawn styles share `ClusterValues`. A style shown in its own palette (Sci-fi, Cartoon, Nixie; Retro, Terminal and Grid with "Original") gives the whole app its colours (`StylePalette`, `res/values/style_palettes.xml`) instead of the accent colour. A button in the navigation's header hides the navigation for a full-width Overview (`PREF_OVERVIEW_FULLSCREEN`); a swipe in from the left edge (`EdgeSwipe`) brings it back; without the app's own full screen the car's sidebar takes that edge, so a tab at the left edge (`edge_tab`) is tapped or swiped instead. Pages change only by the navigation (the pager's own swiping is off) |
| Consumption / Charging | period totals, daily log, charging sessions and curve | | Consumption / Charging | period totals, daily log, charging sessions and curve |
| Live data | on/off, ABRP API key and user token, upload frequency | | Live data | on/off, ABRP API key and user token, upload frequency |
| Server | URL, pairing by QR, ingest token, installation ID (read-only), config import | | Server | URL, pairing by QR, ingest token, installation ID (read-only), config import |
| Settings | drive mode (Eco, Normal, Sport, Snow, Custom; the lit one is what the car reports; changes only in P at a standstill, `DriveModeWrite`; "Set at start" sets the last confirmed choice again when the service starts with the head unit), full screen, voice key, Overview style with the accent colour beside it (offered only for styles drawn in it, `OverviewStyle.usesAccent`; Retro and Terminal add "Original" for their own palette, `hasOriginal`), tyre unit, battery and charging price (one card on wide screens), version, update check, install from USB, About | | Settings | drive mode (Eco, Normal, Sport, Snow, Custom; the lit one is what the car reports; changes only in P at a standstill, `DriveModeWrite`; "Set at start" sets the last confirmed choice again when the service starts with the head unit), full screen, voice key, Overview style with the accent colour beside it (offered only for styles drawn in it, `OverviewStyle.usesAccent`; Retro, Terminal and Grid add "Original" for their own palette, `hasOriginal`), tyre unit, battery and charging price (one card on wide screens), version, update check, install from USB, About |
| Logs | signal recorder (`SignalRecorder`/`SignalRecording`: every property and vendor getter via `VendorSurfaceProbe.capture`, again and again for 3 minutes, plus steering-wheel key broadcasts; lists what changed, rarest first; saved to the app and a USB stick; how to find where the car publishes something), update log (every install step, with the platform's words), upload log | | Logs | signal recorder (`SignalRecorder`/`SignalRecording`: every property and vendor getter via `VendorSurfaceProbe.capture`, again and again for 3 minutes, plus steering-wheel key broadcasts; lists what changed, rarest first; saved to the app and a USB stick; how to find where the car publishes something), update log (every install step, with the platform's words), upload log |
Key classes (`app/src/main/java/com/evsuite/abrp/`): `AbrpUploadService` (foreground service, Key classes (`app/src/main/java/com/evsuite/abrp/`): `AbrpUploadService` (foreground service,
+3 -3
View File
@@ -39,8 +39,8 @@ APIs.
## Screenshots ## Screenshots
**Overview styles:** the Overview comes in twelve looks, from an 80s fluorescent display to a **Overview styles:** the Overview comes in thirteen looks, from an 80s fluorescent display to a
starship console and a watch face. [THEMES.md](THEMES.md) shows each one, with and without the starship console, a watch face and a neon grid. [THEMES.md](THEMES.md) shows each one, with and without the
navigation, and what inspired it. navigation, and what inspired it.
<p align="center"> <p align="center">
@@ -88,7 +88,7 @@ with the server using a token of its own, obtained by pairing with a phone (belo
| Tab | What it shows | | Tab | What it shows |
|---|---| |---|---|
| **Overview** | Battery, range, power and speed; range, usage and energy since start; 5-minute averages of range, usage, power and speed; vehicle info (odometer, lease allowance, climate setpoint) and tyre pressures. Read five times a second. Looks to choose from in Settings → Overview style: Tiles, Gauges, Cockpit, Retro, Futuristic, Sci-fi, Cartoon, MG, Minimal, Terminal, Nixie tubes and Chronograph ([THEMES.md](THEMES.md) shows them all). A style with its own colours (Sci-fi, Cartoon, Nixie tubes, and Retro and Terminal with "Original") colours the whole app to match, e.g. turquoise for Retro. The small button next to the MG4 logo in the navigation hides the navigation for a full-width Overview; swipe in from the left edge (a faint handle marks it) to bring it back; with Settings → Full screen off, the car's own sidebar sits at that edge, so tap the small tab at the left edge (›) instead. Pages change only by the navigation, not by swiping sideways | | **Overview** | Battery, range, power and speed; range, usage and energy since start; 5-minute averages of range, usage, power and speed; vehicle info (odometer, lease allowance, climate setpoint) and tyre pressures. Read five times a second. Looks to choose from in Settings → Overview style: Tiles, Gauges, Cockpit, Retro, Futuristic, Sci-fi, Cartoon, MG, Minimal, Terminal, Nixie tubes, Chronograph and Grid ([THEMES.md](THEMES.md) shows them all). A style with its own colours (Sci-fi, Cartoon, Nixie tubes, and Retro, Terminal and Grid with "Original") colours the whole app to match, e.g. turquoise for Retro. The small button next to the MG4 logo in the navigation hides the navigation for a full-width Overview; swipe in from the left edge (a faint handle marks it) to bring it back; with Settings → Full screen off, the car's own sidebar sits at that edge, so tap the small tab at the left edge (›) instead. Pages change only by the navigation, not by swiping sideways |
| **Consumption** | Lifetime, last 7 and 30 days, Trip A and B (distance, energy, average consumption and speed, SOC change), and a daily log of the last four months | | **Consumption** | Lifetime, last 7 and 30 days, Trip A and B (distance, energy, average consumption and speed, SOC change), and a daily log of the last four months |
| **Charging** | The current session (SOC, voltage, current, power, energy into the battery and from the grid, time, cost) and its SOC/power curve | | **Charging** | The current session (SOC, voltage, current, power, energy into the battery and from the grid, time, cost) and its SOC/power curve |
| **Live data** | Live data on/off, ABRP credentials for sending directly from the car, upload frequency | | **Live data** | Live data on/off, ABRP credentials for sending directly from the car, upload frequency |
+13
View File
@@ -138,6 +138,19 @@ tyre pressures, GPS (lat, lon, elevation, heading) and the is_* flags. It does *
confirmed car facts, app structure, UI conventions, build/test/ship, documents. It replaces confirmed car facts, app structure, UI conventions, build/test/ship, documents. It replaces
upstream's EVABRPUploader text. The test emulator is now in the flake: `nix run .#car-emulator` upstream's EVABRPUploader text. The test emulator is now in the flake: `nix run .#car-emulator`
(Android 9 AOSP at 1920×720, 160 dpi; the car build installs as the system user — checked). (Android 9 AOSP at 1920×720, 160 dpi; the car build installs as the system user — checked).
- 2026-10-26 — App 2.36.0: Overview style **Grid**, after the Tron films (`GridView`): neon lines
on black, a perspective grid floor that runs towards the driver with the speed (4 m tiles,
~30 fps only while moving and on screen), the speed large and thin, the battery as a
20-segment light wall standing on the floor (orange below 20 %, "LOW ENERGY"), power on a disc
(zero at the top, clockwise drawing, anticlockwise regeneration; an inner ring that turns with
the power), the last two minutes of power as a light-cycle trail of right angles
(`ClusterValues.powerTrail`, the Cockpit's `PowerTrace`; gaps stay gaps), P R N D tiles, the
clock, and five read-outs in cut-corner boxes. Glow from layered strokes (no blur filters).
Original cyan, or the accent colour (`hasOriginal`; low battery then white); in Original the
app turns cyan (`ThemeOverlay.EV.Style.Grid`). Named "Grid", not after the films. THEMES.md has
it with two screenshots. Emulator: sample values (full screen, with navigation, accent, 15 %),
the floor moving between frames, and no data (dashes, nothing lit, still floor); no crash.
Tests and lint. Not tried on the car.
- 2026-10-25 — Server 2.6.2: **the update relay logs what it hands out**, per car (installation - 2026-10-25 — Server 2.6.2: **the update relay logs what it hands out**, per car (installation
ID, or "shared token"): `update check` with the number of releases and the newest ID, or "shared token"): `update check` with the number of releases and the newest
pre-release, `update download started` with file and size, then `update handed out` with pre-release, `update download started` with file and size, then `update handed out` with
+16 -2
View File
@@ -1,6 +1,6 @@
# Overview styles # Overview styles
The Overview can look twelve ways: pick one under **Settings → Overview style**. Every style shows The Overview can look thirteen ways: pick one under **Settings → Overview style**. Every style shows
the same readings (battery, range, power, speed, gear, usage, trip, climate, odometer, time) and the same readings (battery, range, power, speed, gear, usage, trip, climate, odometer, time) and
shows "—" for anything the car did not answer, never a made-up zero. shows "—" for anything the car did not answer, never a made-up zero.
@@ -10,7 +10,7 @@ emulator at the car's resolution with **sample values** (87 km/h in D, 76 %, 18.
emulator has no car; on the car the values are live. emulator has no car; on the car the values are live.
**Colours.** Most styles are drawn in the accent colour you pick from the MG4 paints (Settings → **Colours.** Most styles are drawn in the accent colour you pick from the MG4 paints (Settings →
Accent colour; shown here in Dynamic Red). Retro and Terminal can also show their original Accent colour; shown here in Dynamic Red). Retro, Terminal and Grid can also show their original
colours, and Sci-fi, Cartoon and Nixie tubes always do; while one of those is shown, the whole app colours, and Sci-fi, Cartoon and Nixie tubes always do; while one of those is shown, the whole app
(navigation, buttons, switches) takes on its colours too. (navigation, buttons, switches) takes on its colours too.
@@ -28,6 +28,7 @@ colours, and Sci-fi, Cartoon and Nixie tubes always do; while one of those is sh
| [Terminal](#terminal) | green-screen shell | original or accent | | [Terminal](#terminal) | green-screen shell | original or accent |
| [Nixie tubes](#nixie-tubes) | glowing glass tubes | own | | [Nixie tubes](#nixie-tubes) | glowing glass tubes | own |
| [Chronograph](#chronograph) | a watch face | accent | | [Chronograph](#chronograph) | a watch face | accent |
| [Grid](#grid) | neon lines on a glowing grid floor | original or accent |
## Tiles ## Tiles
@@ -143,3 +144,16 @@ odometer.
![Chronograph with the navigation](screenshots/themes/chronograph-panel.png) ![Chronograph with the navigation](screenshots/themes/chronograph-panel.png)
![Chronograph, full screen](screenshots/themes/chronograph.png) ![Chronograph, full screen](screenshots/themes/chronograph.png)
## Grid
After the neon world of the Tron films: glowing lines on black and a perspective grid floor that
runs towards you as fast as you drive. The speed sits large in the middle; the battery is a
segmented light wall standing on the floor, turning orange when it runs low; power lights up a
disc whose inner ring turns with it, anticlockwise for regeneration; and the last two minutes of
power run across the floor as a light-cycle trail that only turns at right angles. In its
original cyan (shown, and the app turns cyan with it) or in the accent colour — red gives the
films' other side.
![Grid with the navigation](screenshots/themes/grid-panel.png)
![Grid, full screen](screenshots/themes/grid.png)
+2 -2
View File
@@ -34,8 +34,8 @@ android {
applicationId "com.stinnesbeck.nilsmg4" applicationId "com.stinnesbeck.nilsmg4"
minSdk 27 minSdk 27
targetSdk 36 targetSdk 36
versionCode 138 versionCode 139
versionName "2.35.3" versionName "2.36.0"
// Nothing about the server ships in the APK: its URL and ingest token are entered in // Nothing about the server ships in the APK: its URL and ingest token are entered in
// the app's Server tab (ServerConfig), and the database is only reachable by the server. // the app's Server tab (ServerConfig), and the database is only reachable by the server.
@@ -21,4 +21,6 @@ final class ClusterValues {
String clock = ""; String clock = "";
/** Tyre pressures formatted in the chosen unit, "—" when unread; never null. */ /** Tyre pressures formatted in the chosen unit, "—" when unread; never null. */
String tyreFl = "—", tyreFr = "—", tyreRl = "—", tyreRr = "—"; String tyreFl = "—", tyreFr = "—", tyreRl = "—", tyreRr = "—";
/** Battery power over the last two minutes, oldest first, NaN where unread; null before any. */
float[] powerTrail;
} }
@@ -0,0 +1,411 @@
package com.evsuite.abrp;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.LinearGradient;
import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.RectF;
import android.graphics.Shader;
import android.graphics.Typeface;
import android.os.SystemClock;
/**
* The Grid Overview style, after the neon world of the Tron films: glowing lines on black, a
* perspective grid floor that runs towards you with the speed, and shapes with cut corners.
*
* Centre: the speed, large and thin. Left: the battery as a segmented light wall standing on
* the floor, with the charge and range beside it. Right: power on a disc — the ring lights from
* the top, clockwise while drawing and anticlockwise while regenerating, and its inner ring
* turns with the power. On the floor, the last two minutes of power as a light-cycle trail,
* which only ever turns at right angles. P R N D as tiles on top, the clock top right, and a
* row of read-outs along the bottom. Unread values show "—", light nothing and leave gaps in
* the trail; the floor stands still when the speed is unread.
*
* Two palettes: the original cyan, or the chosen accent colour ({@link #setAccentPalette}). A
* low battery is orange in the original (the films' other side) and white in the accent one,
* so it never looks like the accent itself. The floor and the disc animate (about 30 frames a
* second) only while the car moves and the view is on screen.
*/
final class GridView extends ClusterCanvasView {
/** Battery segments in the light wall, disc segments, metres per grid tile. */
private static final int WALL = 20, DISC = 60;
private static final float TILE_M = 4f;
private final Paint line = new Paint(Paint.ANTI_ALIAS_FLAG);
private final Paint fill = new Paint(Paint.ANTI_ALIAS_FLAG);
private final Paint text = new Paint(Paint.ANTI_ALIAS_FLAG);
private final Paint sky = new Paint();
private final Path path = new Path();
private android.graphics.DashPathEffect dash;
private float dashFor;
private final RectF r = new RectF();
private final Typeface thin = Typeface.create("sans-serif-thin", Typeface.NORMAL);
private final Typeface label = Typeface.create("sans-serif-condensed", Typeface.BOLD);
private final int accent, accentLight;
private int main, light, dim, alert;
/** The floor's position within one tile (0..1) and the disc's inner ring angle. */
private float floorPhase, discTurn;
private long lastFrame;
private int skyFor = -1;
GridView(Context context) {
super(context);
accent = AccentColor.resolve(context, R.attr.evAccent);
accentLight = AccentColor.resolve(context, R.attr.evAccentLight);
line.setStyle(Paint.Style.STROKE);
line.setStrokeCap(Paint.Cap.SQUARE);
line.setStrokeJoin(Paint.Join.MITER);
setAccentPalette(false);
}
/** The chosen accent colour instead of the original cyan. */
void setAccentPalette(boolean useAccent) {
main = useAccent ? accent : 0xFF18E6FF;
light = useAccent ? accentLight : 0xFFC8FCFF;
dim = AccentColor.withAlpha(main, 0x40);
alert = useAccent ? 0xFFFFFFFF : 0xFFFF8A1F;
skyFor = -1;
invalidate();
}
@Override
protected void onDraw(Canvas c) {
super.onDraw(c);
float w = getWidth(), h = getHeight();
if (w <= 0 || h <= 0) return;
float u = Math.min(h / 640f, w / 1100f);
advance();
c.drawColor(0xFF000000);
float horizon = h * 0.55f;
drawSky(c, w, horizon);
drawFloor(c, w, h, horizon, u);
drawGears(c, w, u);
drawClock(c, w, u);
drawSpeed(c, w, horizon, u);
drawWall(c, horizon, u);
drawDisc(c, w, u);
drawTrail(c, w, h, u);
drawReadouts(c, w, h, u);
boolean moving = v.speedKmh != null && v.speedKmh > 0.5f;
keepAnimating(moving, 33);
}
/** Moves the floor by the distance driven since the last frame, and turns the disc. */
private void advance() {
long now = SystemClock.uptimeMillis();
float dt = lastFrame == 0 ? 0f : Math.min(0.2f, (now - lastFrame) / 1000f);
lastFrame = now;
if (v.speedKmh != null && v.speedKmh > 0.5f) {
floorPhase = (floorPhase + v.speedKmh / 3.6f * dt / TILE_M) % 1f;
}
if (v.powerKw != null) discTurn = (discTurn + v.powerKw * 3f * dt) % 360f;
}
// ---------- Floor ----------
private void drawSky(Canvas c, float w, float horizon) {
if (skyFor != (int) horizon) {
sky.setShader(new LinearGradient(0, horizon - horizon * 0.45f, 0, horizon,
0x00000000, AccentColor.withAlpha(main, 0x30), Shader.TileMode.CLAMP));
skyFor = (int) horizon;
}
c.drawRect(0, horizon - horizon * 0.45f, w, horizon, sky);
}
/** Lines converging on the horizon, and cross lines that run towards the viewer. */
private void drawFloor(Canvas c, float w, float h, float horizon, float u) {
float vx = w / 2f, depth = h - horizon;
line.setStrokeWidth(1.2f * u);
for (int i = -14; i <= 14; i++) {
float bottomX = vx + i * w * 0.09f;
line.setColor(AccentColor.withAlpha(main, 0x38));
c.drawLine(vx + (bottomX - vx) * 0.02f, horizon, bottomX, h, line);
}
for (int k = 0; k < 24; k++) {
float d = k + 1f - floorPhase;
if (d <= 0.05f) continue;
float y = horizon + depth / d;
if (y > h || y < horizon + 1f) continue;
float near = (y - horizon) / depth;
line.setColor(AccentColor.withAlpha(main, (int) (0x10 + 0x50 * near)));
c.drawLine(0, y, w, y, line);
}
glowLine(c, 0, horizon, w, horizon, main, 1.5f * u);
}
// ---------- Top ----------
private void drawGears(Canvas c, float w, float u) {
String[] gears = { "P", "R", "N", "D" };
String now = Gear.label(v.gear);
float tw = 58f * u, th = 40f * u, gap = 12f * u;
float x = w / 2f - (gears.length * tw + (gears.length - 1) * gap) / 2f, top = 22f * u;
text.setTypeface(label);
text.setTextSize(24f * u);
text.setTextAlign(Paint.Align.CENTER);
for (String g : gears) {
boolean on = g.equals(now);
chamfer(x, top, x + tw, top + th, 9f * u);
if (on) {
fill.setColor(main);
c.drawPath(path, fill);
glow(c, path, main, 1.5f * u);
} else {
line.setColor(dim);
line.setStrokeWidth(1.5f * u);
c.drawPath(path, line);
}
text.setColor(on ? 0xFF000000 : dim);
c.drawText(g, x + tw / 2f, top + th * 0.72f, text);
x += tw + gap;
}
}
private void drawClock(Canvas c, float w, float u) {
text.setTypeface(thin);
text.setTextSize(44f * u);
text.setTextAlign(Paint.Align.RIGHT);
glowText(c, v.clock.isEmpty() ? "—" : v.clock, w - 28f * u, 62f * u, light, u);
}
// ---------- Speed ----------
private void drawSpeed(Canvas c, float w, float horizon, float u) {
text.setTypeface(thin);
text.setTextSize(210f * u);
text.setTextAlign(Paint.Align.CENTER);
glowText(c, fmt(v.speedKmh, "%.0f", "—"), w / 2f, horizon - 62f * u, light, u);
caption(c, "KM/H", w / 2f, horizon - 22f * u, u, Paint.Align.CENTER);
}
// ---------- Battery wall ----------
private void drawWall(Canvas c, float horizon, float u) {
boolean low = v.socPercent != null && v.socPercent < 20f;
int colour = low ? alert : main;
float left = 34f * u, right = left + 52f * u, top = 96f * u, bottom = horizon + 70f * u;
float seg = (bottom - top) / WALL;
int lit = v.socPercent == null ? 0 : Math.round(v.socPercent / 100f * WALL);
for (int i = 0; i < WALL; i++) {
float b = bottom - i * seg, t = b - seg + 3f * u;
r.set(left, t, right, b);
if (i < lit) {
fill.setColor(i == lit - 1 ? light : colour);
c.drawRect(r, fill);
} else {
line.setColor(dim);
line.setStrokeWidth(1f * u);
c.drawRect(r, line);
}
}
chamfer(left - 8f * u, top - 8f * u, right + 8f * u, bottom + 8f * u, 10f * u);
glow(c, path, colour, 1.5f * u);
float x = right + 30f * u;
caption(c, low ? "LOW ENERGY" : "ENERGY", x, top + 18f * u, u, Paint.Align.LEFT);
text.setTypeface(thin);
text.setTextSize(96f * u);
text.setTextAlign(Paint.Align.LEFT);
String soc = fmt(v.socPercent, "%.0f", "—");
glowText(c, soc, x, top + 116f * u, low ? alert : light, u);
float socWidth = text.measureText(soc);
text.setTextSize(36f * u);
glowText(c, "%", x + socWidth + 8f * u, top + 116f * u, colour, u);
caption(c, "RANGE", x, top + 168f * u, u, Paint.Align.LEFT);
text.setTypeface(thin);
text.setTextSize(52f * u);
glowText(c, fmt(v.rangeKm, "%.0f km", "—"), x, top + 222f * u, light, u);
}
// ---------- Power disc ----------
/** Zero at the top, −50 kW a quarter turn anticlockwise, 150 kW three quarters clockwise. */
private void drawDisc(Canvas c, float w, float u) {
float cx = w - 170f * u, cy = 238f * u, outer = 132f * u, ring = 22f * u;
line.setColor(dim);
line.setStrokeWidth(1.5f * u);
c.drawCircle(cx, cy, outer + 10f * u, line);
float sweepPerKw = 270f / 200f;
float power = v.powerKw == null ? 0f : Math.max(-50f, Math.min(150f, v.powerKw));
float segDeg = 270f / DISC;
for (int i = 0; i < DISC; i++) {
// Segment i covers −50 + i·(200/DISC) … one step more, drawn from −135° (top-left).
float kwFrom = -50f + i * 200f / DISC, kwTo = kwFrom + 200f / DISC;
boolean on = v.powerKw != null
&& (power >= 0 ? kwFrom >= 0f && kwFrom < power : kwTo <= 0f && kwTo > power);
float start = -90f + kwFrom * sweepPerKw;
r.set(cx - outer + ring / 2f, cy - outer + ring / 2f, cx + outer - ring / 2f, cy + outer - ring / 2f);
line.setStrokeWidth(ring);
line.setStrokeCap(Paint.Cap.BUTT);
line.setColor(on ? (power < 0 ? light : main) : AccentColor.withAlpha(main, 0x22));
c.drawArc(r, start + 0.6f, segDeg - 1.2f, false, line);
}
line.setStrokeCap(Paint.Cap.SQUARE);
// The zero mark, and an inner ring of dashes that turns with the power.
glowLine(c, cx, cy - outer - 16f * u, cx, cy - outer + ring + 6f * u, light, 2f * u);
float inner = outer - ring - 22f * u;
line.setStrokeWidth(3f * u);
line.setColor(main);
r.set(cx - inner, cy - inner, cx + inner, cy + inner);
for (int k = 0; k < 8; k++) c.drawArc(r, discTurn + k * 45f, 26f, false, line);
line.setColor(dim);
line.setStrokeWidth(1f * u);
c.drawCircle(cx, cy, inner - 12f * u, line);
text.setTypeface(thin);
text.setTextSize(64f * u);
text.setTextAlign(Paint.Align.CENTER);
glowText(c, fmt(v.powerKw, "%.1f", "—"), cx, cy + 18f * u, light, u);
caption(c, v.powerKw != null && v.powerKw < 0f ? "KW · REGEN" : "KW", cx, cy + 50f * u, u,
Paint.Align.CENTER);
caption(c, "POWER", cx, cy - 46f * u, u, Paint.Align.CENTER);
}
// ---------- Light trail ----------
/** Power over the last two minutes as a trail of right angles; NaN breaks it. */
private void drawTrail(Canvas c, float w, float h, float u) {
float[] trail = v.powerTrail;
float left = 300f * u, right = w - 360f * u, top = h * 0.66f, bottom = h * 0.84f;
if (right - left < 200f * u) return;
float max = 10f, min = -10f;
if (trail != null) {
for (float p : trail) {
if (Float.isNaN(p)) continue;
max = Math.max(max, (float) Math.ceil(p / 10f) * 10f);
min = Math.min(min, (float) Math.floor(p / 10f) * 10f);
}
}
float zero = bottom - (0f - min) / (max - min) * (bottom - top);
line.setColor(dim);
line.setStrokeWidth(1f * u);
if (dash == null || dashFor != u) {
dash = new android.graphics.DashPathEffect(new float[] { 6f * u, 8f * u }, 0);
dashFor = u;
}
line.setPathEffect(dash);
c.drawLine(left, zero, right, zero, line);
line.setPathEffect(null);
caption(c, "POWER · LAST 2 MIN", left, top - 10f * u, u, Paint.Align.LEFT);
if (trail == null || trail.length < 2) return;
float step = (right - left) / (trail.length - 1);
path.reset();
boolean open = false;
float lastX = 0, lastY = 0;
for (int i = 0; i < trail.length; i++) {
float p = trail[i];
if (Float.isNaN(p)) {
open = false;
continue;
}
float x = left + i * step, y = bottom - (p - min) / (max - min) * (bottom - top);
if (!open) {
path.moveTo(x, y);
open = true;
} else {
path.lineTo(x, lastY); // along, then a right angle
path.lineTo(x, y);
}
lastX = x;
lastY = y;
}
glow(c, path, main, 2.5f * u);
if (open) {
fill.setColor(light);
c.drawCircle(lastX, lastY, 5f * u, fill);
fill.setColor(AccentColor.withAlpha(main, 0x50));
c.drawCircle(lastX, lastY, 12f * u, fill);
}
}
// ---------- Read-outs ----------
private void drawReadouts(Canvas c, float w, float h, float u) {
String[][] cells = {
{ "USAGE · 5 MIN", fmt(v.usage5Min, "%.1f", "—") },
{ "SINCE START", fmt(v.usageTrip, "%.1f", "—") },
{ "TRIP", fmt(v.tripKm, "%.1f km", "—") },
{ "CLIMATE", fmt(v.climateCelsius, "%.0f °C", "—") },
{ "ODOMETER", fmt(v.odometerKm, "%,.0f km", "—") },
};
float margin = 24f * u, gap = 14f * u, top = h - 70f * u, bottom = h - 14f * u;
float cw = (w - 2 * margin - (cells.length - 1) * gap) / cells.length;
for (int i = 0; i < cells.length; i++) {
float l = margin + i * (cw + gap);
chamfer(l, top, l + cw, bottom, 10f * u);
fill.setColor(0xE6000000);
c.drawPath(path, fill);
line.setColor(dim);
line.setStrokeWidth(1.5f * u);
c.drawPath(path, line);
caption(c, cells[i][0], l + 16f * u, top + 20f * u, u, Paint.Align.LEFT);
text.setTypeface(thin);
text.setTextSize(30f * u);
text.setTextAlign(Paint.Align.RIGHT);
glowText(c, cells[i][1], l + cw - 16f * u, bottom - 10f * u, light, u);
}
}
// ---------- Light ----------
/** A rectangle with its top-left and bottom-right corners cut, into [path]. */
private void chamfer(float l, float t, float rr, float b, float cut) {
path.reset();
path.moveTo(l + cut, t);
path.lineTo(rr, t);
path.lineTo(rr, b - cut);
path.lineTo(rr - cut, b);
path.lineTo(l, b);
path.lineTo(l, t + cut);
path.close();
}
/** A line that glows: two wide faint strokes under a sharp one. */
private void glow(Canvas c, Path p, int colour, float width) {
line.setColor(AccentColor.withAlpha(colour, 0x18));
line.setStrokeWidth(width * 6f);
c.drawPath(p, line);
line.setColor(AccentColor.withAlpha(colour, 0x40));
line.setStrokeWidth(width * 3f);
c.drawPath(p, line);
line.setColor(colour);
line.setStrokeWidth(width);
c.drawPath(p, line);
}
private void glowLine(Canvas c, float x1, float y1, float x2, float y2, int colour, float width) {
path.reset();
path.moveTo(x1, y1);
path.lineTo(x2, y2);
glow(c, path, colour, width);
}
/** Text with a halo: a wide faint outline under the letters. */
private void glowText(Canvas c, String s, float x, float y, int colour, float u) {
text.setStyle(Paint.Style.STROKE);
text.setStrokeWidth(7f * u);
text.setColor(AccentColor.withAlpha(colour, 0x30));
c.drawText(s, x, y, text);
text.setStyle(Paint.Style.FILL);
text.setColor(colour);
c.drawText(s, x, y, text);
}
/** A small label in spaced capitals. */
private void caption(Canvas c, String s, float x, float y, float u, Paint.Align align) {
text.setTypeface(label);
text.setTextSize(15f * u);
text.setLetterSpacing(0.2f);
text.setTextAlign(align);
text.setStyle(Paint.Style.FILL);
text.setColor(main);
c.drawText(s, x, y, text);
text.setLetterSpacing(0f);
}
}
@@ -68,6 +68,7 @@ public class MainActivity extends AppCompatActivity {
/** The styles built on {@link ClusterCanvasView}, fed together. */ /** The styles built on {@link ClusterCanvasView}, fed together. */
private final java.util.List<ClusterCanvasView> canvasStyles = new java.util.ArrayList<>(); private final java.util.List<ClusterCanvasView> canvasStyles = new java.util.ArrayList<>();
private TerminalView terminalView; private TerminalView terminalView;
private GridView gridView;
/** The pane of each Overview style, by its {@link OverviewStyle} ordinal. */ /** The pane of each Overview style, by its {@link OverviewStyle} ordinal. */
private View[] stylePanes; private View[] stylePanes;
private View consumptionPane; private View consumptionPane;
@@ -342,13 +343,14 @@ public class MainActivity extends AppCompatActivity {
View sciFiPane = drawnPane(sciFiView), cartoonPane = drawnPane(cartoonView), View sciFiPane = drawnPane(sciFiView), cartoonPane = drawnPane(cartoonView),
mgPane = drawnPane(octagonView); mgPane = drawnPane(octagonView);
terminalView = new TerminalView(this); terminalView = new TerminalView(this);
gridView = new GridView(this);
java.util.Collections.addAll(canvasStyles, new MinimalView(this), terminalView, java.util.Collections.addAll(canvasStyles, new MinimalView(this), terminalView,
new NixieView(this), new ChronographView(this)); new NixieView(this), new ChronographView(this), gridView);
View[] canvasPanes = new View[canvasStyles.size()]; View[] canvasPanes = new View[canvasStyles.size()];
for (int i = 0; i < canvasPanes.length; i++) canvasPanes[i] = drawnPane(canvasStyles.get(i)); for (int i = 0; i < canvasPanes.length; i++) canvasPanes[i] = drawnPane(canvasStyles.get(i));
stylePanes = new View[] { vehiclePane, gaugesPane, cockpitPane, retroPane, stylePanes = new View[] { vehiclePane, gaugesPane, cockpitPane, retroPane,
futuristicPane, sciFiPane, cartoonPane, mgPane, futuristicPane, sciFiPane, cartoonPane, mgPane,
canvasPanes[0], canvasPanes[1], canvasPanes[2], canvasPanes[3] }; canvasPanes[0], canvasPanes[1], canvasPanes[2], canvasPanes[3], canvasPanes[4] };
// The Overview page holds every style; Settings → Overview style picks the one shown. // The Overview page holds every style; Settings → Overview style picks the one shown.
android.widget.FrameLayout overviewPage = new android.widget.FrameLayout(this); android.widget.FrameLayout overviewPage = new android.widget.FrameLayout(this);
overviewPage.setLayoutParams(new android.view.ViewGroup.LayoutParams( overviewPage.setLayoutParams(new android.view.ViewGroup.LayoutParams(
@@ -1713,9 +1715,10 @@ public class MainActivity extends AppCompatActivity {
private void applyOverviewStyle() { private void applyOverviewStyle() {
OverviewStyle style = overviewStyle(); OverviewStyle style = overviewStyle();
for (View pane : stylePanes) pane.setVisibility(pane == stylePanes[style.ordinal()] ? View.VISIBLE : View.GONE); for (View pane : stylePanes) pane.setVisibility(pane == stylePanes[style.ordinal()] ? View.VISIBLE : View.GONE);
// Retro and Terminal: their original palette or the accent colour, as chosen for each. // Retro, Terminal and Grid: their original palette or the accent colour, as chosen for each.
retroView.setAccentPalette(!originalColours(OverviewStyle.RETRO)); retroView.setAccentPalette(!originalColours(OverviewStyle.RETRO));
terminalView.setAccentPalette(!originalColours(OverviewStyle.TERMINAL)); terminalView.setAccentPalette(!originalColours(OverviewStyle.TERMINAL));
gridView.setAccentPalette(!originalColours(OverviewStyle.GRID));
// The accent choices depend on the style ("Original colours" for Retro and Terminal). // The accent choices depend on the style ("Original colours" for Retro and Terminal).
if (settingsPane != null) bindAccentColor(); if (settingsPane != null) bindAccentColor();
} }
@@ -1746,7 +1749,7 @@ public class MainActivity extends AppCompatActivity {
getString(R.string.overview_style_mg), getString(R.string.overview_style_mg),
getString(R.string.overview_style_minimal), getString(R.string.overview_style_terminal), getString(R.string.overview_style_minimal), getString(R.string.overview_style_terminal),
getString(R.string.overview_style_nixie), getString(R.string.overview_style_nixie),
getString(R.string.overview_style_chronograph) }; getString(R.string.overview_style_chronograph), getString(R.string.overview_style_grid) };
} }
/** The style drop-down; every style is fed on every reading, only the chosen one is shown. */ /** The style drop-down; every style is fed on every reading, only the chosen one is shown. */
@@ -1812,6 +1815,7 @@ public class MainActivity extends AppCompatActivity {
v.tyreFr = unit.format(s.getTirePressures().getFrontRightKpa()); v.tyreFr = unit.format(s.getTirePressures().getFrontRightKpa());
v.tyreRl = unit.format(s.getTirePressures().getRearLeftKpa()); v.tyreRl = unit.format(s.getTirePressures().getRearLeftKpa());
v.tyreRr = unit.format(s.getTirePressures().getRearRightKpa()); v.tyreRr = unit.format(s.getTirePressures().getRearRightKpa());
v.powerTrail = cockpitTrace.snapshot();
retroView.setValues(v); retroView.setValues(v);
flowView.setValues(v); flowView.setValues(v);
sciFiView.setValues(v); sciFiView.setValues(v);
@@ -8,7 +8,7 @@ package com.evsuite.abrp;
* colour for it. Styles with only their own fixed palette (the starship console, the cartoon, * colour for it. Styles with only their own fixed palette (the starship console, the cartoon,
* the flight display, the nixie tubes) ignore it and Settings greys it out. * the flight display, the nixie tubes) ignore it and Settings greys it out.
* - {@link #hasOriginal}: has an original palette of its own as well (Retro's turquoise * - {@link #hasOriginal}: has an original palette of its own as well (Retro's turquoise
* display, the terminal's green phosphor); Settings then offers "Original colours" above the * display, the terminal's green phosphor, the Grid's neon cyan); Settings then offers "Original colours" above the
* accent colours, remembered per style under {@link #originalKey()}. * accent colours, remembered per style under {@link #originalKey()}.
* *
* Pure Java, unit-tested. * Pure Java, unit-tested.
@@ -25,7 +25,8 @@ enum OverviewStyle {
MINIMAL(true, false), MINIMAL(true, false),
TERMINAL(true, true), TERMINAL(true, true),
NIXIE(false, false), NIXIE(false, false),
CHRONOGRAPH(true, false); CHRONOGRAPH(true, false),
GRID(true, true);
static final String PREF_KEY = "overview_style"; static final String PREF_KEY = "overview_style";
@@ -18,6 +18,7 @@ final class StylePalette {
case SCIFI: return R.style.ThemeOverlay_EV_Style_SciFi; case SCIFI: return R.style.ThemeOverlay_EV_Style_SciFi;
case CARTOON: return R.style.ThemeOverlay_EV_Style_Cartoon; case CARTOON: return R.style.ThemeOverlay_EV_Style_Cartoon;
case NIXIE: return R.style.ThemeOverlay_EV_Style_Nixie; case NIXIE: return R.style.ThemeOverlay_EV_Style_Nixie;
case GRID: return R.style.ThemeOverlay_EV_Style_Grid;
default: return 0; default: return 0;
} }
} }
+1
View File
@@ -135,6 +135,7 @@
<string name="overview_style_minimal">Minimal</string> <string name="overview_style_minimal">Minimal</string>
<string name="overview_style_terminal">Terminal</string> <string name="overview_style_terminal">Terminal</string>
<string name="accent_original">Original</string> <string name="accent_original">Original</string>
<string name="overview_style_grid">Grid</string>
<string name="overview_style_nixie">Nixie tubes</string> <string name="overview_style_nixie">Nixie tubes</string>
<string name="overview_style_chronograph">Chronograph</string> <string name="overview_style_chronograph">Chronograph</string>
<string name="cockpit_battery">BATTERY</string> <string name="cockpit_battery">BATTERY</string>
@@ -54,4 +54,14 @@
<item name="evAccentStroke">#80774A26</item> <item name="evAccentStroke">#80774A26</item>
<item name="evAccentTrack">#6A4224</item> <item name="evAccentTrack">#6A4224</item>
</style> </style>
<!-- Grid: the neon cyan of its lines. -->
<style name="ThemeOverlay.EV.Style.Grid" parent="">
<item name="evAccent">#18E6FF</item>
<item name="evOnAccent">#00161A</item>
<item name="evAccentLight">#C8FCFF</item>
<item name="evAccentDeep">#06343C</item>
<item name="evAccentSurface">#0A1C20</item>
<item name="evAccentStroke">#80267A86</item>
<item name="evAccentTrack">#1E6672</item>
</style>
</resources> </resources>
Binary file not shown.

After

Width:  |  Height:  |  Size: 72 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 64 KiB