App 2.26.0: Overview styles Minimal, Terminal, Aviation, Nixie, Chronograph
Five more styles that differ in arrangement as well as look: the speed alone with hairlines; a green text terminal with ASCII bars; a flight display with tapes and a horizon that pitches with power; nixie tubes on walnut and brass; a watch face with sub-dials. They share a new base, ClusterCanvasView. 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 |
|
| 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, classic or in the accent colour), Futuristic (`FlowView`, animated energy flow), Sci-fi (`SciFiView`, starship console), Cartoon (`CartoonView`), Blueprint (`BlueprintView`, top-view drawing with tyre pressures), MG (`OctagonView`, octagonal dials after the classic MG dashboards); the drawn styles share `ClusterValues` |
|
| 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, classic or in the accent colour), Futuristic (`FlowView`, animated energy flow), Sci-fi (`SciFiView`, starship console), Cartoon (`CartoonView`), Blueprint (`BlueprintView`, top-view drawing with tyre pressures), MG (`OctagonView`, octagonal dials after the classic MG dashboards), and on `ClusterCanvasView`: Minimal, Terminal, Aviation, Nixie tubes, Chronograph; the drawn styles share `ClusterValues` |
|
||||||
| 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 |
|
||||||
|
|||||||
@@ -79,7 +79,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, climate setpoint) and tyre pressures. Read five times a second. Looks to choose from in Settings → Overview style: Tiles, Gauges, Cockpit, Retro (classic turquoise or in your accent colour), Futuristic, Sci-fi, Cartoon, Blueprint and MG |
|
| **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. Looks to choose from in Settings → Overview style: Tiles, Gauges, Cockpit, Retro (classic turquoise or in your accent colour), Futuristic, Sci-fi, Cartoon, Blueprint, MG, Minimal, Terminal, Aviation, Nixie tubes and Chronograph |
|
||||||
| **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 |
|
||||||
|
|||||||
@@ -131,6 +131,26 @@ 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-24 — App 2.26.0 (branch `styles-retro-future`): five more Overview styles, chosen to
|
||||||
|
differ in arrangement as much as in look, on a new base class `ClusterCanvasView` (values,
|
||||||
|
easing, null-safe formatting, animating only while something moves and the view is shown).
|
||||||
|
**Minimal** (`MinimalView`): the speed alone, very large and thin; power as a short line
|
||||||
|
growing left (green, regenerating) or right from the centre; the battery as a hairline along
|
||||||
|
the bottom edge. **Terminal** (`TerminalView`): text only, green monospace with scan lines —
|
||||||
|
rows with ASCII bars, the gear as [D], tyres, a prompt with a cursor that blinks as readings
|
||||||
|
redraw it; a low battery row turns amber. **Aviation** (`AviationView`): a primary flight
|
||||||
|
display — speed and battery on scrolling tapes with readout boxes (blank, not centred on 0,
|
||||||
|
when unread), a horizon that pitches with power (up drawing, down regenerating) with a kW
|
||||||
|
ladder and the aircraft symbol, annunciators on top, data boxes below. **Nixie tubes**
|
||||||
|
(`NixieView`): orange glowing digits in glass tubes with the unlit numerals behind and a mesh,
|
||||||
|
brass name plates on walnut; speed, gear and battery large, power, range, usage and clock
|
||||||
|
smaller (`SevenSegment` lays the text out in tubes). **Chronograph** (`ChronographView`): a
|
||||||
|
watch face — the speed on the main hand over a bezel scale, sub-dials for battery, power and
|
||||||
|
usage, the gear in a window, engraved columns either side (its radial gradient is a Canvas
|
||||||
|
shader made in `onSizeChanged`). Checked on the emulator with a throwaway build cycling the
|
||||||
|
five styles with sample values (drawing and regenerating;
|
||||||
|
`screenshots/nilsmg4-2.26.0-minimal.png`, `-terminal`, `-aviation`, `-nixie`, `-chronograph`)
|
||||||
|
and without data; no crash. Not tried on the car.
|
||||||
- 2026-10-24 — App 2.25.0 (branch `styles-retro-future`): Overview style "MG" (`OctagonView`),
|
- 2026-10-24 — App 2.25.0 (branch `styles-retro-future`): Overview style "MG" (`OctagonView`),
|
||||||
after the classic MG dashboards whose dials sat in octagonal bezels — not a copy of the MG4's
|
after the classic MG dashboards whose dials sat in octagonal bezels — not a copy of the MG4's
|
||||||
own driver display, and without MG's octagon logo. Three octagonal dials with chrome bezels,
|
own driver display, and without MG's octagon logo. Three octagonal dials with chrome bezels,
|
||||||
|
|||||||
+2
-2
@@ -34,8 +34,8 @@ android {
|
|||||||
applicationId "com.stinnesbeck.nilsmg4"
|
applicationId "com.stinnesbeck.nilsmg4"
|
||||||
minSdk 27
|
minSdk 27
|
||||||
targetSdk 36
|
targetSdk 36
|
||||||
versionCode 119
|
versionCode 120
|
||||||
versionName "2.25.0"
|
versionName "2.26.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.
|
||||||
|
|||||||
@@ -0,0 +1,233 @@
|
|||||||
|
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;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The Aviation Overview style: a pilot's primary flight display.
|
||||||
|
*
|
||||||
|
* The speed on a scrolling tape at the left and the battery on one at the right (where the
|
||||||
|
* altitude would be), each with a readout box. In the middle, a horizon that "pitches" with the
|
||||||
|
* power: drawing power raises the nose (the horizon drops), regenerating lowers it; a ladder of
|
||||||
|
* kW marks and the yellow aircraft symbol over it. Green annunciators at the top (gear, power,
|
||||||
|
* clock), boxes of range, trip, usage, climate and odometer along the bottom. Unread values
|
||||||
|
* show "—" and leave the horizon level.
|
||||||
|
*
|
||||||
|
* The tapes and the horizon ease to new values and redraw only until settled, only on screen.
|
||||||
|
*/
|
||||||
|
final class AviationView extends ClusterCanvasView {
|
||||||
|
|
||||||
|
private static final long FRAME_MS = 33;
|
||||||
|
|
||||||
|
private final Paint fill = new Paint(Paint.ANTI_ALIAS_FLAG);
|
||||||
|
private final Paint line = new Paint(Paint.ANTI_ALIAS_FLAG);
|
||||||
|
private final Paint text = new Paint(Paint.ANTI_ALIAS_FLAG);
|
||||||
|
private final Path path = new Path();
|
||||||
|
private final RectF rect = new RectF();
|
||||||
|
private final int sky = 0xFF1E4C80, ground = 0xFF5A3A1E, white = 0xFFF2F4F7, green = 0xFF3CE07A,
|
||||||
|
cyan = 0xFF4FD8F0, magenta = 0xFFE05CE0, yellow = 0xFFFFD21F, tape = 0xCC1A1F26, amber = 0xFFFFB000;
|
||||||
|
private float speedShown = Float.NaN, socShown = Float.NaN, powerShown = Float.NaN;
|
||||||
|
|
||||||
|
AviationView(Context context) {
|
||||||
|
super(context);
|
||||||
|
line.setStyle(Paint.Style.STROKE);
|
||||||
|
line.setStrokeCap(Paint.Cap.ROUND);
|
||||||
|
text.setTypeface(Typeface.create("sans-serif-condensed", Typeface.BOLD));
|
||||||
|
text.setFontFeatureSettings("tnum");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onDraw(Canvas c) {
|
||||||
|
super.onDraw(c);
|
||||||
|
float w = getWidth(), h = getHeight();
|
||||||
|
if (w <= 0 || h <= 0) return;
|
||||||
|
speedShown = ease(speedShown, v.speedKmh, 180f);
|
||||||
|
socShown = ease(socShown, v.socPercent, 100f);
|
||||||
|
powerShown = ease(powerShown, v.powerKw, 200f);
|
||||||
|
boolean moving = (v.speedKmh != null && speedShown != v.speedKmh)
|
||||||
|
|| (v.socPercent != null && socShown != v.socPercent)
|
||||||
|
|| (v.powerKw != null && powerShown != v.powerKw);
|
||||||
|
|
||||||
|
c.drawColor(0xFF05070A);
|
||||||
|
float top = h * 0.10f, bottom = h * 0.84f;
|
||||||
|
float tapeW = w * 0.10f;
|
||||||
|
float leftTape = w * 0.04f, rightTape = w - w * 0.04f - tapeW;
|
||||||
|
drawHorizon(c, leftTape + tapeW + 16f * dp, top, rightTape - 16f * dp, bottom);
|
||||||
|
drawTape(c, leftTape, top, tapeW, bottom, v.speedKmh == null ? null : speedShown, 0f, 250f, 10f, 20f,
|
||||||
|
fmt(v.speedKmh, "%.0f", "—"), "KM/H", true);
|
||||||
|
drawTape(c, rightTape, top, tapeW, bottom, v.socPercent == null ? null : socShown, 0f, 100f, 5f, 10f,
|
||||||
|
fmt(v.socPercent, "%.0f", "—"), "BATT %", false);
|
||||||
|
drawAnnunciators(c, w, top);
|
||||||
|
drawBoxes(c, w, h, bottom);
|
||||||
|
keepAnimating(moving, FRAME_MS);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Sky and ground split by a horizon that moves with the power, a kW ladder, the aircraft. */
|
||||||
|
private void drawHorizon(Canvas c, float l, float t, float r, float b) {
|
||||||
|
float cx = (l + r) / 2f, cy = (t + b) / 2f, half = (b - t) / 2f;
|
||||||
|
// 150 kW lifts the nose by 0.8 of the half height; regeneration lowers it, 50 kW as far.
|
||||||
|
float pitch = v.powerKw == null ? 0f
|
||||||
|
: powerShown >= 0f ? Math.min(150f, powerShown) / 150f : Math.max(-50f, powerShown) / 50f * 0.6f;
|
||||||
|
float horizon = cy + pitch * half * 0.8f;
|
||||||
|
float perKw = half * 0.8f / 150f;
|
||||||
|
c.save();
|
||||||
|
rect.set(l, t, r, b);
|
||||||
|
c.clipRect(rect);
|
||||||
|
fill.setColor(sky);
|
||||||
|
c.drawRect(l, t, r, horizon, fill);
|
||||||
|
fill.setColor(ground);
|
||||||
|
c.drawRect(l, horizon, r, b, fill);
|
||||||
|
line.setColor(white);
|
||||||
|
line.setStrokeWidth(2.5f * dp);
|
||||||
|
c.drawLine(l, horizon, r, horizon, line);
|
||||||
|
// The ladder: a mark every 25 kW above the horizon (drawing), below it (regenerating).
|
||||||
|
text.setTextSize(13f * dp);
|
||||||
|
for (int kw = -50; kw <= 150; kw += 25) {
|
||||||
|
if (kw == 0) continue;
|
||||||
|
float y = horizon - kw * (kw > 0 ? perKw : perKw * 150f / 50f * 0.6f);
|
||||||
|
float half2 = kw % 50 == 0 ? (r - l) * 0.10f : (r - l) * 0.05f;
|
||||||
|
line.setStrokeWidth(1.5f * dp);
|
||||||
|
c.drawLine(cx - half2, y, cx + half2, y, line);
|
||||||
|
if (kw % 50 == 0) {
|
||||||
|
text.setColor(white);
|
||||||
|
text.setTextAlign(Paint.Align.LEFT);
|
||||||
|
c.drawText(String.valueOf(kw), cx + half2 + 6f * dp, y + 5f * dp, text);
|
||||||
|
text.setTextAlign(Paint.Align.RIGHT);
|
||||||
|
c.drawText(String.valueOf(kw), cx - half2 - 6f * dp, y + 5f * dp, text);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
c.restore();
|
||||||
|
// The aircraft symbol, fixed: two wings and a dot, yellow outlined in black.
|
||||||
|
float wing = (r - l) * 0.14f;
|
||||||
|
for (int pass = 0; pass < 2; pass++) {
|
||||||
|
line.setColor(pass == 0 ? 0xFF000000 : yellow);
|
||||||
|
line.setStrokeWidth(pass == 0 ? 9f * dp : 5f * dp);
|
||||||
|
c.drawLine(cx - wing * 2f, cy, cx - wing, cy, line);
|
||||||
|
c.drawLine(cx - wing, cy, cx - wing, cy + wing * 0.25f, line);
|
||||||
|
c.drawLine(cx + wing * 2f, cy, cx + wing, cy, line);
|
||||||
|
c.drawLine(cx + wing, cy, cx + wing, cy + wing * 0.25f, line);
|
||||||
|
}
|
||||||
|
fill.setColor(yellow);
|
||||||
|
c.drawCircle(cx, cy, 5f * dp, fill);
|
||||||
|
// The power readout in magenta under the symbol.
|
||||||
|
text.setTextAlign(Paint.Align.CENTER);
|
||||||
|
text.setTextSize(22f * dp);
|
||||||
|
text.setColor(v.powerKw != null && v.powerKw < 0f ? cyan : magenta);
|
||||||
|
c.drawText(fmt(v.powerKw, "%.1f kW", "— kW"), cx, b - 14f * dp, text);
|
||||||
|
line.setColor(0xFF2A3038);
|
||||||
|
line.setStrokeWidth(2f * dp);
|
||||||
|
c.drawRect(l, t, r, b, line);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A vertical tape scrolling with its value: ticks every {@code minor}, numbers every
|
||||||
|
* {@code major}, and a readout box at the middle pointing in (right for the left tape).
|
||||||
|
*/
|
||||||
|
private void drawTape(Canvas c, float l, float t, float tw, float b, Float value, float min, float max,
|
||||||
|
float minor, float major, String readout, String title, boolean left) {
|
||||||
|
float cy = (t + b) / 2f, perUnit = (b - t) / (max - min) * (max > 150f ? 3.2f : 2.2f);
|
||||||
|
fill.setColor(tape);
|
||||||
|
c.drawRect(l, t, l + tw, b, fill);
|
||||||
|
c.save();
|
||||||
|
c.clipRect(l, t, l + tw, b);
|
||||||
|
float centre = value == null ? min : value;
|
||||||
|
text.setTextSize(16f * dp);
|
||||||
|
text.setColor(white);
|
||||||
|
line.setColor(white);
|
||||||
|
// No value, no scale: a tape centred on zero would read as 0.
|
||||||
|
for (float u = min; value != null && u <= max; u += minor) {
|
||||||
|
float y = cy - (u - centre) * perUnit;
|
||||||
|
if (y < t - 20f * dp || y > b + 20f * dp) continue;
|
||||||
|
boolean isMajor = Math.round(u) % Math.round(major) == 0;
|
||||||
|
float len = isMajor ? tw * 0.22f : tw * 0.12f;
|
||||||
|
line.setStrokeWidth(2f * dp);
|
||||||
|
float edge = left ? l + tw : l;
|
||||||
|
c.drawLine(edge, y, edge + (left ? -len : len), y, line);
|
||||||
|
if (isMajor) {
|
||||||
|
text.setTextAlign(left ? Paint.Align.RIGHT : Paint.Align.LEFT);
|
||||||
|
c.drawText(String.valueOf(Math.round(u)), left ? edge - len - 6f * dp : edge + len + 6f * dp, y + 6f * dp, text);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
c.restore();
|
||||||
|
// The readout box with its pointer.
|
||||||
|
boolean low = !left && v.socPercent != null && v.socPercent < 20f;
|
||||||
|
float bh = 44f * dp;
|
||||||
|
path.reset();
|
||||||
|
if (left) {
|
||||||
|
path.moveTo(l + 2f * dp, cy - bh / 2f);
|
||||||
|
path.lineTo(l + tw - 12f * dp, cy - bh / 2f);
|
||||||
|
path.lineTo(l + tw + 2f * dp, cy);
|
||||||
|
path.lineTo(l + tw - 12f * dp, cy + bh / 2f);
|
||||||
|
path.lineTo(l + 2f * dp, cy + bh / 2f);
|
||||||
|
} else {
|
||||||
|
path.moveTo(l + tw - 2f * dp, cy - bh / 2f);
|
||||||
|
path.lineTo(l + 12f * dp, cy - bh / 2f);
|
||||||
|
path.lineTo(l - 2f * dp, cy);
|
||||||
|
path.lineTo(l + 12f * dp, cy + bh / 2f);
|
||||||
|
path.lineTo(l + tw - 2f * dp, cy + bh / 2f);
|
||||||
|
}
|
||||||
|
path.close();
|
||||||
|
fill.setColor(0xFF000000);
|
||||||
|
c.drawPath(path, fill);
|
||||||
|
line.setColor(low ? amber : white);
|
||||||
|
line.setStrokeWidth(2f * dp);
|
||||||
|
c.drawPath(path, line);
|
||||||
|
text.setTextAlign(Paint.Align.CENTER);
|
||||||
|
text.setTextSize(30f * dp);
|
||||||
|
text.setColor(low ? amber : green);
|
||||||
|
c.drawText(readout, l + tw / 2f + (left ? -4f * dp : 4f * dp), cy + 11f * dp, text);
|
||||||
|
text.setTextSize(14f * dp);
|
||||||
|
text.setColor(cyan);
|
||||||
|
c.drawText(title, l + tw / 2f, t - 8f * dp, text);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** The annunciators: gear, drive state and the clock, green on black in boxes. */
|
||||||
|
private void drawAnnunciators(Canvas c, float w, float top) {
|
||||||
|
String gear = Gear.label(v.gear);
|
||||||
|
String state = v.powerKw == null ? "—" : v.powerKw < -0.3f ? "REGEN" : v.powerKw > 0.3f ? "DRIVE PWR" : "IDLE";
|
||||||
|
String[] items = { "GEAR " + (gear.isEmpty() ? "—" : gear), state, v.clock.isEmpty() ? "—" : v.clock };
|
||||||
|
float bw = w * 0.16f, gap = w * 0.02f, x = (w - 3 * bw - 2 * gap) / 2f, bt = 4f * dp, bb = top - 26f * dp;
|
||||||
|
if (bb - bt < 18f * dp) bb = bt + 22f * dp;
|
||||||
|
text.setTextSize((bb - bt) * 0.6f);
|
||||||
|
text.setTextAlign(Paint.Align.CENTER);
|
||||||
|
for (String item : items) {
|
||||||
|
line.setColor(0xFF2A3038);
|
||||||
|
line.setStrokeWidth(1.5f * dp);
|
||||||
|
c.drawRect(x, bt, x + bw, bb, line);
|
||||||
|
text.setColor(green);
|
||||||
|
c.drawText(item, x + bw / 2f, bb - (bb - bt) * 0.25f, text);
|
||||||
|
x += bw + gap;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Data boxes along the bottom, cyan labels over white values. */
|
||||||
|
private void drawBoxes(Canvas c, float w, float h, float top) {
|
||||||
|
String[][] boxes = {
|
||||||
|
{ "RANGE", fmt(v.rangeKm, "%.0f km", "—") },
|
||||||
|
{ "TRIP", fmt(v.tripKm, "%.1f km", "—") },
|
||||||
|
{ "USAGE 5 MIN", fmt(v.usage5Min, "%.1f", "—") },
|
||||||
|
{ "SINCE START", fmt(v.usageTrip, "%.1f", "—") },
|
||||||
|
{ "CLIMATE", fmt(v.climateCelsius, "%.0f °C", "—") },
|
||||||
|
{ "ODOMETER", fmt(v.odometerKm, "%,.0f", "—") },
|
||||||
|
};
|
||||||
|
float gap = 8f * dp, bt = top + 14f * dp, bb = h - 4f * dp;
|
||||||
|
float bw = (w - gap * (boxes.length - 1)) / boxes.length;
|
||||||
|
for (int i = 0; i < boxes.length; i++) {
|
||||||
|
float x = i * (bw + gap);
|
||||||
|
line.setColor(0xFF2A3038);
|
||||||
|
line.setStrokeWidth(1.5f * dp);
|
||||||
|
c.drawRect(x, bt, x + bw, bb, line);
|
||||||
|
text.setTextAlign(Paint.Align.CENTER);
|
||||||
|
text.setTextSize(12f * dp);
|
||||||
|
text.setColor(cyan);
|
||||||
|
c.drawText(boxes[i][0], x + bw / 2f, bt + 17f * dp, text);
|
||||||
|
text.setTextSize(Math.min(24f * dp, (bb - bt) * 0.42f));
|
||||||
|
text.setColor(white);
|
||||||
|
c.drawText(boxes[i][1], x + bw / 2f, bb - 10f * dp, text);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,231 @@
|
|||||||
|
package com.evsuite.abrp;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import android.graphics.Canvas;
|
||||||
|
import android.graphics.Paint;
|
||||||
|
import android.graphics.RadialGradient;
|
||||||
|
import android.graphics.RectF;
|
||||||
|
import android.graphics.Shader;
|
||||||
|
import android.graphics.Typeface;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The Chronograph Overview style: a watch face.
|
||||||
|
*
|
||||||
|
* One large dial, deep blue with a steel bezel carrying the speed scale; the long main hand is
|
||||||
|
* the speed. Three sub-dials inside it: battery at nine o'clock, power at three (charge zone
|
||||||
|
* below zero in green), 5-minute usage at six. A window under twelve shows the gear. Either side
|
||||||
|
* of the watch, engraved columns: range and trip on the left, time, climate and odometer on the
|
||||||
|
* right. Unread values draw no hand and "—".
|
||||||
|
*
|
||||||
|
* Hands ease to new values and redraw only until settled, only on screen. The gradient is a
|
||||||
|
* Canvas shader with fixed colours (not a drawable with theme colours, which Android 9 rejects).
|
||||||
|
*/
|
||||||
|
final class ChronographView extends ClusterCanvasView {
|
||||||
|
|
||||||
|
private static final long FRAME_MS = 33;
|
||||||
|
private static final float START = 135f, SWEEP = 270f, MAX_KMH = 200f;
|
||||||
|
|
||||||
|
private final Paint fill = new Paint(Paint.ANTI_ALIAS_FLAG);
|
||||||
|
private final Paint dial = new Paint(Paint.ANTI_ALIAS_FLAG);
|
||||||
|
private final Paint line = new Paint(Paint.ANTI_ALIAS_FLAG);
|
||||||
|
private final Paint text = new Paint(Paint.ANTI_ALIAS_FLAG);
|
||||||
|
private final RectF rect = new RectF();
|
||||||
|
private final int accent, steel = 0xFFC9CED3, steelDark = 0xFF6E757C, ivory = 0xFFF1EBDD,
|
||||||
|
green, red;
|
||||||
|
private float speedShown = Float.NaN, socShown = Float.NaN, powerShown = Float.NaN, usageShown = Float.NaN;
|
||||||
|
|
||||||
|
ChronographView(Context context) {
|
||||||
|
super(context);
|
||||||
|
accent = AccentColor.resolve(context, R.attr.evAccent);
|
||||||
|
green = context.getColor(R.color.ev_ok);
|
||||||
|
red = context.getColor(R.color.ev_error);
|
||||||
|
line.setStyle(Paint.Style.STROKE);
|
||||||
|
line.setStrokeCap(Paint.Cap.ROUND);
|
||||||
|
text.setTypeface(Typeface.create("serif", Typeface.NORMAL));
|
||||||
|
text.setFontFeatureSettings("tnum");
|
||||||
|
text.setTextAlign(Paint.Align.CENTER);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onSizeChanged(int w, int h, int oldW, int oldH) {
|
||||||
|
super.onSizeChanged(w, h, oldW, oldH);
|
||||||
|
float r = h * 0.47f;
|
||||||
|
if (r > 0f) dial.setShader(new RadialGradient(0f, 0f, r * 0.88f, 0xFF20365A, 0xFF0A1424, Shader.TileMode.CLAMP));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onDraw(Canvas c) {
|
||||||
|
super.onDraw(c);
|
||||||
|
float w = getWidth(), h = getHeight();
|
||||||
|
if (w <= 0 || h <= 0) return;
|
||||||
|
speedShown = ease(speedShown, v.speedKmh, MAX_KMH);
|
||||||
|
socShown = ease(socShown, v.socPercent, 100f);
|
||||||
|
powerShown = ease(powerShown, v.powerKw, 200f);
|
||||||
|
usageShown = ease(usageShown, v.usage5Min == null ? null : v.usage5Min.floatValue(), 30f);
|
||||||
|
boolean moving = (v.speedKmh != null && speedShown != v.speedKmh)
|
||||||
|
|| (v.socPercent != null && socShown != v.socPercent)
|
||||||
|
|| (v.powerKw != null && powerShown != v.powerKw)
|
||||||
|
|| (v.usage5Min != null && usageShown != v.usage5Min.floatValue());
|
||||||
|
|
||||||
|
float r = h * 0.47f, cx = w / 2f, cy = h / 2f;
|
||||||
|
drawCase(c, cx, cy, r);
|
||||||
|
drawScale(c, cx, cy, r);
|
||||||
|
|
||||||
|
// Sub-dials.
|
||||||
|
float sr = r * 0.24f;
|
||||||
|
subDial(c, cx - r * 0.45f, cy, sr, 0f, 100f, 25f, v.socPercent == null ? null : socShown,
|
||||||
|
"BATTERY", fmt(v.socPercent, "%.0f%%", "—"), 20f, false);
|
||||||
|
subDial(c, cx + r * 0.45f, cy, sr, -50f, 150f, 50f, v.powerKw == null ? null : powerShown,
|
||||||
|
"kW", fmt(v.powerKw, "%.1f", "—"), null, true);
|
||||||
|
subDial(c, cx, cy + r * 0.45f, sr, 0f, 30f, 10f, v.usage5Min == null ? null : usageShown,
|
||||||
|
"kWh/100", fmt(v.usage5Min, "%.1f", "—"), null, false);
|
||||||
|
|
||||||
|
// The gear window under twelve, the maker's line above it.
|
||||||
|
text.setTextSize(r * 0.065f);
|
||||||
|
text.setColor(ivory);
|
||||||
|
text.setLetterSpacing(0.25f);
|
||||||
|
c.drawText("NILS' MG4", cx, cy - r * 0.50f, text);
|
||||||
|
text.setTextSize(r * 0.04f);
|
||||||
|
c.drawText("ELECTRIC · CHRONOGRAPH", cx, cy - r * 0.43f, text);
|
||||||
|
text.setLetterSpacing(0f);
|
||||||
|
String gear = Gear.label(v.gear);
|
||||||
|
rect.set(cx - r * 0.07f, cy - r * 0.36f, cx + r * 0.07f, cy - r * 0.22f);
|
||||||
|
fill.setColor(ivory);
|
||||||
|
c.drawRect(rect, fill);
|
||||||
|
line.setColor(steelDark);
|
||||||
|
line.setStrokeWidth(1.5f * dp);
|
||||||
|
c.drawRect(rect, line);
|
||||||
|
text.setColor(0xFF111111);
|
||||||
|
text.setTextSize(r * 0.10f);
|
||||||
|
c.drawText(gear.isEmpty() ? " " : gear, cx, cy - r * 0.255f, text);
|
||||||
|
|
||||||
|
// The speed: the long main hand, and the value small above the hub.
|
||||||
|
text.setColor(ivory);
|
||||||
|
text.setTextSize(r * 0.075f);
|
||||||
|
c.drawText(fmt(v.speedKmh, "%.0f km/h", "— km/h"), cx, cy - r * 0.10f, text);
|
||||||
|
if (v.speedKmh != null) hand(c, cx, cy, r * 0.86f, r * 0.15f, angle(speedShown, 0f, MAX_KMH), r * 0.022f, steel, accent);
|
||||||
|
fill.setColor(steel);
|
||||||
|
c.drawCircle(cx, cy, r * 0.035f, fill);
|
||||||
|
fill.setColor(accent);
|
||||||
|
c.drawCircle(cx, cy, r * 0.015f, fill);
|
||||||
|
|
||||||
|
drawColumns(c, w, h, cx, r);
|
||||||
|
keepAnimating(moving, FRAME_MS);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static float angle(float value, float min, float max) {
|
||||||
|
return START + SWEEP * (Math.max(min, Math.min(max, value)) - min) / (max - min);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** The case and bezel in steel, the dial deep blue with a soft light in the middle. */
|
||||||
|
private void drawCase(Canvas c, float cx, float cy, float r) {
|
||||||
|
fill.setShader(null);
|
||||||
|
fill.setColor(steelDark);
|
||||||
|
c.drawCircle(cx, cy, r * 1.04f, fill);
|
||||||
|
fill.setColor(steel);
|
||||||
|
c.drawCircle(cx, cy, r, fill);
|
||||||
|
fill.setColor(0xFF2A2F35);
|
||||||
|
c.drawCircle(cx, cy, r * 0.90f, fill);
|
||||||
|
c.save();
|
||||||
|
c.translate(cx, cy);
|
||||||
|
c.drawCircle(0f, 0f, r * 0.88f, dial);
|
||||||
|
c.restore();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** The speed scale on the bezel ring: numbers every 20, ticks every 5; applied indices inside. */
|
||||||
|
private void drawScale(Canvas c, float cx, float cy, float r) {
|
||||||
|
text.setTextSize(r * 0.06f);
|
||||||
|
text.setColor(0xFF1A1D21);
|
||||||
|
for (int kmh = 0; kmh <= MAX_KMH; kmh += 5) {
|
||||||
|
double a = Math.toRadians(angle(kmh, 0f, MAX_KMH));
|
||||||
|
float cos = (float) Math.cos(a), sin = (float) Math.sin(a);
|
||||||
|
boolean major = kmh % 20 == 0;
|
||||||
|
line.setColor(major ? ivory : 0xAAF1EBDD);
|
||||||
|
line.setStrokeWidth(major ? r * 0.012f : r * 0.006f);
|
||||||
|
c.drawLine(cx + r * 0.87f * cos, cy + r * 0.87f * sin, cx + r * (major ? 0.79f : 0.83f) * cos,
|
||||||
|
cy + r * (major ? 0.79f : 0.83f) * sin, line);
|
||||||
|
if (major) {
|
||||||
|
c.drawText(String.valueOf(kmh), cx + r * 0.95f * cos, cy + r * 0.95f * sin + text.getTextSize() * 0.36f, text);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** A small dial: ticks, end labels, a zone, the hand, its caption and value. */
|
||||||
|
private void subDial(Canvas c, float cx, float cy, float r, float min, float max, float major,
|
||||||
|
Float value, String caption, String readout, Float warnBelow, boolean chargeZone) {
|
||||||
|
fill.setColor(0xFF0C1626);
|
||||||
|
c.drawCircle(cx, cy, r, fill);
|
||||||
|
line.setColor(steelDark);
|
||||||
|
line.setStrokeWidth(1.5f * dp);
|
||||||
|
c.drawCircle(cx, cy, r, line);
|
||||||
|
rect.set(cx - r * 0.82f, cy - r * 0.82f, cx + r * 0.82f, cy + r * 0.82f);
|
||||||
|
line.setStrokeWidth(r * 0.06f);
|
||||||
|
line.setStrokeCap(Paint.Cap.BUTT);
|
||||||
|
if (warnBelow != null) {
|
||||||
|
line.setColor(red);
|
||||||
|
c.drawArc(rect, START, SWEEP * (warnBelow - min) / (max - min), false, line);
|
||||||
|
}
|
||||||
|
if (chargeZone) {
|
||||||
|
line.setColor(green);
|
||||||
|
c.drawArc(rect, START, SWEEP * (0f - min) / (max - min), false, line);
|
||||||
|
}
|
||||||
|
line.setStrokeCap(Paint.Cap.ROUND);
|
||||||
|
line.setColor(ivory);
|
||||||
|
for (float u = min; u <= max + 0.01f; u += major / 2f) {
|
||||||
|
double a = Math.toRadians(angle(u, min, max));
|
||||||
|
boolean isMajor = Math.round(u * 10) % Math.round(major * 10) == 0;
|
||||||
|
line.setStrokeWidth(isMajor ? r * 0.04f : r * 0.02f);
|
||||||
|
float outer = r * 0.92f, inner = isMajor ? r * 0.72f : r * 0.80f;
|
||||||
|
c.drawLine(cx + outer * (float) Math.cos(a), cy + outer * (float) Math.sin(a),
|
||||||
|
cx + inner * (float) Math.cos(a), cy + inner * (float) Math.sin(a), line);
|
||||||
|
}
|
||||||
|
text.setColor(ivory);
|
||||||
|
text.setTextSize(r * 0.16f);
|
||||||
|
c.drawText(caption, cx, cy - r * 0.32f, text);
|
||||||
|
text.setTextSize(r * 0.24f);
|
||||||
|
c.drawText(readout, cx, cy + r * 0.55f, text);
|
||||||
|
if (value != null) hand(c, cx, cy, r * 0.78f, r * 0.12f, angle(value, min, max), r * 0.05f, ivory, accent);
|
||||||
|
fill.setColor(ivory);
|
||||||
|
c.drawCircle(cx, cy, r * 0.07f, fill);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** A hand: a tail behind the hub, a shaft, and a coloured tip. */
|
||||||
|
private void hand(Canvas c, float cx, float cy, float length, float tail, float deg, float width, int colour, int tip) {
|
||||||
|
double a = Math.toRadians(deg);
|
||||||
|
float cos = (float) Math.cos(a), sin = (float) Math.sin(a);
|
||||||
|
line.setColor(0x66000000);
|
||||||
|
line.setStrokeWidth(width * 1.6f);
|
||||||
|
c.drawLine(cx - tail * cos + 2f * dp, cy - tail * sin + 3f * dp, cx + length * cos + 2f * dp, cy + length * sin + 3f * dp, line);
|
||||||
|
line.setColor(colour);
|
||||||
|
line.setStrokeWidth(width);
|
||||||
|
c.drawLine(cx - tail * cos, cy - tail * sin, cx + length * cos, cy + length * sin, line);
|
||||||
|
line.setColor(tip);
|
||||||
|
c.drawLine(cx + length * 0.78f * cos, cy + length * 0.78f * sin, cx + length * cos, cy + length * sin, line);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Engraved columns either side of the watch. */
|
||||||
|
private void drawColumns(Canvas c, float w, float h, float cx, float r) {
|
||||||
|
float leftX = (cx - r * 1.04f) / 2f, rightX = w - leftX;
|
||||||
|
String[][] left = { { "RANGE", fmt(v.rangeKm, "%.0f km", "—") }, { "TRIP", fmt(v.tripKm, "%.1f km", "—") },
|
||||||
|
{ "SINCE START", fmt(v.usageTrip, "%.1f kWh/100", "—") } };
|
||||||
|
String[][] right = { { "TIME", v.clock.isEmpty() ? "—" : v.clock }, { "CLIMATE", fmt(v.climateCelsius, "%.0f °C", "—") },
|
||||||
|
{ "ODOMETER", fmt(v.odometerKm, "%,.0f km", "—") } };
|
||||||
|
column(c, leftX, h, left);
|
||||||
|
column(c, rightX, h, right);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void column(Canvas c, float x, float h, String[][] rows) {
|
||||||
|
float y = h * 0.26f;
|
||||||
|
for (String[] row : rows) {
|
||||||
|
text.setTextSize(12f * dp);
|
||||||
|
text.setLetterSpacing(0.25f);
|
||||||
|
text.setColor(0xFF9AA3AD);
|
||||||
|
c.drawText(row[0], x, y, text);
|
||||||
|
text.setLetterSpacing(0f);
|
||||||
|
text.setTextSize(30f * dp);
|
||||||
|
text.setColor(ivory);
|
||||||
|
c.drawText(row[1], x, y + 38f * dp, text);
|
||||||
|
y += h * 0.27f;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
package com.evsuite.abrp;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import android.view.View;
|
||||||
|
|
||||||
|
import java.util.Locale;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The plumbing the drawn Overview styles share: the latest {@link ClusterValues}, the density,
|
||||||
|
* needle easing and null-safe formatting ("—" for an unread value, never 0).
|
||||||
|
*/
|
||||||
|
abstract class ClusterCanvasView extends View {
|
||||||
|
|
||||||
|
protected ClusterValues v = new ClusterValues();
|
||||||
|
protected final float dp;
|
||||||
|
|
||||||
|
ClusterCanvasView(Context context) {
|
||||||
|
super(context);
|
||||||
|
dp = context.getResources().getDisplayMetrics().density;
|
||||||
|
}
|
||||||
|
|
||||||
|
void setValues(ClusterValues values) {
|
||||||
|
v = values == null ? new ClusterValues() : values;
|
||||||
|
invalidate();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** One step of a needle towards its value; jumps there when close or on the first value. */
|
||||||
|
static float ease(float shown, Float target, float span) {
|
||||||
|
if (target == null || !Float.isFinite(target)) return shown;
|
||||||
|
if (Float.isNaN(shown)) return target;
|
||||||
|
float next = shown + (target - shown) * 0.25f;
|
||||||
|
return Math.abs(target - next) < span * 0.002f ? target : next;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** The value in the pattern, or {@code none} when unread. */
|
||||||
|
static String fmt(Number value, String pattern, String none) {
|
||||||
|
if (value == null || Double.isNaN(value.doubleValue())) return none;
|
||||||
|
return String.format(Locale.US, pattern, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Keeps drawing while something moves, but only while the view is on screen. */
|
||||||
|
protected void keepAnimating(boolean moving, long frameMs) {
|
||||||
|
if (moving && isShown()) postInvalidateDelayed(frameMs);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -66,6 +66,8 @@ public class MainActivity extends AppCompatActivity {
|
|||||||
private CartoonView cartoonView;
|
private CartoonView cartoonView;
|
||||||
private BlueprintView blueprintView;
|
private BlueprintView blueprintView;
|
||||||
private OctagonView octagonView;
|
private OctagonView octagonView;
|
||||||
|
/** The styles built on {@link ClusterCanvasView}, fed together. */
|
||||||
|
private final java.util.List<ClusterCanvasView> canvasStyles = new java.util.ArrayList<>();
|
||||||
/** The pane of each Overview style, by its index in {@link #OVERVIEW_STYLES}. */
|
/** The pane of each Overview style, by its index in {@link #OVERVIEW_STYLES}. */
|
||||||
private View[] stylePanes;
|
private View[] stylePanes;
|
||||||
private View consumptionPane;
|
private View consumptionPane;
|
||||||
@@ -254,8 +256,13 @@ public class MainActivity extends AppCompatActivity {
|
|||||||
octagonView = new OctagonView(this, null);
|
octagonView = new OctagonView(this, null);
|
||||||
View sciFiPane = drawnPane(sciFiView), cartoonPane = drawnPane(cartoonView),
|
View sciFiPane = drawnPane(sciFiView), cartoonPane = drawnPane(cartoonView),
|
||||||
blueprintPane = drawnPane(blueprintView), mgPane = drawnPane(octagonView);
|
blueprintPane = drawnPane(blueprintView), mgPane = drawnPane(octagonView);
|
||||||
|
java.util.Collections.addAll(canvasStyles, new MinimalView(this), new TerminalView(this),
|
||||||
|
new AviationView(this), new NixieView(this), new ChronographView(this));
|
||||||
|
View[] canvasPanes = new View[canvasStyles.size()];
|
||||||
|
for (int i = 0; i < canvasPanes.length; i++) canvasPanes[i] = drawnPane(canvasStyles.get(i));
|
||||||
stylePanes = new View[] { vehiclePane, gaugesPane, cockpitPane, retroPane, retroPane,
|
stylePanes = new View[] { vehiclePane, gaugesPane, cockpitPane, retroPane, retroPane,
|
||||||
futuristicPane, sciFiPane, cartoonPane, blueprintPane, mgPane };
|
futuristicPane, sciFiPane, cartoonPane, blueprintPane, mgPane,
|
||||||
|
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(
|
||||||
@@ -270,6 +277,7 @@ public class MainActivity extends AppCompatActivity {
|
|||||||
overviewPage.addView(cartoonPane);
|
overviewPage.addView(cartoonPane);
|
||||||
overviewPage.addView(blueprintPane);
|
overviewPage.addView(blueprintPane);
|
||||||
overviewPage.addView(mgPane);
|
overviewPage.addView(mgPane);
|
||||||
|
for (View pane : canvasPanes) overviewPage.addView(pane);
|
||||||
consumptionPane = inflatePane(R.layout.pane_consumption);
|
consumptionPane = inflatePane(R.layout.pane_consumption);
|
||||||
chargingPane = inflatePane(R.layout.pane_charging);
|
chargingPane = inflatePane(R.layout.pane_charging);
|
||||||
settingsPane = inflatePane(R.layout.pane_settings);
|
settingsPane = inflatePane(R.layout.pane_settings);
|
||||||
@@ -1454,7 +1462,7 @@ public class MainActivity extends AppCompatActivity {
|
|||||||
|
|
||||||
/** The stored values of the Overview styles, in the order of the drop-down. */
|
/** The stored values of the Overview styles, in the order of the drop-down. */
|
||||||
private static final String[] OVERVIEW_STYLES = { "TILES", "GAUGES", "COCKPIT", "RETRO", "RETRO_ACCENT", "FUTURISTIC",
|
private static final String[] OVERVIEW_STYLES = { "TILES", "GAUGES", "COCKPIT", "RETRO", "RETRO_ACCENT", "FUTURISTIC",
|
||||||
"SCIFI", "CARTOON", "BLUEPRINT", "MG" };
|
"SCIFI", "CARTOON", "BLUEPRINT", "MG", "MINIMAL", "TERMINAL", "AVIATION", "NIXIE", "CHRONOGRAPH" };
|
||||||
|
|
||||||
/** The chosen style's index in {@link #OVERVIEW_STYLES}; anything unknown is Tiles. */
|
/** The chosen style's index in {@link #OVERVIEW_STYLES}; anything unknown is Tiles. */
|
||||||
private int overviewStyle() {
|
private int overviewStyle() {
|
||||||
@@ -1477,7 +1485,10 @@ public class MainActivity extends AppCompatActivity {
|
|||||||
getString(R.string.overview_style_cockpit), getString(R.string.overview_style_retro),
|
getString(R.string.overview_style_cockpit), getString(R.string.overview_style_retro),
|
||||||
getString(R.string.overview_style_retro_accent), getString(R.string.overview_style_futuristic),
|
getString(R.string.overview_style_retro_accent), getString(R.string.overview_style_futuristic),
|
||||||
getString(R.string.overview_style_scifi), getString(R.string.overview_style_cartoon),
|
getString(R.string.overview_style_scifi), getString(R.string.overview_style_cartoon),
|
||||||
getString(R.string.overview_style_blueprint), getString(R.string.overview_style_mg) };
|
getString(R.string.overview_style_blueprint), getString(R.string.overview_style_mg),
|
||||||
|
getString(R.string.overview_style_minimal), getString(R.string.overview_style_terminal),
|
||||||
|
getString(R.string.overview_style_aviation), getString(R.string.overview_style_nixie),
|
||||||
|
getString(R.string.overview_style_chronograph) };
|
||||||
bindChoices(settingsPane.findViewById(R.id.overview_style_choices), labels, overviewStyle(),
|
bindChoices(settingsPane.findViewById(R.id.overview_style_choices), labels, overviewStyle(),
|
||||||
position -> {
|
position -> {
|
||||||
prefs.edit().putString(PREF_OVERVIEW_STYLE, OVERVIEW_STYLES[position]).apply();
|
prefs.edit().putString(PREF_OVERVIEW_STYLE, OVERVIEW_STYLES[position]).apply();
|
||||||
@@ -1540,6 +1551,7 @@ public class MainActivity extends AppCompatActivity {
|
|||||||
cartoonView.setValues(v);
|
cartoonView.setValues(v);
|
||||||
blueprintView.setValues(v);
|
blueprintView.setValues(v);
|
||||||
octagonView.setValues(v);
|
octagonView.setValues(v);
|
||||||
|
for (ClusterCanvasView view : canvasStyles) view.setValues(v);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------- Cockpit (Overview style) ----------
|
// ---------- Cockpit (Overview style) ----------
|
||||||
|
|||||||
@@ -0,0 +1,92 @@
|
|||||||
|
package com.evsuite.abrp;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import android.graphics.Canvas;
|
||||||
|
import android.graphics.Paint;
|
||||||
|
import android.graphics.Typeface;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The Minimal Overview style: one number and almost nothing else.
|
||||||
|
*
|
||||||
|
* The speed, very large and thin, in the middle, the gear above it. Under it, power as a short
|
||||||
|
* line growing from the centre — right in the accent colour while drawing, left in green while
|
||||||
|
* regenerating. Along the bottom edge, the battery as a hairline filled to the charge, with the
|
||||||
|
* percentage and range at its ends. Power and clock sit small in the top corners, usage at the
|
||||||
|
* bottom. Unread values show "—" and draw no line.
|
||||||
|
*/
|
||||||
|
final class MinimalView extends ClusterCanvasView {
|
||||||
|
|
||||||
|
private final Paint big = new Paint(Paint.ANTI_ALIAS_FLAG);
|
||||||
|
private final Paint small = new Paint(Paint.ANTI_ALIAS_FLAG);
|
||||||
|
private final Paint bar = new Paint(Paint.ANTI_ALIAS_FLAG);
|
||||||
|
private final int accent, light, regen, warn, white, grey, track;
|
||||||
|
|
||||||
|
MinimalView(Context context) {
|
||||||
|
super(context);
|
||||||
|
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);
|
||||||
|
track = context.getColor(R.color.ev_outline);
|
||||||
|
big.setTypeface(Typeface.create("sans-serif-thin", Typeface.NORMAL));
|
||||||
|
big.setTextAlign(Paint.Align.CENTER);
|
||||||
|
big.setFontFeatureSettings("tnum");
|
||||||
|
big.setColor(white);
|
||||||
|
small.setTypeface(Typeface.create("sans-serif-light", Typeface.NORMAL));
|
||||||
|
small.setFontFeatureSettings("tnum");
|
||||||
|
small.setLetterSpacing(0.06f);
|
||||||
|
bar.setStrokeCap(Paint.Cap.ROUND);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onDraw(Canvas c) {
|
||||||
|
super.onDraw(c);
|
||||||
|
float w = getWidth(), h = getHeight();
|
||||||
|
if (w <= 0 || h <= 0) return;
|
||||||
|
|
||||||
|
// The gear, the speed, its unit.
|
||||||
|
String gear = Gear.label(v.gear);
|
||||||
|
text(c, gear.isEmpty() ? " " : gear, w / 2f, h * 0.17f, 26f * dp, light, Paint.Align.CENTER);
|
||||||
|
big.setTextSize(h * 0.50f);
|
||||||
|
c.drawText(fmt(v.speedKmh, "%.0f", "—"), w / 2f, h * 0.58f, big);
|
||||||
|
text(c, "km/h", w / 2f, h * 0.67f, 18f * dp, grey, Paint.Align.CENTER);
|
||||||
|
|
||||||
|
// Power: a line from the centre, its length up to a quarter of the width at 150 kW.
|
||||||
|
float y = h * 0.75f;
|
||||||
|
bar.setStrokeWidth(4f * dp);
|
||||||
|
bar.setColor(track);
|
||||||
|
c.drawLine(w / 2f - 1f * dp, y, w / 2f + 1f * dp, y, bar);
|
||||||
|
if (v.powerKw != null && Math.abs(v.powerKw) >= 0.1f) {
|
||||||
|
float len = w * 0.25f * Math.min(150f, Math.abs(v.powerKw)) / 150f * (v.powerKw < 0f ? 3f : 1f);
|
||||||
|
len = Math.min(len, w * 0.25f);
|
||||||
|
bar.setColor(v.powerKw < 0f ? regen : accent);
|
||||||
|
c.drawLine(w / 2f, y, w / 2f + (v.powerKw < 0f ? -len : len), y, bar);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Corners: power and the clock; bottom middle: usage.
|
||||||
|
text(c, fmt(v.powerKw, "%.1f kW", "— kW"), 8f * dp, 30f * dp, 20f * dp, grey, Paint.Align.LEFT);
|
||||||
|
text(c, v.clock.isEmpty() ? "—" : v.clock, w - 8f * dp, 30f * dp, 20f * dp, grey, Paint.Align.RIGHT);
|
||||||
|
text(c, fmt(v.usage5Min, "%.1f kWh/100 km", "— kWh/100 km"), w / 2f, h * 0.86f, 16f * dp, grey, Paint.Align.CENTER);
|
||||||
|
|
||||||
|
// The battery: a hairline along the bottom, filled to the charge.
|
||||||
|
float by = h - 6f * dp;
|
||||||
|
bar.setStrokeWidth(3f * dp);
|
||||||
|
bar.setColor(track);
|
||||||
|
c.drawLine(8f * dp, by, w - 8f * dp, by, bar);
|
||||||
|
if (v.socPercent != null) {
|
||||||
|
bar.setColor(v.socPercent < 20f ? warn : accent);
|
||||||
|
c.drawLine(8f * dp, by, 8f * dp + (w - 16f * dp) * Math.max(0f, Math.min(100f, v.socPercent)) / 100f, by, bar);
|
||||||
|
}
|
||||||
|
text(c, fmt(v.socPercent, "%.0f %%", "— %"), 8f * dp, by - 12f * dp, 20f * dp, white, Paint.Align.LEFT);
|
||||||
|
text(c, fmt(v.rangeKm, "%.0f km", "— km"), w - 8f * dp, by - 12f * dp, 20f * dp, white, Paint.Align.RIGHT);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void text(Canvas c, String s, float x, float y, float size, int colour, Paint.Align align) {
|
||||||
|
small.setTextSize(size);
|
||||||
|
small.setColor(colour);
|
||||||
|
small.setTextAlign(align);
|
||||||
|
c.drawText(s, x, y, small);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,182 @@
|
|||||||
|
package com.evsuite.abrp;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import android.graphics.Canvas;
|
||||||
|
import android.graphics.Paint;
|
||||||
|
import android.graphics.RectF;
|
||||||
|
import android.graphics.Typeface;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The Nixie Overview style: glowing orange digits in glass tubes, on a dark walnut panel with
|
||||||
|
* brass name plates.
|
||||||
|
*
|
||||||
|
* Two rows of tube groups. Top, large: speed, the gear in a single tube, battery. Bottom,
|
||||||
|
* smaller: power (with its sign), range, 5-minute usage and the clock (with a neon colon).
|
||||||
|
* Each tube shows the unlit numerals faintly behind the lit one, and a fine mesh. Decimal
|
||||||
|
* points are small neon dots ({@link SevenSegment} lays the text out in tubes). Unread values
|
||||||
|
* light dashes. Redrawn only when a reading arrives.
|
||||||
|
*/
|
||||||
|
final class NixieView extends ClusterCanvasView {
|
||||||
|
|
||||||
|
private final Paint glass = new Paint(Paint.ANTI_ALIAS_FLAG);
|
||||||
|
private final Paint rim = new Paint(Paint.ANTI_ALIAS_FLAG);
|
||||||
|
private final Paint mesh = new Paint();
|
||||||
|
private final Paint ghost = new Paint(Paint.ANTI_ALIAS_FLAG);
|
||||||
|
private final Paint glow = new Paint(Paint.ANTI_ALIAS_FLAG);
|
||||||
|
private final Paint digit = new Paint(Paint.ANTI_ALIAS_FLAG);
|
||||||
|
private final Paint plate = new Paint(Paint.ANTI_ALIAS_FLAG);
|
||||||
|
private final Paint engraving = new Paint(Paint.ANTI_ALIAS_FLAG);
|
||||||
|
private final Paint bg = new Paint();
|
||||||
|
private final Paint grain = new Paint(Paint.ANTI_ALIAS_FLAG);
|
||||||
|
private final Paint shine = new Paint(Paint.ANTI_ALIAS_FLAG);
|
||||||
|
private final Paint dotPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
|
||||||
|
private final RectF rect = new RectF();
|
||||||
|
private final int neon = 0xFFFF8A1F, panel = 0xFF140C07;
|
||||||
|
|
||||||
|
NixieView(Context context) {
|
||||||
|
super(context);
|
||||||
|
glass.setColor(0xFF1C130D);
|
||||||
|
rim.setStyle(Paint.Style.STROKE);
|
||||||
|
rim.setColor(0xFF4A3524);
|
||||||
|
mesh.setColor(0x16FFB070);
|
||||||
|
Typeface face = Typeface.create("sans-serif-light", Typeface.NORMAL);
|
||||||
|
ghost.setTypeface(face);
|
||||||
|
ghost.setTextAlign(Paint.Align.CENTER);
|
||||||
|
ghost.setColor(0x22FF8A1F);
|
||||||
|
glow.setTypeface(face);
|
||||||
|
glow.setTextAlign(Paint.Align.CENTER);
|
||||||
|
glow.setStyle(Paint.Style.STROKE);
|
||||||
|
glow.setColor(0x55FF6A00);
|
||||||
|
digit.setTypeface(face);
|
||||||
|
digit.setTextAlign(Paint.Align.CENTER);
|
||||||
|
digit.setColor(neon);
|
||||||
|
plate.setColor(0xFF6B5230);
|
||||||
|
engraving.setColor(0xFFF0DDB2);
|
||||||
|
engraving.setTextAlign(Paint.Align.CENTER);
|
||||||
|
engraving.setTypeface(Typeface.create("serif", Typeface.BOLD));
|
||||||
|
engraving.setLetterSpacing(0.18f);
|
||||||
|
bg.setColor(panel);
|
||||||
|
grain.setColor(0x0EFFC890);
|
||||||
|
shine.setColor(0x18FFFFFF);
|
||||||
|
shine.setStyle(Paint.Style.STROKE);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onDraw(Canvas c) {
|
||||||
|
super.onDraw(c);
|
||||||
|
float w = getWidth(), h = getHeight();
|
||||||
|
if (w <= 0 || h <= 0) return;
|
||||||
|
rect.set(0, 0, w, h);
|
||||||
|
c.drawRoundRect(rect, 20f * dp, 20f * dp, bg);
|
||||||
|
// Walnut grain: a few faint horizontal strokes.
|
||||||
|
grain.setStrokeWidth(dp);
|
||||||
|
for (int i = 0; i < 14; i++) {
|
||||||
|
float y = h * (0.04f + i * 0.07f);
|
||||||
|
c.drawLine(w * 0.02f, y + (i % 3) * 2f * dp, w * 0.98f, y - (i % 2) * 3f * dp, grain);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Top row, large: speed, gear, battery.
|
||||||
|
float th = h * 0.42f, tw = th * 0.50f, gap = tw * 0.12f, ty = h * 0.06f;
|
||||||
|
String speed = fmt(v.speedKmh, "%.0f", "---");
|
||||||
|
String soc = fmt(v.socPercent, "%.0f", "---");
|
||||||
|
String gear = Gear.label(v.gear);
|
||||||
|
float groupGap = tw * 0.9f;
|
||||||
|
float total = 3 * tw + 2 * gap + groupGap + tw + groupGap + 3 * tw + 2 * gap;
|
||||||
|
float x = (w - total) / 2f;
|
||||||
|
x = tubes(c, speed, 3, x, ty, tw, th, gap, "KM/H");
|
||||||
|
x += groupGap;
|
||||||
|
x = tubes(c, gear.isEmpty() ? " " : gear.equals("—") ? "-" : gear, 1, x, ty, tw, th, gap, "GEAR");
|
||||||
|
x += groupGap;
|
||||||
|
tubes(c, soc, 3, x, ty, tw, th, gap, "BATTERY %");
|
||||||
|
|
||||||
|
// Bottom row, smaller: power, range, usage, clock.
|
||||||
|
// 14 tubes, 14 gaps, a colon and three group gaps: about 18.6 tube widths, fitted to the width.
|
||||||
|
float bw = Math.min(h * 0.24f * 0.52f, w * 0.96f / 18.6f), bh = bw / 0.52f, bgap = bw * 0.12f, by = h * 0.62f;
|
||||||
|
float bGroup = bw * 0.8f;
|
||||||
|
String power = v.powerKw == null ? "---" : String.format(java.util.Locale.US, "%.1f", v.powerKw);
|
||||||
|
String range = fmt(v.rangeKm, "%.0f", "---");
|
||||||
|
String usage = fmt(v.usage5Min, "%.1f", "--.-");
|
||||||
|
String clock = v.clock.length() == 5 ? v.clock : "--:--";
|
||||||
|
float colon = bw * 0.5f;
|
||||||
|
float bTotal = 14 * bw + 14 * bgap + colon + 3 * bGroup;
|
||||||
|
x = (w - bTotal) / 2f;
|
||||||
|
x = tubes(c, power, 4, x, by, bw, bh, bgap, "POWER KW");
|
||||||
|
x += bGroup;
|
||||||
|
x = tubes(c, range, 3, x, by, bw, bh, bgap, "RANGE KM");
|
||||||
|
x += bGroup;
|
||||||
|
x = tubes(c, usage, 3, x, by, bw, bh, bgap, "KWH/100");
|
||||||
|
x += bGroup;
|
||||||
|
float clockStart = x;
|
||||||
|
x = tubes(c, clock.substring(0, 2), 2, x, by, bw, bh, bgap, null);
|
||||||
|
// The colon: two neon dots.
|
||||||
|
float cx = x - bgap + colon / 2f + bgap / 2f;
|
||||||
|
neonDot(c, cx, by + bh * 0.38f, bw * 0.07f);
|
||||||
|
neonDot(c, cx, by + bh * 0.62f, bw * 0.07f);
|
||||||
|
x += colon;
|
||||||
|
x = tubes(c, clock.substring(3, 5), 2, x, by, bw, bh, bgap, null);
|
||||||
|
nameplate(c, (clockStart + x - bgap) / 2f, by + bh + 8f * dp, "TIME");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tubes for the text, right-aligned in {@code cells}, from {@code x}; a name plate under
|
||||||
|
* them unless null. Returns the x after the last tube and its gap.
|
||||||
|
*/
|
||||||
|
private float tubes(Canvas c, String text, int cells, float x, float y, float tw, float th, float gap, String name) {
|
||||||
|
char[] chars = SevenSegment.cells(text, cells);
|
||||||
|
boolean[] dots = SevenSegment.dots(text, cells);
|
||||||
|
float start = x;
|
||||||
|
for (int i = 0; i < cells; i++) {
|
||||||
|
tube(c, x, y, tw, th, chars[i], dots[i]);
|
||||||
|
x += tw + gap;
|
||||||
|
}
|
||||||
|
if (name != null) nameplate(c, (start + x - gap) / 2f, y + th + 8f * dp, name);
|
||||||
|
return x;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void tube(Canvas c, float x, float y, float tw, float th, char ch, boolean dot) {
|
||||||
|
rect.set(x, y, x + tw, y + th);
|
||||||
|
float radius = tw * 0.45f;
|
||||||
|
c.drawRoundRect(rect, radius, radius, glass);
|
||||||
|
// The mesh in front of the digits.
|
||||||
|
c.save();
|
||||||
|
c.clipRect(x + tw * 0.08f, y + th * 0.06f, x + tw * 0.92f, y + th * 0.94f);
|
||||||
|
float step = Math.max(3f * dp, tw * 0.07f);
|
||||||
|
for (float mx = x; mx < x + tw; mx += step) c.drawLine(mx, y, mx, y + th, mesh);
|
||||||
|
for (float my = y; my < y + th; my += step) c.drawLine(x, my, x + tw, my, mesh);
|
||||||
|
c.restore();
|
||||||
|
// The stacked cathodes, faint, then the lit one with its glow.
|
||||||
|
float size = th * 0.66f, base = y + th * 0.5f + size * 0.36f;
|
||||||
|
ghost.setTextSize(size);
|
||||||
|
c.drawText("8", x + tw / 2f, base, ghost);
|
||||||
|
c.drawText("0", x + tw / 2f, base, ghost);
|
||||||
|
if (ch != ' ') {
|
||||||
|
glow.setTextSize(size);
|
||||||
|
glow.setStrokeWidth(size * 0.08f);
|
||||||
|
c.drawText(String.valueOf(ch), x + tw / 2f, base, glow);
|
||||||
|
digit.setTextSize(size);
|
||||||
|
c.drawText(String.valueOf(ch), x + tw / 2f, base, digit);
|
||||||
|
}
|
||||||
|
if (dot) neonDot(c, x + tw * 0.85f, y + th * 0.84f, tw * 0.06f);
|
||||||
|
rim.setStrokeWidth(Math.max(1.5f * dp, tw * 0.025f));
|
||||||
|
c.drawRoundRect(rect, radius, radius, rim);
|
||||||
|
// A highlight on the glass.
|
||||||
|
shine.setStrokeWidth(tw * 0.05f);
|
||||||
|
c.drawLine(x + tw * 0.2f, y + th * 0.12f, x + tw * 0.2f, y + th * 0.45f, shine);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void neonDot(Canvas c, float x, float y, float r) {
|
||||||
|
dotPaint.setColor(0x55FF6A00);
|
||||||
|
c.drawCircle(x, y, r * 2f, dotPaint);
|
||||||
|
dotPaint.setColor(neon);
|
||||||
|
c.drawCircle(x, y, r, dotPaint);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** A brass plate with engraved capitals, centred at x. */
|
||||||
|
private void nameplate(Canvas c, float x, float y, String name) {
|
||||||
|
engraving.setTextSize(12f * dp);
|
||||||
|
float tw = engraving.measureText(name) + 20f * dp;
|
||||||
|
rect.set(x - tw / 2f, y, x + tw / 2f, y + 22f * dp);
|
||||||
|
c.drawRoundRect(rect, 4f * dp, 4f * dp, plate);
|
||||||
|
c.drawText(name, x, y + 16f * dp, engraving);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,119 @@
|
|||||||
|
package com.evsuite.abrp;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import android.graphics.Canvas;
|
||||||
|
import android.graphics.Paint;
|
||||||
|
import android.graphics.Typeface;
|
||||||
|
import android.os.SystemClock;
|
||||||
|
|
||||||
|
import java.util.Locale;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The Terminal Overview style: text only, green phosphor on black, faint scan lines, as if a
|
||||||
|
* shell printed the car's state — each value on a row with an ASCII bar, the gear as [D], and
|
||||||
|
* a prompt with a blinking cursor (it blinks because readings redraw the view; it does not
|
||||||
|
* animate on its own). A low battery turns its row amber. Unread values show "—" and an empty
|
||||||
|
* bar.
|
||||||
|
*/
|
||||||
|
final class TerminalView extends ClusterCanvasView {
|
||||||
|
|
||||||
|
private static final int BAR = 24;
|
||||||
|
|
||||||
|
private final Paint text = new Paint(Paint.ANTI_ALIAS_FLAG);
|
||||||
|
private final Paint scan = new Paint();
|
||||||
|
private final int green, dim, amber;
|
||||||
|
|
||||||
|
TerminalView(Context context) {
|
||||||
|
super(context);
|
||||||
|
green = 0xFF33FF66;
|
||||||
|
dim = 0xFF1C8A3A;
|
||||||
|
amber = 0xFFFFB000;
|
||||||
|
text.setTypeface(Typeface.MONOSPACE);
|
||||||
|
scan.setColor(0x0CFFFFFF);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onDraw(Canvas c) {
|
||||||
|
super.onDraw(c);
|
||||||
|
float w = getWidth(), h = getHeight();
|
||||||
|
if (w <= 0 || h <= 0) return;
|
||||||
|
c.drawColor(0xFF020A04);
|
||||||
|
|
||||||
|
// About 14 rows at 1.32 em each plus margins; monospace is about 0.6 em wide and the
|
||||||
|
// longest row about 58 characters.
|
||||||
|
float size = Math.min(h / 19.5f, w / 38f);
|
||||||
|
text.setTextSize(size);
|
||||||
|
float line = size * 1.32f, x = 16f * dp, y = 12f * dp + line;
|
||||||
|
String gear = gearRow();
|
||||||
|
|
||||||
|
print(c, "nils@mg4:~$ watch -n 0.2 vehicle-status", x, y, dim);
|
||||||
|
y += line * 1.4f;
|
||||||
|
print(c, row("SPEED", fmt(v.speedKmh, "%.0f km/h", "—")) + bar(v.speedKmh, 0f, 180f), x, y, green);
|
||||||
|
y += line;
|
||||||
|
print(c, row("POWER", fmt(v.powerKw, "%.1f kW", "—")) + powerBar(), x, y, green);
|
||||||
|
y += line;
|
||||||
|
boolean low = v.socPercent != null && v.socPercent < 20f;
|
||||||
|
print(c, row("BATTERY", fmt(v.socPercent, "%.0f %%", "—")) + bar(v.socPercent, 0f, 100f)
|
||||||
|
+ (low ? " ! LOW" : ""), x, y, low ? amber : green);
|
||||||
|
y += line;
|
||||||
|
print(c, row("RANGE", fmt(v.rangeKm, "%.0f km", "—")), x, y, green);
|
||||||
|
y += line;
|
||||||
|
print(c, row("GEAR", gear), x, y, green);
|
||||||
|
y += line * 1.4f;
|
||||||
|
print(c, row("USAGE", fmt(v.usage5Min, "%.1f kWh/100km", "—")) + " # last 5 min", x, y, green);
|
||||||
|
y += line;
|
||||||
|
print(c, row("SINCE", fmt(v.usageTrip, "%.1f kWh/100km", "—")) + " # trip "
|
||||||
|
+ fmt(v.tripKm, "%.1f km", "—"), x, y, green);
|
||||||
|
y += line;
|
||||||
|
print(c, row("CLIMATE", fmt(v.climateCelsius, "%.0f C", "—")) + " ODO " + fmt(v.odometerKm, "%06.0f km", "—"), x, y, green);
|
||||||
|
y += line;
|
||||||
|
print(c, row("TYRES", "FL " + v.tyreFl + " FR " + v.tyreFr + " RL " + v.tyreRl + " RR " + v.tyreRr), x, y, green);
|
||||||
|
y += line * 1.4f;
|
||||||
|
boolean cursor = (SystemClock.uptimeMillis() / 500) % 2 == 0;
|
||||||
|
print(c, "[" + (v.clock.isEmpty() ? "--:--" : v.clock) + "] nils@mg4:~$ " + (cursor ? "█" : ""), x, y, green);
|
||||||
|
|
||||||
|
// Scan lines over everything.
|
||||||
|
for (float sy = 0; sy < h; sy += 3f * dp) c.drawLine(0, sy, w, sy, scan);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void print(Canvas c, String s, float x, float y, int colour) {
|
||||||
|
text.setColor(colour);
|
||||||
|
c.drawText(s, x, y, text);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String row(String label, String value) {
|
||||||
|
return String.format(Locale.US, "%-9s%-16s", label, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** [#####.......] from min to max; empty when unread. */
|
||||||
|
private static String bar(Float value, float min, float max) {
|
||||||
|
int n = value == null ? 0 : Math.round(BAR * (Math.max(min, Math.min(max, value)) - min) / (max - min));
|
||||||
|
StringBuilder sb = new StringBuilder("[");
|
||||||
|
for (int i = 0; i < BAR; i++) sb.append(i < n ? '#' : '.');
|
||||||
|
return sb.append(']').toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** −50…150 kW with a | at zero: < for regeneration to its left, # for drawing to its right. */
|
||||||
|
private String powerBar() {
|
||||||
|
int zero = BAR / 4;
|
||||||
|
int to = v.powerKw == null ? zero : zero + Math.round(BAR * Math.max(-50f, Math.min(150f, v.powerKw)) / 200f);
|
||||||
|
StringBuilder sb = new StringBuilder("[");
|
||||||
|
for (int i = 0; i < BAR; i++) {
|
||||||
|
if (i == zero) sb.append('|');
|
||||||
|
else if (v.powerKw != null && i < zero && i >= to) sb.append('<');
|
||||||
|
else if (v.powerKw != null && i > zero && i <= to) sb.append('#');
|
||||||
|
else sb.append('.');
|
||||||
|
}
|
||||||
|
return sb.append(']').toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
private String gearRow() {
|
||||||
|
String[] letters = { "P", "R", "N", "D" };
|
||||||
|
StringBuilder sb = new StringBuilder();
|
||||||
|
for (int i = 0; i < 4; i++) {
|
||||||
|
boolean on = v.gear != null && v.gear == i + 1;
|
||||||
|
sb.append(on ? "[" + letters[i] + "]" : " " + letters[i] + " ");
|
||||||
|
}
|
||||||
|
return sb.toString();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -131,6 +131,11 @@
|
|||||||
<string name="overview_style_cartoon">Cartoon</string>
|
<string name="overview_style_cartoon">Cartoon</string>
|
||||||
<string name="overview_style_blueprint">Blueprint</string>
|
<string name="overview_style_blueprint">Blueprint</string>
|
||||||
<string name="overview_style_mg">MG</string>
|
<string name="overview_style_mg">MG</string>
|
||||||
|
<string name="overview_style_minimal">Minimal</string>
|
||||||
|
<string name="overview_style_terminal">Terminal</string>
|
||||||
|
<string name="overview_style_aviation">Aviation</string>
|
||||||
|
<string name="overview_style_nixie">Nixie tubes</string>
|
||||||
|
<string name="overview_style_chronograph">Chronograph</string>
|
||||||
<string name="cockpit_battery">BATTERY</string>
|
<string name="cockpit_battery">BATTERY</string>
|
||||||
<string name="cockpit_usage">USAGE · 5 MIN</string>
|
<string name="cockpit_usage">USAGE · 5 MIN</string>
|
||||||
<string name="cockpit_usage_unit">kWh/100 km</string>
|
<string name="cockpit_usage_unit">kWh/100 km</string>
|
||||||
|
|||||||
Binary file not shown.
|
After Width: | Height: | Size: 104 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 312 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 121 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 351 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 241 KiB |
Reference in New Issue
Block a user