diff --git a/AGENTS.md b/AGENTS.md
index 786414c..42eaa83 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -69,7 +69,7 @@ Repository: `ssh://git@git.stinnesbeck.com:2222/nils/mg4.git` (remote `origin`),
| 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.5 s on a background thread. Three styles (Settings → Overview style): Tiles, Gauges (`GaugeView` needles), Cockpit (`RingView` rings, `TraceView` power trace) |
+| 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. Five styles (Settings → Overview style): Tiles, Gauges (`GaugeView` needles), Cockpit (`RingView` rings, `TraceView` power trace), Retro (`RetroView`, seven-segment VFD), Futuristic (`FlowView`, animated energy flow) |
| Consumption / Charging | period totals, daily log, charging sessions and curve |
| 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 |
diff --git a/README.md b/README.md
index 65e59a6..bad9809 100644
--- a/README.md
+++ b/README.md
@@ -79,7 +79,7 @@ with the server using a token of its own, obtained by pairing with a phone (belo
| 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, climate setpoint) and tyre pressures. Read every half second |
+| **Overview** | Battery, range, power and speed; range, usage and energy since start; 5-minute averages of range, usage, power and speed; vehicle info (odometer, climate setpoint) and tyre pressures. Read five times a second. Five looks to choose from in Settings → Overview style: Tiles, Gauges, Cockpit, Retro and Futuristic |
| **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 |
| **Live data** | Live data on/off, ABRP credentials for sending directly from the car, upload frequency |
diff --git a/SPECS.md b/SPECS.md
index 16f3a51..273edef 100644
--- a/SPECS.md
+++ b/SPECS.md
@@ -131,6 +131,22 @@ 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
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).
+- 2026-10-24 — App 2.23.0 (branch `styles-retro-future`): two more Overview styles, each one
+ Canvas view fed a `ClusterValues` per reading. **Retro** (`RetroView`): an 80s digital cluster
+ as a vacuum fluorescent display — slanted seven-segment digits (`SevenSegment`, unit-tested)
+ with the unlit segments faintly visible, amber labels, a lit P R N D selector, the speed large,
+ power as a −50…150 kW bar (regeneration green), charge as a vertical bar (red below 20 %),
+ range, 5-minute and since-start usage, trip, climate, clock and odometer; its fixed palette is
+ `retro_*` in colors.xml (a style's own colours, not the accent). **Futuristic** (`FlowView`): a
+ hairline HUD in the accent colour — a battery capsule of scan lines, an energy channel whose
+ particles flow to the wheel while drawing and back in green while regenerating (faster with
+ more power), the speed inside a ring of dashes that turns with the speed and a power arc,
+ P R N D in a column, and a readout line. It animates at about 30 frames a second only while
+ power or speed is non-zero and the view is shown. Unread values show dashes and light or move
+ nothing in both. The Overview now reads the car every 0.2 s instead of 0.5 s (a read still
+ running skips the tick, as before). Checked on the emulator with sample values in a throwaway
+ build (`screenshots/nilsmg4-2.23.0-retro.png`, `…-futuristic.png`) and without data through
+ the real setting; no crash. Not tried on the car, including the 0.2 s reading rate.
- 2026-10-24 — App 2.22.3: the Overview style "Dashboard" is now called "Gauges" (stored as
`GAUGES`; the old value `DASHBOARD` still selects it, so the choice survives the update;
`pane_dashboard.xml` is `pane_gauges.xml`). The branches `dashboard-gauges` and
diff --git a/app/build.gradle b/app/build.gradle
index b95ac77..ad19ae0 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -34,8 +34,8 @@ android {
applicationId "com.stinnesbeck.nilsmg4"
minSdk 27
targetSdk 36
- versionCode 114
- versionName "2.22.3"
+ versionCode 115
+ versionName "2.23.0"
// 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.
diff --git a/app/src/main/java/com/evsuite/abrp/ClusterValues.java b/app/src/main/java/com/evsuite/abrp/ClusterValues.java
new file mode 100644
index 0000000..d5a8a1c
--- /dev/null
+++ b/app/src/main/java/com/evsuite/abrp/ClusterValues.java
@@ -0,0 +1,22 @@
+package com.evsuite.abrp;
+
+/**
+ * One reading for the drawn Overview styles (Retro, Futuristic): what they show, already in
+ * display units. Every field is null when unread, and the styles show that as dashes, never 0.
+ */
+final class ClusterValues {
+ Float socPercent;
+ Float rangeKm;
+ Float powerKw;
+ Float speedKmh;
+ /** The car's raw gear code ({@link Gear}). */
+ Integer gear;
+ /** kWh/100 km over the last 5 minutes and since start; km since start. */
+ Double usage5Min;
+ Double usageTrip;
+ Double tripKm;
+ Float climateCelsius;
+ Float odometerKm;
+ /** "HH:mm", never null. */
+ String clock = "";
+}
diff --git a/app/src/main/java/com/evsuite/abrp/FlowView.java b/app/src/main/java/com/evsuite/abrp/FlowView.java
new file mode 100644
index 0000000..85f942f
--- /dev/null
+++ b/app/src/main/java/com/evsuite/abrp/FlowView.java
@@ -0,0 +1,277 @@
+package com.evsuite.abrp;
+
+import android.content.Context;
+import android.graphics.Canvas;
+import android.graphics.Paint;
+import android.graphics.RectF;
+import android.graphics.Typeface;
+import android.os.SystemClock;
+import android.util.AttributeSet;
+import android.view.View;
+
+import java.util.Locale;
+
+/**
+ * The Futuristic Overview style: a holographic HUD in hairlines and the accent colour.
+ *
+ * Left, the battery as a capsule filled to the charge. Right, the "wheel": the speed inside a
+ * ring of dashes that turns with the speed, and an arc for the power. Between them, an energy
+ * channel in which particles flow from the battery to the wheel while drawing power, and back
+ * in green while regenerating, faster with more power. A column of P R N D, and a line of
+ * readouts at the bottom. Unread values show "—", fill nothing and move nothing.
+ *
+ * It animates only while something moves (power or speed) and the view is on screen, at about
+ * 30 frames a second; parked and idle it draws once per reading like the other styles.
+ */
+public final class FlowView extends View {
+
+ private static final long FRAME_MS = 33;
+
+ private final Paint line = new Paint(Paint.ANTI_ALIAS_FLAG);
+ private final Paint dim = new Paint(Paint.ANTI_ALIAS_FLAG);
+ private final Paint fill = new Paint(Paint.ANTI_ALIAS_FLAG);
+ private final Paint glow = new Paint(Paint.ANTI_ALIAS_FLAG);
+ private final Paint thin = new Paint(Paint.ANTI_ALIAS_FLAG);
+ private final Paint small = new Paint(Paint.ANTI_ALIAS_FLAG);
+ private final RectF rect = new RectF();
+ private final int accent, light, regen, warn, white, grey;
+ private ClusterValues v = new ClusterValues();
+ /** Particle offset along the channel (px) and the wheel's turn (degrees). */
+ private float flow, turn;
+ private long lastFrame;
+
+ public FlowView(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ accent = AccentColor.resolve(context, R.attr.evAccent);
+ light = AccentColor.resolve(context, R.attr.evAccentLight);
+ regen = context.getColor(R.color.ev_ok);
+ warn = context.getColor(R.color.ev_error);
+ white = context.getColor(R.color.ev_text_primary);
+ grey = context.getColor(R.color.ev_text_secondary);
+ line.setStyle(Paint.Style.STROKE);
+ line.setStrokeCap(Paint.Cap.ROUND);
+ dim.setStyle(Paint.Style.STROKE);
+ dim.setColor(AccentColor.withAlpha(accent, 0x40));
+ glow.setStyle(Paint.Style.STROKE);
+ glow.setStrokeCap(Paint.Cap.ROUND);
+ Typeface thinFace = Typeface.create("sans-serif-thin", Typeface.NORMAL);
+ thin.setTypeface(thinFace);
+ thin.setColor(white);
+ thin.setFontFeatureSettings("tnum");
+ small.setTypeface(Typeface.create("sans-serif-medium", Typeface.NORMAL));
+ small.setLetterSpacing(0.2f);
+ small.setFontFeatureSettings("tnum");
+ }
+
+ void setValues(ClusterValues values) {
+ v = values == null ? new ClusterValues() : values;
+ invalidate();
+ }
+
+ @Override
+ protected void onDraw(Canvas c) {
+ super.onDraw(c);
+ float w = getWidth(), h = getHeight();
+ if (w <= 0 || h <= 0) return;
+ float d = getResources().getDisplayMetrics().density;
+
+ long now = SystemClock.uptimeMillis();
+ float dt = lastFrame == 0 ? 0f : Math.min(100, now - lastFrame) / 1000f;
+ lastFrame = now;
+ float power = v.powerKw == null ? 0f : v.powerKw;
+ float speed = v.speedKmh == null ? 0f : v.speedKmh;
+ boolean flowing = Math.abs(power) >= 0.3f;
+ if (flowing) flow += Math.signum(power) * (60f + Math.min(150f, Math.abs(power)) * 7f) * d * dt;
+ if (speed >= 0.5f) turn += Math.min(180f, speed) * 1.2f * dt;
+
+ drawBrackets(c, w, h, d);
+ float cy = h * 0.44f;
+ float bl = w * 0.05f, br = w * 0.115f;
+ drawBattery(c, bl, h * 0.12f, br, h * 0.78f, d);
+ float wheelR = h * 0.34f, wheelX = w * 0.68f;
+ drawWheel(c, wheelX, cy, wheelR, d);
+ drawChannel(c, br + 10f * d, wheelX - wheelR - 14f * d, cy, d, power, flowing);
+ drawGears(c, w * 0.935f, h, d);
+ drawReadouts(c, w, h, d);
+
+ if ((flowing || speed >= 0.5f) && isShown()) postInvalidateDelayed(FRAME_MS);
+ else lastFrame = 0;
+ }
+
+ private void drawBrackets(Canvas c, float w, float h, float d) {
+ line.setColor(AccentColor.withAlpha(accent, 0x90));
+ line.setStrokeWidth(1.5f * d);
+ float l = 18f * d, m = 4f * d;
+ float[][] corners = { { m, m, 1, 1 }, { w - m, m, -1, 1 }, { m, h - m, 1, -1 }, { w - m, h - m, -1, -1 } };
+ for (float[] k : corners) {
+ c.drawLine(k[0], k[1], k[0] + k[2] * l, k[1], line);
+ c.drawLine(k[0], k[1], k[0], k[1] + k[3] * l, line);
+ }
+ }
+
+ private void drawBattery(Canvas c, float l, float t, float r, float b, float d) {
+ float radius = (r - l) * 0.35f;
+ boolean low = v.socPercent != null && v.socPercent < 20f;
+ int colour = low ? warn : accent;
+ // The terminal, then the outline.
+ fill.setColor(AccentColor.withAlpha(colour, 0xB0));
+ rect.set(l + (r - l) * 0.32f, t - 8f * d, r - (r - l) * 0.32f, t - 2f * d);
+ c.drawRoundRect(rect, 2f * d, 2f * d, fill);
+ line.setColor(colour);
+ line.setStrokeWidth(1.5f * d);
+ rect.set(l, t, r, b);
+ c.drawRoundRect(rect, radius, radius, line);
+ // The charge as scan lines from the bottom.
+ if (v.socPercent != null) {
+ float inset = 6f * d, top = b - inset - (b - t - 2 * inset) * Math.max(0f, Math.min(100f, v.socPercent)) / 100f;
+ line.setStrokeWidth(2f * d);
+ for (float y = b - inset; y >= top; y -= 5f * d) {
+ float k = (y - top) / Math.max(1f, b - inset - top);
+ line.setColor(AccentColor.withAlpha(colour, (int) (0x50 + 0xAF * (1f - k * 0.6f))));
+ c.drawLine(l + inset, y, r - inset, y, line);
+ }
+ glow.setColor(AccentColor.withAlpha(colour, 0x50));
+ glow.setStrokeWidth(5f * d);
+ c.drawLine(l + inset, top, r - inset, top, glow);
+ }
+
+ float x = r + 28f * d;
+ caption(c, "BATTERY", x, t + 10f * d, Paint.Align.LEFT);
+ thin.setTextAlign(Paint.Align.LEFT);
+ thin.setTextSize((b - t) * 0.30f);
+ thin.setColor(low ? warn : white);
+ String soc = v.socPercent == null ? "—" : String.format(Locale.US, "%.0f", v.socPercent);
+ c.drawText(soc, x - 4f * d, t + (b - t) * 0.40f, thin);
+ float sw = thin.measureText(soc);
+ thin.setTextSize((b - t) * 0.10f);
+ thin.setColor(light);
+ c.drawText("%", x + sw + 2f * d, t + (b - t) * 0.40f, thin);
+ thin.setColor(white);
+ thin.setTextSize((b - t) * 0.10f);
+ c.drawText(v.rangeKm == null ? "— km" : String.format(Locale.US, "%.0f km", v.rangeKm), x, b - (b - t) * 0.10f, thin);
+ caption(c, "RANGE", x, b - (b - t) * 0.25f, Paint.Align.LEFT);
+ }
+
+ private void drawChannel(Canvas c, float x0, float x1, float cy, float d, float power, boolean flowing) {
+ if (x1 <= x0) return;
+ boolean regenerating = v.powerKw != null && v.powerKw < 0f;
+ int colour = regenerating ? regen : accent;
+ float half = 9f * d;
+ dim.setStrokeWidth(d);
+ c.drawLine(x0, cy - half, x1, cy - half, dim);
+ c.drawLine(x0, cy + half, x1, cy + half, dim);
+
+ if (flowing) {
+ // Particles every 34 dp, fading in and out at the ends.
+ float spacing = 34f * d, len = 12f * d;
+ float offset = ((flow % spacing) + spacing) % spacing;
+ line.setStrokeWidth(4f * d);
+ glow.setStrokeWidth(10f * d);
+ for (float x = x0 + offset - spacing; x < x1; x += spacing) {
+ float a = Math.max(x0, x), b = Math.min(x1, x + len);
+ if (b <= a) continue;
+ float edge = Math.min(a - x0, x1 - b) / (spacing * 2f);
+ int alpha = (int) (0xFF * Math.max(0.15f, Math.min(1f, edge)));
+ glow.setColor(AccentColor.withAlpha(colour, alpha / 5));
+ c.drawLine(a, cy, b, cy, glow);
+ line.setColor(AccentColor.withAlpha(colour, alpha));
+ c.drawLine(a, cy, b, cy, line);
+ }
+ }
+
+ float mid = (x0 + x1) / 2f;
+ thin.setTextAlign(Paint.Align.CENTER);
+ thin.setTextSize(30f * d);
+ thin.setColor(regenerating ? regen : white);
+ c.drawText(v.powerKw == null ? "— kW" : String.format(Locale.US, "%.1f kW", v.powerKw), mid, cy - half - 16f * d, thin);
+ thin.setColor(white);
+ String state = v.powerKw == null ? "POWER" : regenerating ? "REGENERATING" : flowing ? "DRAWING" : "IDLE";
+ caption(c, state, mid, cy + half + 26f * d, Paint.Align.CENTER);
+ }
+
+ private void drawWheel(Canvas c, float cx, float cy, float r, float d) {
+ // An outer ring of dashes that turns with the speed.
+ dim.setStrokeWidth(d);
+ c.drawCircle(cx, cy, r * 0.84f, dim);
+ line.setColor(AccentColor.withAlpha(accent, 0xA0));
+ line.setStrokeWidth(3f * d);
+ for (int i = 0; i < 72; i++) {
+ double a = Math.toRadians(turn + i * 5f);
+ float len = i % 6 == 0 ? r * 0.07f : r * 0.035f;
+ float cos = (float) Math.cos(a), sin = (float) Math.sin(a);
+ c.drawLine(cx + r * cos, cy + r * sin, cx + (r - len) * cos, cy + (r - len) * sin, line);
+ }
+ // Power as an arc from the top: clockwise drawing (150 kW = 300°), anticlockwise regen.
+ if (v.powerKw != null && Math.abs(v.powerKw) >= 0.1f) {
+ float sweep = Math.max(-100f, Math.min(300f, v.powerKw * 2f));
+ int colour = sweep < 0 ? regen : accent;
+ rect.set(cx - r * 0.92f, cy - r * 0.92f, cx + r * 0.92f, cy + r * 0.92f);
+ glow.setColor(AccentColor.withAlpha(colour, 0x40));
+ glow.setStrokeWidth(12f * d);
+ c.drawArc(rect, -90f, sweep, false, glow);
+ line.setColor(colour);
+ line.setStrokeWidth(4f * d);
+ c.drawArc(rect, -90f, sweep, false, line);
+ }
+
+ thin.setTextAlign(Paint.Align.CENTER);
+ thin.setColor(white);
+ thin.setTextSize(r * 0.62f);
+ c.drawText(v.speedKmh == null ? "—" : String.format(Locale.US, "%.0f", v.speedKmh), cx, cy + r * 0.18f, thin);
+ caption(c, "KM/H", cx, cy + r * 0.42f, Paint.Align.CENTER);
+ }
+
+ private void drawGears(Canvas c, float x, float h, float d) {
+ String[] letters = { "P", "R", "N", "D" };
+ thin.setTextAlign(Paint.Align.CENTER);
+ thin.setTextSize(h * 0.075f);
+ float y0 = h * 0.20f, pitch = h * 0.13f;
+ for (int i = 0; i < letters.length; i++) {
+ boolean on = v.gear != null && v.gear == i + 1;
+ float y = y0 + i * pitch;
+ thin.setColor(on ? white : AccentColor.withAlpha(grey, 0x50));
+ thin.setFakeBoldText(on);
+ c.drawText(letters[i], x, y, thin);
+ if (on) {
+ line.setColor(accent);
+ line.setStrokeWidth(2f * d);
+ float s = h * 0.05f, top = y - h * 0.075f;
+ c.drawLine(x - s - 6f * d, top, x - s - 6f * d, y + 6f * d, line);
+ c.drawLine(x + s + 6f * d, top, x + s + 6f * d, y + 6f * d, line);
+ }
+ }
+ thin.setFakeBoldText(false);
+ }
+
+ private void drawReadouts(Canvas c, float w, float h, float d) {
+ float top = h * 0.84f;
+ dim.setStrokeWidth(d);
+ c.drawLine(w * 0.04f, top, w * 0.96f, top, dim);
+ String[] labels = { "USAGE 5 MIN", "SINCE START", "TRIP", "CLIMATE", "ODOMETER", "TIME" };
+ String[] values = {
+ v.usage5Min == null ? "—" : String.format(Locale.US, "%.1f", v.usage5Min),
+ v.usageTrip == null ? "—" : String.format(Locale.US, "%.1f", v.usageTrip),
+ v.tripKm == null ? "— km" : String.format(Locale.US, "%.1f km", v.tripKm),
+ v.climateCelsius == null ? "— °C" : String.format(Locale.US, "%.0f °C", v.climateCelsius),
+ v.odometerKm == null ? "— km" : String.format(Locale.US, "%,.0f km", v.odometerKm),
+ v.clock.isEmpty() ? "—" : v.clock,
+ };
+ float pitch = (w * 0.92f) / labels.length;
+ for (int i = 0; i < labels.length; i++) {
+ float x = w * 0.04f + pitch * i + pitch / 2f;
+ caption(c, labels[i], x, top + 22f * d, Paint.Align.CENTER);
+ thin.setTextAlign(Paint.Align.CENTER);
+ thin.setTextSize(i == labels.length - 1 ? 34f * d : 28f * d);
+ thin.setColor(white);
+ c.drawText(values[i], x, top + 58f * d, thin);
+ if (i > 0) c.drawLine(w * 0.04f + pitch * i, top + 10f * d, w * 0.04f + pitch * i, top + 60f * d, dim);
+ }
+ }
+
+ private void caption(Canvas c, String text, float x, float baseline, Paint.Align align) {
+ small.setTextSize(12f * getResources().getDisplayMetrics().density);
+ small.setTextAlign(align);
+ small.setColor(light);
+ c.drawText(text, x, baseline, small);
+ }
+}
diff --git a/app/src/main/java/com/evsuite/abrp/MainActivity.java b/app/src/main/java/com/evsuite/abrp/MainActivity.java
index f6b2ceb..1411ecf 100644
--- a/app/src/main/java/com/evsuite/abrp/MainActivity.java
+++ b/app/src/main/java/com/evsuite/abrp/MainActivity.java
@@ -68,6 +68,7 @@ public class MainActivity extends AppCompatActivity {
private View vehiclePane;
private View gaugesPane;
private View cockpitPane;
+ private View retroPane, futuristicPane;
private View consumptionPane;
private View chargingPane;
/** Display, battery, charging price and Wi-Fi. */
@@ -246,6 +247,8 @@ public class MainActivity extends AppCompatActivity {
vehiclePane = inflatePane(R.layout.pane_vehicle);
gaugesPane = inflatePane(R.layout.pane_gauges);
cockpitPane = inflatePane(R.layout.pane_cockpit);
+ retroPane = inflatePane(R.layout.pane_retro);
+ futuristicPane = inflatePane(R.layout.pane_futuristic);
// The Overview page holds both styles; Settings → Overview style picks the one shown.
android.widget.FrameLayout overviewPage = new android.widget.FrameLayout(this);
overviewPage.setLayoutParams(new android.view.ViewGroup.LayoutParams(
@@ -254,6 +257,8 @@ public class MainActivity extends AppCompatActivity {
overviewPage.addView(vehiclePane);
overviewPage.addView(gaugesPane);
overviewPage.addView(cockpitPane);
+ overviewPage.addView(retroPane);
+ overviewPage.addView(futuristicPane);
consumptionPane = inflatePane(R.layout.pane_consumption);
chargingPane = inflatePane(R.layout.pane_charging);
settingsPane = inflatePane(R.layout.pane_settings);
@@ -1343,10 +1348,13 @@ public class MainActivity extends AppCompatActivity {
* is not work to repeat every two seconds. A read that throws leaves the cells as they
* were rather than blanking the page — a transient miss is not news worth showing.
*/
- // ---------- Overview: read every half second, off the main thread ----------
+ // ---------- Overview: read five times a second, off the main thread ----------
- /** How often the Overview reads the car while it is on screen. */
- private static final long OVERVIEW_REFRESH_MS = 500L;
+ /**
+ * How often the Overview reads the car while it is on screen: 0.2 s, so needles, rings and
+ * the speed follow the car closely. A read still running makes the next tick skip.
+ */
+ private static final long OVERVIEW_REFRESH_MS = 200L;
/** One reader thread; a read that is still running makes the next tick skip, not queue. */
private final java.util.concurrent.ExecutorService carReader =
@@ -1434,7 +1442,7 @@ public class MainActivity extends AppCompatActivity {
private Double dashTripKm;
/** The stored values of the Overview styles, in the order of the drop-down. */
- private static final String[] OVERVIEW_STYLES = { "TILES", "GAUGES", "COCKPIT" };
+ private static final String[] OVERVIEW_STYLES = { "TILES", "GAUGES", "COCKPIT", "RETRO", "FUTURISTIC" };
/** The chosen style's index in {@link #OVERVIEW_STYLES}; anything unknown is Tiles. */
private int overviewStyle() {
@@ -1449,12 +1457,15 @@ public class MainActivity extends AppCompatActivity {
vehiclePane.setVisibility(style == 0 ? View.VISIBLE : View.GONE);
gaugesPane.setVisibility(style == 1 ? View.VISIBLE : View.GONE);
cockpitPane.setVisibility(style == 2 ? View.VISIBLE : View.GONE);
+ retroPane.setVisibility(style == 3 ? View.VISIBLE : View.GONE);
+ futuristicPane.setVisibility(style == 4 ? View.VISIBLE : View.GONE);
}
- /** Tiles (the cards), Gauges (needles) or Cockpit (rings); all are fed on every reading. */
+ /** Tiles, Gauges, Cockpit, Retro or Futuristic; all are fed on every reading. */
private void bindOverviewStyle() {
String[] labels = { getString(R.string.overview_style_tiles), getString(R.string.overview_style_gauges),
- getString(R.string.overview_style_cockpit) };
+ getString(R.string.overview_style_cockpit), getString(R.string.overview_style_retro),
+ getString(R.string.overview_style_futuristic) };
bindChoices(settingsPane.findViewById(R.id.overview_style_choices), labels, overviewStyle(),
position -> {
prefs.edit().putString(PREF_OVERVIEW_STYLE, OVERVIEW_STYLES[position]).apply();
@@ -1478,6 +1489,30 @@ public class MainActivity extends AppCompatActivity {
dashBattery.setWarnBelow(20f);
}
+ // ---------- Retro and Futuristic (Overview styles, each one drawn view) ----------
+
+ private final java.text.SimpleDateFormat clockFormat =
+ new java.text.SimpleDateFormat("HH:mm", java.util.Locale.US);
+
+ private void showDrawnStyles(EnergySnapshot s) {
+ RetroView retro = retroPane.findViewById(R.id.retro_view);
+ FlowView flow = futuristicPane.findViewById(R.id.flow_view);
+ ClusterValues v = new ClusterValues();
+ v.socPercent = s.getSocPercent();
+ v.rangeKm = s.getRangeKm();
+ v.powerKw = s.getBatteryPowerKw();
+ v.speedKmh = s.getSpeedKmh();
+ v.gear = s.getGearRaw();
+ v.usage5Min = cockpitAverage == null ? null : cockpitAverage.kwhPer100Km;
+ v.usageTrip = cockpitTrip == null ? null : cockpitTrip.kwhPer100Km;
+ v.tripKm = cockpitTrip == null ? null : cockpitTrip.distanceKm;
+ v.climateCelsius = s.getClimate().getDriverTargetCelsius();
+ v.odometerKm = s.getOdometerKm();
+ v.clock = clockFormat.format(new java.util.Date());
+ retro.setValues(v);
+ flow.setValues(v);
+ }
+
// ---------- Cockpit (Overview style) ----------
/** Power over the last two minutes, a slot per second. */
@@ -1578,6 +1613,7 @@ public class MainActivity extends AppCompatActivity {
vehTireRr.setText(unit.format(s.getTirePressures().getRearRightKpa()));
showGauges(s);
showCockpit(s);
+ showDrawnStyles(s);
} catch (Throwable ignored) {
// Leave the last good reading on screen.
}
diff --git a/app/src/main/java/com/evsuite/abrp/RetroView.java b/app/src/main/java/com/evsuite/abrp/RetroView.java
new file mode 100644
index 0000000..b14743f
--- /dev/null
+++ b/app/src/main/java/com/evsuite/abrp/RetroView.java
@@ -0,0 +1,300 @@
+package com.evsuite.abrp;
+
+import android.content.Context;
+import android.graphics.Canvas;
+import android.graphics.Paint;
+import android.graphics.Path;
+import android.graphics.RectF;
+import android.graphics.Typeface;
+import android.util.AttributeSet;
+import android.view.View;
+
+import java.util.Locale;
+
+/**
+ * The Retro Overview style: an 80s digital cluster, a vacuum fluorescent display behind smoked
+ * glass. Slanted seven-segment digits ({@link SevenSegment}) with the unlit segments faintly
+ * visible, bar graphs, amber labels and a lit P R N D selector.
+ *
+ * Centre: the speed, and power as a bar from −50 to 150 kW (regeneration left of zero, green).
+ * Left: charge as a bar, the percentage and range. Right: 5-minute usage, the average since
+ * start and the distance. Bottom: climate, clock, odometer. Unread values show dashes and light
+ * no bar segments. The whole pane is one Canvas, redrawn only when a reading arrives.
+ */
+public final class RetroView extends View {
+
+ private static final float SKEW = 0.09f;
+
+ private final Paint ghost = new Paint(Paint.ANTI_ALIAS_FLAG);
+ private final Paint litPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
+ private final Paint glow = new Paint(Paint.ANTI_ALIAS_FLAG);
+ private final Paint label = new Paint(Paint.ANTI_ALIAS_FLAG);
+ private final Paint panel = new Paint(Paint.ANTI_ALIAS_FLAG);
+ private final Paint bezel = new Paint(Paint.ANTI_ALIAS_FLAG);
+ private final Path path = new Path();
+ private final RectF rect = new RectF();
+ private final int lit, regen, amber, warn;
+ private ClusterValues v = new ClusterValues();
+
+ public RetroView(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ lit = context.getColor(R.color.retro_lit);
+ regen = context.getColor(R.color.retro_regen);
+ amber = context.getColor(R.color.retro_label);
+ warn = context.getColor(R.color.retro_warn);
+ ghost.setColor(AccentColor.withAlpha(lit, 0x14));
+ glow.setStyle(Paint.Style.STROKE);
+ glow.setStrokeJoin(Paint.Join.ROUND);
+ label.setColor(amber);
+ label.setTypeface(Typeface.create(Typeface.MONOSPACE, Typeface.BOLD));
+ label.setLetterSpacing(0.12f);
+ panel.setColor(context.getColor(R.color.retro_panel));
+ bezel.setStyle(Paint.Style.STROKE);
+ bezel.setColor(context.getColor(R.color.retro_bezel));
+ }
+
+ void setValues(ClusterValues values) {
+ v = values == null ? new ClusterValues() : values;
+ invalidate();
+ }
+
+ @Override
+ protected void onDraw(Canvas c) {
+ super.onDraw(c);
+ float w = getWidth(), h = getHeight();
+ if (w <= 0 || h <= 0) return;
+ float d = getResources().getDisplayMetrics().density;
+
+ bezel.setStrokeWidth(2f * d);
+ rect.set(d, d, w - d, h - d);
+ c.drawRoundRect(rect, 18f * d, 18f * d, panel);
+ c.drawRoundRect(rect, 18f * d, 18f * d, bezel);
+
+ drawCentre(c, w, h);
+ drawLeft(c, w, h);
+ drawRight(c, w, h);
+ drawBottom(c, w, h, d);
+ }
+
+ // ---------- Blocks ----------
+
+ private void drawCentre(Canvas c, float w, float h) {
+ float cx = w / 2f;
+ // The gear selector, the lit letter boxed.
+ drawGears(c, cx, h * 0.06f, h * 0.085f);
+
+ float dh = h * 0.38f;
+ String speed = v.speedKmh == null ? "---" : String.format(Locale.US, "%.0f", v.speedKmh);
+ float right = cx + cellWidth(dh) * 1.5f;
+ drawDigits(c, speed, 3, right, h * 0.19f, dh, lit);
+ label(c, "KM/H", right + h * 0.02f, h * 0.19f + dh, h * 0.045f, Paint.Align.LEFT);
+
+ // Power: a bar from −50 to 150 kW in 5 kW segments, zero after the tenth.
+ float left = w * 0.29f, barRight = w * 0.71f, top = h * 0.70f, bottom = h * 0.77f;
+ label(c, "POWER", left, h * 0.67f, h * 0.04f, Paint.Align.LEFT);
+ String kw = v.powerKw == null ? "---" : String.format(Locale.US, "%.1f", v.powerKw);
+ label(c, "KW", barRight, h * 0.67f, h * 0.04f, Paint.Align.RIGHT);
+ drawDigits(c, kw, 4, barRight - h * 0.075f, h * 0.605f, h * 0.065f,
+ v.powerKw != null && v.powerKw < 0f ? regen : lit);
+ int segments = 40, zero = 10;
+ float pitch = (barRight - left) / segments;
+ int litTo = v.powerKw == null ? zero : zero + Math.round(Math.max(-50f, Math.min(150f, v.powerKw)) / 5f);
+ for (int i = 0; i < segments; i++) {
+ boolean on = v.powerKw != null && (litTo > zero ? i >= zero && i < litTo : i >= litTo && i < zero);
+ float x = left + i * pitch;
+ float segTop = i == zero ? top - h * 0.015f : top;
+ bar(c, x + pitch * 0.18f, segTop, x + pitch * 0.82f, bottom, on, i < zero ? regen : lit);
+ }
+ float[] marks = { -50f, 0f, 50f, 100f, 150f };
+ for (float m : marks) {
+ float x = left + (zero + m / 5f) * pitch + pitch / 2f;
+ label(c, String.format(Locale.US, "%.0f", m), x, h * 0.83f, h * 0.035f, Paint.Align.CENTER);
+ }
+ }
+
+ private void drawLeft(Canvas c, float w, float h) {
+ float x0 = w * 0.035f;
+ label(c, "CHARGE", x0, h * 0.10f, h * 0.045f, Paint.Align.LEFT);
+ // A vertical bar of 20 segments, red below 20 %.
+ float top = h * 0.16f, bottom = h * 0.82f, bw = w * 0.035f;
+ int segments = 20;
+ float pitch = (bottom - top) / segments;
+ int on = v.socPercent == null ? 0 : Math.round(Math.max(0f, Math.min(100f, v.socPercent)) / 5f);
+ boolean low = v.socPercent != null && v.socPercent < 20f;
+ for (int i = 0; i < segments; i++) {
+ float y = bottom - (i + 1) * pitch;
+ bar(c, x0, y + pitch * 0.2f, x0 + bw, y + pitch * 0.8f, i < on, low ? warn : lit);
+ }
+
+ float right = w * 0.235f;
+ String soc = v.socPercent == null ? "---" : String.format(Locale.US, "%.0f", v.socPercent);
+ drawDigits(c, soc, 3, right, h * 0.16f, h * 0.17f, low ? warn : lit);
+ label(c, "%", right + h * 0.015f, h * 0.33f, h * 0.05f, Paint.Align.LEFT);
+ label(c, "RANGE", x0 + bw + w * 0.03f, h * 0.47f, h * 0.04f, Paint.Align.LEFT);
+ String range = v.rangeKm == null ? "---" : String.format(Locale.US, "%.0f", v.rangeKm);
+ drawDigits(c, range, 3, right, h * 0.51f, h * 0.12f, lit);
+ label(c, "KM", right + h * 0.015f, h * 0.63f, h * 0.04f, Paint.Align.LEFT);
+ }
+
+ private void drawRight(Canvas c, float w, float h) {
+ float right = w * 0.925f, x0 = w * 0.77f;
+ label(c, "ECONOMY", x0, h * 0.10f, h * 0.045f, Paint.Align.LEFT);
+ drawDigits(c, oneDecimal(v.usage5Min), 4, right, h * 0.16f, h * 0.17f, lit);
+ label(c, "KWH/100KM 5 MIN", x0, h * 0.40f, h * 0.035f, Paint.Align.LEFT);
+ label(c, "AVG", x0, h * 0.535f, h * 0.04f, Paint.Align.LEFT);
+ drawDigits(c, oneDecimal(v.usageTrip), 4, right, h * 0.47f, h * 0.085f, lit);
+ label(c, "TRIP", x0, h * 0.675f, h * 0.04f, Paint.Align.LEFT);
+ drawDigits(c, oneDecimal(v.tripKm), 4, right, h * 0.61f, h * 0.085f, lit);
+ label(c, "KM", right + h * 0.015f, h * 0.695f, h * 0.035f, Paint.Align.LEFT);
+ }
+
+ private void drawBottom(Canvas c, float w, float h, float d) {
+ bezel.setStrokeWidth(d);
+ c.drawLine(w * 0.03f, h * 0.875f, w * 0.97f, h * 0.875f, bezel);
+ float top = h * 0.90f, dh = h * 0.07f, base = top + dh;
+
+ label(c, "TEMP", w * 0.035f, base, h * 0.04f, Paint.Align.LEFT);
+ String temp = v.climateCelsius == null ? "--" : String.format(Locale.US, "%.0f", v.climateCelsius);
+ drawDigits(c, temp, 2, w * 0.035f + h * 0.30f, top, dh, lit);
+ label(c, "°C", w * 0.035f + h * 0.315f, base, h * 0.04f, Paint.Align.LEFT);
+
+ // The clock: two pairs of digits and a colon.
+ String clock = v.clock.length() == 5 ? v.clock : "--:--";
+ float cx = w / 2f, cell = cellWidth(dh), colon = dh * 0.35f;
+ drawDigits(c, clock.substring(0, 2), 2, cx - colon / 2f, top, dh, lit);
+ drawDigits(c, clock.substring(3, 5), 2, cx + colon / 2f + 2 * cell, top, dh, lit);
+ float t = dh * 0.13f;
+ glowRect(c, cx - t / 2f + SKEW * dh * 0.3f, top + dh * 0.28f, t, lit);
+ glowRect(c, cx - t / 2f - SKEW * dh * 0.1f, top + dh * 0.66f, t, lit);
+
+ String odo = v.odometerKm == null ? "------" : String.format(Locale.US, "%06.0f", v.odometerKm);
+ float right = w * 0.925f;
+ label(c, "ODO", right - 6 * cell - h * 0.02f, base, h * 0.04f, Paint.Align.RIGHT);
+ drawDigits(c, odo, 6, right, top, dh, lit);
+ label(c, "KM", right + h * 0.015f, base, h * 0.035f, Paint.Align.LEFT);
+ }
+
+ private void drawGears(Canvas c, float cx, float top, float size) {
+ String[] letters = { "P", "R", "N", "D" };
+ float pitch = size * 1.6f, x0 = cx - pitch * 1.5f;
+ label.setTextSize(size * 0.8f);
+ label.setTextAlign(Paint.Align.CENTER);
+ for (int i = 0; i < letters.length; i++) {
+ float x = x0 + i * pitch;
+ boolean on = v.gear != null && v.gear == i + 1;
+ rect.set(x - size * 0.6f, top, x + size * 0.6f, top + size * 1.1f);
+ if (on) {
+ glow.setColor(AccentColor.withAlpha(lit, 0x40));
+ glow.setStrokeWidth(size * 0.25f);
+ c.drawRoundRect(rect, size * 0.12f, size * 0.12f, glow);
+ litPaint.setColor(lit);
+ c.drawRoundRect(rect, size * 0.12f, size * 0.12f, litPaint);
+ label.setColor(0xFF000000);
+ } else {
+ c.drawRoundRect(rect, size * 0.12f, size * 0.12f, ghost);
+ label.setColor(AccentColor.withAlpha(lit, 0x40));
+ }
+ c.drawText(letters[i], x, top + size * 0.85f, label);
+ }
+ label.setColor(amber);
+ }
+
+ // ---------- Primitives ----------
+
+ private static String oneDecimal(Double value) {
+ return value == null || value.isNaN() ? "---" : String.format(Locale.US, "%.1f", value);
+ }
+
+ private void label(Canvas c, String text, float x, float baseline, float size, Paint.Align align) {
+ label.setTextSize(size);
+ label.setTextAlign(align);
+ c.drawText(text, x, baseline, label);
+ }
+
+ private static float cellWidth(float dh) {
+ return dh * 0.72f;
+ }
+
+ /** Text as seven-segment digits in {@code cells} cells ending at {@code right}. */
+ private void drawDigits(Canvas c, String text, int cells, float right, float top, float dh, int colour) {
+ char[] chars = SevenSegment.cells(text, cells);
+ boolean[] dots = SevenSegment.dots(text, cells);
+ float cw = cellWidth(dh);
+ for (int i = 0; i < cells; i++) {
+ float x = right - (cells - i) * cw;
+ drawDigit(c, x, top, dh, SevenSegment.mask(chars[i]), dots[i], colour);
+ }
+ }
+
+ private void drawDigit(Canvas c, float x, float y, float dh, int mask, boolean dot, int colour) {
+ float wd = dh * 0.5f, t = dh * 0.12f, g = t * 0.18f;
+ float yMid = y + dh / 2f, yBottom = y + dh;
+ segment(c, true, x + g, x + wd - g, y + t / 2f, t, y, dh, (mask & SevenSegment.A) != 0, colour);
+ segment(c, true, x + g, x + wd - g, yMid, t, y, dh, (mask & SevenSegment.G) != 0, colour);
+ segment(c, true, x + g, x + wd - g, yBottom - t / 2f, t, y, dh, (mask & SevenSegment.D) != 0, colour);
+ segment(c, false, y + t / 2f + g, yMid - g, x + t / 2f, t, y, dh, (mask & SevenSegment.F) != 0, colour);
+ segment(c, false, yMid + g, yBottom - t / 2f - g, x + t / 2f, t, y, dh, (mask & SevenSegment.E) != 0, colour);
+ segment(c, false, y + t / 2f + g, yMid - g, x + wd - t / 2f, t, y, dh, (mask & SevenSegment.B) != 0, colour);
+ segment(c, false, yMid + g, yBottom - t / 2f - g, x + wd - t / 2f, t, y, dh, (mask & SevenSegment.C) != 0, colour);
+ float dx = x + wd + dh * 0.06f + SKEW * t;
+ if (dot) glowRect(c, dx, yBottom - t, t, colour);
+ else { rect.set(dx, yBottom - t, dx + t, yBottom); c.drawRect(rect, ghost); }
+ }
+
+ /**
+ * One segment, a hexagon from {@code a} to {@code b} along its axis at {@code across}; slanted
+ * to the right with the digit's height (top {@code y}, height {@code dh}).
+ */
+ private void segment(Canvas c, boolean horizontal, float a, float b, float across, float t,
+ float y, float dh, boolean on, int colour) {
+ float h2 = t / 2f;
+ path.reset();
+ if (horizontal) {
+ point(true, a, across, y, dh);
+ point(false, a + h2, across - h2, y, dh);
+ point(false, b - h2, across - h2, y, dh);
+ point(false, b, across, y, dh);
+ point(false, b - h2, across + h2, y, dh);
+ point(false, a + h2, across + h2, y, dh);
+ } else {
+ point(true, across, a, y, dh);
+ point(false, across + h2, a + h2, y, dh);
+ point(false, across + h2, b - h2, y, dh);
+ point(false, across, b, y, dh);
+ point(false, across - h2, b - h2, y, dh);
+ point(false, across - h2, a + h2, y, dh);
+ }
+ path.close();
+ if (!on) {
+ c.drawPath(path, ghost);
+ return;
+ }
+ glow.setColor(AccentColor.withAlpha(colour, 0x38));
+ glow.setStrokeWidth(t * 0.9f);
+ c.drawPath(path, glow);
+ litPaint.setColor(colour);
+ c.drawPath(path, litPaint);
+ }
+
+ private void point(boolean first, float px, float py, float y, float dh) {
+ float x = px + SKEW * (y + dh - py);
+ if (first) path.moveTo(x, py); else path.lineTo(x, py);
+ }
+
+ private void bar(Canvas c, float l, float t, float r, float b, boolean on, int colour) {
+ rect.set(l, t, r, b);
+ if (!on) {
+ c.drawRect(rect, ghost);
+ return;
+ }
+ glow.setColor(AccentColor.withAlpha(colour, 0x38));
+ glow.setStrokeWidth(Math.min(r - l, b - t) * 0.5f);
+ c.drawRect(rect, glow);
+ litPaint.setColor(colour);
+ c.drawRect(rect, litPaint);
+ }
+
+ private void glowRect(Canvas c, float x, float y, float size, int colour) {
+ bar(c, x, y, x + size, y + size, true, colour);
+ }
+}
diff --git a/app/src/main/java/com/evsuite/abrp/SevenSegment.java b/app/src/main/java/com/evsuite/abrp/SevenSegment.java
new file mode 100644
index 0000000..084de7e
--- /dev/null
+++ b/app/src/main/java/com/evsuite/abrp/SevenSegment.java
@@ -0,0 +1,64 @@
+package com.evsuite.abrp;
+
+/**
+ * Seven-segment digits for the Retro style: which segments a character lights, and how text
+ * is laid out in cells. Pure Java so it can be tested off the car.
+ *
+ * Segments as bits: a (top) 1, b (upper right) 2, c (lower right) 4, d (bottom) 8, e (lower
+ * left) 16, f (upper left) 32, g (middle) 64.
+ */
+final class SevenSegment {
+
+ static final int A = 1, B = 2, C = 4, D = 8, E = 16, F = 32, G = 64;
+ static final int ALL = 127;
+
+ private static final int[] DIGITS = {
+ A | B | C | D | E | F, // 0
+ B | C, // 1
+ A | B | D | E | G, // 2
+ A | B | C | D | G, // 3
+ B | C | F | G, // 4
+ A | C | D | F | G, // 5
+ A | C | D | E | F | G, // 6
+ A | B | C, // 7
+ ALL, // 8
+ A | B | C | D | F | G, // 9
+ };
+
+ private SevenSegment() {}
+
+ /** The segments a character lights: digits and '-'; anything else lights none. */
+ static int mask(char c) {
+ if (c >= '0' && c <= '9') return DIGITS[c - '0'];
+ if (c == '-') return G;
+ return 0;
+ }
+
+ /**
+ * The text as digit cells, right-aligned in {@code cells}: each cell's character, with
+ * '.' folded into the cell before it (see {@link #dots}). Longer text keeps its right end.
+ */
+ static char[] cells(String text, int cells) {
+ char[] out = new char[cells];
+ java.util.Arrays.fill(out, ' ');
+ String digits = text == null ? "" : text.replace(".", "");
+ int start = Math.max(0, digits.length() - cells);
+ for (int i = start; i < digits.length(); i++) out[cells - (digits.length() - i)] = digits.charAt(i);
+ return out;
+ }
+
+ /** For each cell of {@link #cells}, whether a decimal point follows it. */
+ static boolean[] dots(String text, int cells) {
+ boolean[] out = new boolean[cells];
+ if (text == null) return out;
+ // From the right: a '.' marks the digit to its left.
+ int cell = cells;
+ boolean pending = false;
+ for (int i = text.length() - 1; i >= 0; i--) {
+ if (text.charAt(i) == '.') { pending = true; continue; }
+ if (--cell < 0) break;
+ if (pending) { out[cell] = true; pending = false; }
+ }
+ return out;
+ }
+}
diff --git a/app/src/main/res/layout/pane_futuristic.xml b/app/src/main/res/layout/pane_futuristic.xml
new file mode 100644
index 0000000..062a2df
--- /dev/null
+++ b/app/src/main/res/layout/pane_futuristic.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
diff --git a/app/src/main/res/layout/pane_retro.xml b/app/src/main/res/layout/pane_retro.xml
new file mode 100644
index 0000000..74d06c1
--- /dev/null
+++ b/app/src/main/res/layout/pane_retro.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml
index 6fdc37f..5b2e4b6 100644
--- a/app/src/main/res/values/colors.xml
+++ b/app/src/main/res/values/colors.xml
@@ -21,4 +21,11 @@
#28FFFFFF
#121212
#00000000
+
+ #4DF5D0
+ #8CFF6A
+ #FFB13D
+ #FF5C4D
+ #060B0A
+ #1E2B28
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 0ea84a3..6b199b6 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -128,6 +128,8 @@
TOTAL %1$s
TRIP %1$s
Cockpit
+ Retro
+ Futuristic
BATTERY
USAGE · 5 MIN
kWh/100 km
diff --git a/app/src/test/java/com/evsuite/abrp/SevenSegmentTest.java b/app/src/test/java/com/evsuite/abrp/SevenSegmentTest.java
new file mode 100644
index 0000000..92e7fb5
--- /dev/null
+++ b/app/src/test/java/com/evsuite/abrp/SevenSegmentTest.java
@@ -0,0 +1,35 @@
+package com.evsuite.abrp;
+
+import static org.junit.Assert.assertArrayEquals;
+import static org.junit.Assert.assertEquals;
+
+import org.junit.Test;
+
+/** Seven-segment digits for the Retro style — pure JVM. */
+public class SevenSegmentTest {
+
+ @Test
+ public void digitsLightTheRightSegments() {
+ assertEquals(SevenSegment.ALL, SevenSegment.mask('8'));
+ assertEquals(SevenSegment.B | SevenSegment.C, SevenSegment.mask('1'));
+ assertEquals(SevenSegment.ALL & ~SevenSegment.G, SevenSegment.mask('0'));
+ assertEquals(SevenSegment.G, SevenSegment.mask('-'));
+ assertEquals(0, SevenSegment.mask(' '));
+ assertEquals(0, SevenSegment.mask('x'));
+ }
+
+ @Test
+ public void textIsRightAligned() {
+ assertArrayEquals(new char[] { ' ', '8', '7' }, SevenSegment.cells("87", 3));
+ assertArrayEquals(new char[] { '1', '2', '3' }, SevenSegment.cells("9123", 3));
+ assertArrayEquals(new char[] { ' ', ' ', ' ' }, SevenSegment.cells(null, 3));
+ }
+
+ @Test
+ public void decimalPointsFoldIntoTheDigitBefore() {
+ assertArrayEquals(new char[] { '1', '5', '8' }, SevenSegment.cells("15.8", 3));
+ assertArrayEquals(new boolean[] { false, true, false }, SevenSegment.dots("15.8", 3));
+ assertArrayEquals(new boolean[] { false, false, true, false }, SevenSegment.dots("5.8", 4));
+ assertArrayEquals(new boolean[] { false, false }, SevenSegment.dots("42", 2));
+ }
+}
diff --git a/screenshots/nilsmg4-2.23.0-futuristic.png b/screenshots/nilsmg4-2.23.0-futuristic.png
new file mode 100644
index 0000000..bac1274
Binary files /dev/null and b/screenshots/nilsmg4-2.23.0-futuristic.png differ
diff --git a/screenshots/nilsmg4-2.23.0-retro.png b/screenshots/nilsmg4-2.23.0-retro.png
new file mode 100644
index 0000000..3d3179b
Binary files /dev/null and b/screenshots/nilsmg4-2.23.0-retro.png differ