App 2.23.0: Overview styles Retro and Futuristic; read every 0.2 s
Retro: an 80s vacuum fluorescent cluster with seven-segment digits, bar graphs and a lit P R N D selector. Futuristic: a hairline HUD with an animated energy flow between battery and wheel, a turning speed ring and a power arc. Both show unread values as dashes. The Overview now reads the car five times a second. Emulator only. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LhzAVaUyqcw5PkGjz4mWW6
This commit is contained in:
@@ -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 |
|
||||
|
||||
@@ -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 |
|
||||
|
||||
@@ -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
|
||||
|
||||
+2
-2
@@ -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.
|
||||
|
||||
@@ -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 = "";
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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.
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- The Overview as a holographic HUD (Settings → Overview style → Futuristic), drawn by FlowView. -->
|
||||
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" android:paddingStart="24dp" android:paddingEnd="24dp" android:paddingTop="12dp" android:paddingBottom="12dp">
|
||||
<com.evsuite.abrp.FlowView android:id="@+id/flow_view" android:layout_width="match_parent" android:layout_height="match_parent" />
|
||||
</FrameLayout>
|
||||
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- The Overview as an 80s digital cluster (Settings → Overview style → Retro), drawn by RetroView. -->
|
||||
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" android:paddingStart="24dp" android:paddingEnd="24dp" android:paddingTop="12dp" android:paddingBottom="12dp">
|
||||
<com.evsuite.abrp.RetroView android:id="@+id/retro_view" android:layout_width="match_parent" android:layout_height="match_parent" />
|
||||
</FrameLayout>
|
||||
@@ -21,4 +21,11 @@
|
||||
<color name="ev_pressed_strong">#28FFFFFF</color>
|
||||
<color name="ev_icon_tile">#121212</color>
|
||||
<color name="ev_transparent">#00000000</color>
|
||||
<!-- The Retro Overview style: a vacuum fluorescent display behind smoked glass. -->
|
||||
<color name="retro_lit">#4DF5D0</color>
|
||||
<color name="retro_regen">#8CFF6A</color>
|
||||
<color name="retro_label">#FFB13D</color>
|
||||
<color name="retro_warn">#FF5C4D</color>
|
||||
<color name="retro_panel">#060B0A</color>
|
||||
<color name="retro_bezel">#1E2B28</color>
|
||||
</resources>
|
||||
|
||||
@@ -128,6 +128,8 @@
|
||||
<string name="dash_total">TOTAL %1$s</string>
|
||||
<string name="dash_trip">TRIP %1$s</string>
|
||||
<string name="overview_style_cockpit">Cockpit</string>
|
||||
<string name="overview_style_retro">Retro</string>
|
||||
<string name="overview_style_futuristic">Futuristic</string>
|
||||
<string name="cockpit_battery">BATTERY</string>
|
||||
<string name="cockpit_usage">USAGE · 5 MIN</string>
|
||||
<string name="cockpit_usage_unit">kWh/100 km</string>
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 209 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 234 KiB |
Reference in New Issue
Block a user