App 2.35.0: the app takes on the Overview style's own colours

While the style shows its own palette (Sci-fi, Cartoon, Aviation, Nixie;
Retro and Terminal with "Original"), the navigation, buttons and titles
take it instead of the accent colour, e.g. turquoise for Retro
(StylePalette, style_palettes.xml). Blueprint and Mascot are removed; a
stored one opens Tiles. The navigation no longer saves view state, which
crashed a restore across the wide and narrow layouts.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LhzAVaUyqcw5PkGjz4mWW6
This commit is contained in:
2026-10-25 20:38:32 +01:00
co-authored by Claude Opus 5.5
parent c5f0d5dc9a
commit 921bab7170
24 changed files with 195 additions and 1834 deletions
+2 -2
View File
@@ -75,11 +75,11 @@ 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.2 s on a background thread. Styles (Settings → Overview style): Tiles, Gauges (`GaugeView` needles), Cockpit (`RingView` rings, `TraceView` power trace), Retro (`RetroView`, seven-segment VFD), Futuristic (`FlowView`, animated energy flow), Sci-fi (`SciFiView`, starship console), Cartoon (`CartoonView`), 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, Mascot (`MascotView`, an anime co-pilot whose face follows `MascotMood`; classic, `ChibiFigure` or `FoxFigure` on `ChibiBase`, chosen by `MascotCharacter`); the drawn styles share `ClusterValues`. A button in the navigation's header hides the navigation for a full-width Overview (`PREF_OVERVIEW_FULLSCREEN`); a swipe in from the left edge (`EdgeSwipe`) brings it back; without the app's own full screen the car's sidebar takes that edge, so a tab at the left edge (`edge_tab`) is tapped or swiped instead. Pages change only by the navigation (the pager's own swiping is off) |
| Overview | value tiles (battery, range, range/usage/energy since start, 5-min averages of range, usage, power, speed), vehicle info (odometer, lease allowance and the odometer against it), tyre pressures; read every 0.2 s on a background thread. Styles (Settings → Overview style): Tiles, Gauges (`GaugeView` needles), Cockpit (`RingView` rings, `TraceView` power trace), Retro (`RetroView`, seven-segment VFD), Futuristic (`FlowView`, animated energy flow), Sci-fi (`SciFiView`, starship console), Cartoon (`CartoonView`), MG (`OctagonView`, octagonal dials after the classic MG dashboards), and on `ClusterCanvasView`: Minimal, Terminal, Aviation, Nixie tubes, Chronograph; the drawn styles share `ClusterValues`. A style shown in its own palette (Sci-fi, Cartoon, Aviation, Nixie; Retro and Terminal with "Original") gives the whole app its colours (`StylePalette`, `res/values/style_palettes.xml`) instead of the accent colour. A button in the navigation's header hides the navigation for a full-width Overview (`PREF_OVERVIEW_FULLSCREEN`); a swipe in from the left edge (`EdgeSwipe`) brings it back; without the app's own full screen the car's sidebar takes that edge, so a tab at the left edge (`edge_tab`) is tapped or swiped instead. Pages change only by the navigation (the pager's own swiping is off) |
| 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 |
| Settings | drive mode (Eco, Normal, Sport, Snow, Custom; the lit one is what the car reports; changes only in P at a standstill, `DriveModeWrite`; "Set at start" sets the last confirmed choice again when the service starts with the head unit), full screen, voice key, Overview style with the accent colour beside it, the Mascot character (only for that style) (offered only for styles drawn in it, `OverviewStyle.usesAccent`; Retro and Terminal add "Original" for their own palette, `hasOriginal`), tyre unit, battery and charging price (one card on wide screens), version, update check, install from USB, About |
| Settings | drive mode (Eco, Normal, Sport, Snow, Custom; the lit one is what the car reports; changes only in P at a standstill, `DriveModeWrite`; "Set at start" sets the last confirmed choice again when the service starts with the head unit), full screen, voice key, Overview style with the accent colour beside it (offered only for styles drawn in it, `OverviewStyle.usesAccent`; Retro and Terminal add "Original" for their own palette, `hasOriginal`), tyre unit, battery and charging price (one card on wide screens), version, update check, install from USB, About |
| Logs | signal recorder (`SignalRecorder`/`SignalRecording`: every property and vendor getter via `VendorSurfaceProbe.capture`, again and again for 3 minutes, plus steering-wheel key broadcasts; lists what changed, rarest first; saved to the app and a USB stick; how to find where the car publishes something), update log (every install step, with the platform's words), upload log |
Key classes (`app/src/main/java/com/evsuite/abrp/`): `AbrpUploadService` (foreground service,
+1 -1
View File
@@ -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 five times a second. Looks to choose from in Settings → Overview style: Tiles, Gauges, Cockpit, Retro, Futuristic, Sci-fi, Cartoon, Blueprint, MG, Minimal, Terminal, Aviation, Nixie tubes, Chronograph and Mascot (an anime co-pilot who reacts to the drive, classic, chibi or a chibi fox). The small button next to the MG4 logo in the navigation hides the navigation for a full-width Overview; swipe in from the left edge (a faint handle marks it) to bring it back; with Settings → Full screen off, the car's own sidebar sits at that edge, so tap the small tab at the left edge (›) instead. Pages change only by the navigation, not by swiping sideways |
| **Overview** | Battery, range, power and speed; range, usage and energy since start; 5-minute averages of range, usage, power and speed; vehicle info (odometer, climate setpoint) and tyre pressures. Read five times a second. Looks to choose from in Settings → Overview style: Tiles, Gauges, Cockpit, Retro, Futuristic, Sci-fi, Cartoon, MG, Minimal, Terminal, Aviation, Nixie tubes and Chronograph. A style with its own colours (Sci-fi, Cartoon, Aviation, Nixie tubes, and Retro and Terminal with "Original") colours the whole app to match, e.g. turquoise for Retro. The small button next to the MG4 logo in the navigation hides the navigation for a full-width Overview; swipe in from the left edge (a faint handle marks it) to bring it back; with Settings → Full screen off, the car's own sidebar sits at that edge, so tap the small tab at the left edge (›) instead. Pages change only by the navigation, not by swiping sideways |
| **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 |
+16
View File
@@ -138,6 +138,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-25 — App 2.35.0: **the app takes on the style's own colours.** While the Overview style
shows its own palette — Sci-fi, Cartoon, Aviation and Nixie always, Retro and Terminal with
"Original" — the navigation, buttons, switches and titles take that palette instead of the
accent colour: Retro turquoise (`retro_lit`), Terminal green phosphor, Sci-fi orange, Cartoon
orange, Aviation cyan, Nixie neon orange (`StylePalette`, overlays in
`res/values/style_palettes.xml`, laid over the accent overlay in `onCreate`; a style or colour
choice that changes the overlay recreates the activity, as an accent change did). The accent
colour stays stored for the styles that use it. **Blueprint and Mascot removed** (owner's
choice): `BlueprintView`, `MascotView`, `MascotMood`, `MascotCharacter`, `ChibiBase`,
`ChibiFigure`, `FoxFigure`, their strings and the Mascot field on Settings; a stored
"BLUEPRINT" or "MASCOT" opens Tiles. Fix: the navigation (`nav_bar`) no longer saves view state —
it is a ScrollView in the wide layout and a LinearLayout in the narrow one, and restoring one
into the other crashed when the width changed (seen on the emulator only; the car's width does
not change). Tests (`OverviewStyleTest`: palettes per style, removed styles) and lint;
emulator: each palette, the switch to Retro recolouring at once, the width change. Not tried
on the car.
- 2026-10-25 — `styles-retro-future` (app 2.23.0–2.34.1) merged into `main` (merge commit
`aff79cb`, the summary as its message). Work goes on `main` again.
- 2026-10-25 — App 2.34.1 (branch `styles-retro-future`): the Overview's full screen without the
+2 -2
View File
@@ -34,8 +34,8 @@ android {
applicationId "com.stinnesbeck.nilsmg4"
minSdk 27
targetSdk 36
versionCode 134
versionName "2.34.1"
versionCode 135
versionName "2.35.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.
@@ -1,283 +0,0 @@
package com.evsuite.abrp;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.DashPathEffect;
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 Blueprint Overview style: a technical drawing of the car from above, white lines on a navy
* sheet with a grid.
*
* The battery pack between the axles is hatched as far as it is charged. Each tyre has a leader
* line to its pressure. Power is a force arrow at the front — out of the car while drawing,
* into it (dashed) while regenerating — its length scaled to the power. Dimension lines give the
* range and the trip, the cabin is annotated with the climate setpoint, and a title block in the
* corner holds speed, power, gear, usage, odometer and time. Unread values show "—", no hatch and
* no arrow. Redrawn only when a reading arrives.
*/
public final class BlueprintView extends View {
/** The MG4's length over its width, 4.287 m by 1.836 m. */
private static final float ASPECT = 4.287f / 1.836f;
private final Paint sheet = new Paint(Paint.ANTI_ALIAS_FLAG);
private final Paint grid = new Paint();
private final Paint line = new Paint(Paint.ANTI_ALIAS_FLAG);
private final Paint faint = new Paint(Paint.ANTI_ALIAS_FLAG);
private final Paint dashed = 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 ink;
private ClusterValues v = new ClusterValues();
public BlueprintView(Context context, AttributeSet attrs) {
super(context, attrs);
float d = context.getResources().getDisplayMetrics().density;
ink = context.getColor(R.color.blueprint_line);
sheet.setColor(context.getColor(R.color.blueprint_sheet));
grid.setStyle(Paint.Style.STROKE);
line.setStyle(Paint.Style.STROKE);
line.setColor(ink);
line.setStrokeJoin(Paint.Join.ROUND);
line.setStrokeCap(Paint.Cap.ROUND);
faint.setStyle(Paint.Style.STROKE);
faint.setColor(AccentColor.withAlpha(ink, 0x99));
dashed.setStyle(Paint.Style.STROKE);
dashed.setColor(AccentColor.withAlpha(ink, 0xB0));
dashed.setPathEffect(new DashPathEffect(new float[] { 8f * d, 6f * d }, 0f));
text.setColor(ink);
text.setTypeface(Typeface.create(Typeface.MONOSPACE, Typeface.NORMAL));
text.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;
// The sheet and its grid: fine every 20 dp, bolder every 100 dp; a border inset.
rect.set(0, 0, w, h);
c.drawRect(rect, sheet);
for (float step : new float[] { 20f * d, 100f * d }) {
grid.setColor(AccentColor.withAlpha(ink, step > 50f * d ? 0x22 : 0x10));
grid.setStrokeWidth(Math.max(1f, d * 0.75f));
for (float x = 0; x <= w; x += step) c.drawLine(x, 0, x, h, grid);
for (float y = 0; y <= h; y += step) c.drawLine(0, y, w, y, grid);
}
line.setStrokeWidth(2f * d);
c.drawRect(8f * d, 8f * d, w - 8f * d, h - 8f * d, line);
// The car, front to the right, its size from the space left of the title block.
// Centred, with its power arrow, in the space left of the title block.
float carH = Math.min(h * 0.40f, w * 0.44f / ASPECT), carW = carH * ASPECT;
float l = w * 0.04f + (w * 0.60f - carW - w * 0.12f) / 2f, t = (h - carH) / 2f, r = l + carW, b = t + carH;
drawCar(c, l, t, r, b, d);
drawTyres(c, l, t, r, b, d);
drawPower(c, r, (t + b) / 2f, w * 0.10f, d);
drawDimensions(c, l, t, r, b, d);
drawTitleBlock(c, w * 0.66f, h * 0.10f, w - 22f * d, h - 22f * d, d);
}
private void drawCar(Canvas c, float l, float t, float r, float b, float d) {
float len = r - l, wid = b - t;
// Body: rounded, fuller at the front.
path.reset();
path.moveTo(l + len * 0.10f, t);
path.lineTo(r - len * 0.14f, t);
path.cubicTo(r - len * 0.02f, t, r, t + wid * 0.12f, r, t + wid * 0.30f);
path.lineTo(r, b - wid * 0.30f);
path.cubicTo(r, b - wid * 0.12f, r - len * 0.02f, b, r - len * 0.14f, b);
path.lineTo(l + len * 0.10f, b);
path.cubicTo(l + len * 0.02f, b, l, b - wid * 0.14f, l, b - wid * 0.32f);
path.lineTo(l, t + wid * 0.32f);
path.cubicTo(l, t + wid * 0.14f, l + len * 0.02f, t, l + len * 0.10f, t);
line.setStrokeWidth(2.5f * d);
c.drawPath(path, line);
// Glass: windscreen, roof and rear window, dashed.
rect.set(l + len * 0.24f, t + wid * 0.12f, l + len * 0.74f, b - wid * 0.12f);
c.drawRoundRect(rect, wid * 0.12f, wid * 0.12f, dashed);
c.drawLine(l + len * 0.62f, t + wid * 0.13f, l + len * 0.62f, b - wid * 0.13f, dashed);
c.drawLine(l + len * 0.33f, t + wid * 0.13f, l + len * 0.33f, b - wid * 0.13f, dashed);
// Mirrors.
float mx = l + len * 0.66f;
line.setStrokeWidth(2f * d);
c.drawLine(mx, t, mx - len * 0.03f, t - wid * 0.09f, line);
c.drawLine(mx - len * 0.03f, t - wid * 0.09f, mx - len * 0.07f, t - wid * 0.02f, line);
c.drawLine(mx, b, mx - len * 0.03f, b + wid * 0.09f, line);
c.drawLine(mx - len * 0.03f, b + wid * 0.09f, mx - len * 0.07f, b + wid * 0.02f, line);
// The battery between the axles, hatched as far as it is charged (from the rear).
float pl = l + len * 0.25f, pr = l + len * 0.73f, pt = t + wid * 0.24f, pb = b - wid * 0.24f;
faint.setStrokeWidth(1.5f * d);
c.drawRect(pl, pt, pr, pb, faint);
if (v.socPercent != null) {
float fx = pl + (pr - pl) * Math.max(0f, Math.min(100f, v.socPercent)) / 100f;
c.save();
c.clipRect(pl, pt, fx, pb);
float step = 9f * d;
for (float x = pl - (pb - pt); x < fx; x += step) c.drawLine(x, pb, x + (pb - pt), pt, faint);
c.restore();
line.setStrokeWidth(2f * d);
c.drawLine(fx, pt - 6f * d, fx, pb + 6f * d, line);
}
text.setTextAlign(Paint.Align.CENTER);
text.setTextSize(14f * d);
String soc = v.socPercent == null ? "—" : String.format(Locale.US, "%.0f %%", v.socPercent);
float labelY = (pt + pb) / 2f + 5f * d;
rect.set((pl + pr) / 2f - 70f * d, labelY - 18f * d, (pl + pr) / 2f + 70f * d, labelY + 8f * d);
c.drawRect(rect, sheet);
c.drawText("BATTERY " + soc, (pl + pr) / 2f, labelY, text);
// The cabin's climate setpoint, as a note.
String climate = v.climateCelsius == null ? "—" : String.format(Locale.US, "%.0f °C", v.climateCelsius);
text.setTextSize(12f * d);
c.drawText("CABIN SET " + climate, l + len * 0.49f, t + wid * 0.19f, text);
}
private void drawTyres(Canvas c, float l, float t, float r, float b, float d) {
float len = r - l, wid = b - t, tw = len * 0.13f, th = wid * 0.13f;
float rear = l + len * 0.20f, front = l + len * 0.80f;
// Driving to the right, the car's left side is at the top.
Object[][] tyres = {
{ front, t, v.tyreFl, true, "FL" }, { front, b, v.tyreFr, false, "FR" },
{ rear, t, v.tyreRl, true, "RL" }, { rear, b, v.tyreRr, false, "RR" },
};
line.setStrokeWidth(2f * d);
text.setTextSize(15f * d);
for (Object[] tyre : tyres) {
float x = (Float) tyre[0], y = (Float) tyre[1];
boolean top = (Boolean) tyre[3];
rect.set(x - tw / 2f, y - th / 2f, x + tw / 2f, y + th / 2f);
c.drawRect(rect, sheet);
c.drawRoundRect(rect, 4f * d, 4f * d, line);
// A leader line out to the pressure.
float ly = top ? y - th / 2f - wid * 0.24f : y + th / 2f + wid * 0.24f;
float lx = x + len * 0.06f;
faint.setStrokeWidth(1.5f * d);
c.drawLine(x, top ? y - th / 2f : y + th / 2f, lx, ly, faint);
c.drawLine(lx, ly, lx + 110f * d, ly, faint);
c.drawCircle(x, top ? y - th / 2f : y + th / 2f, 3f * d, line);
text.setTextAlign(Paint.Align.LEFT);
c.drawText(tyre[4] + " " + tyre[2], lx + 4f * d, ly - 6f * d, text);
}
}
/** Power as a force arrow at the front; length scaled to 150 kW; regeneration points back. */
private void drawPower(Canvas c, float x, float y, float maxLen, float d) {
text.setTextAlign(Paint.Align.LEFT);
text.setTextSize(15f * d);
if (v.powerKw == null || Math.abs(v.powerKw) < 0.1f) {
c.drawText(v.powerKw == null ? "P = —" : "P = 0 kW", x + 16f * d, y + 5f * d, text);
return;
}
float kw = v.powerKw;
float len = Math.max(24f * d, maxLen * Math.min(150f, Math.abs(kw)) / 150f);
float x0 = x + 10f * d, x1 = x0 + len;
Paint p = kw >= 0f ? line : dashed;
p.setStrokeWidth(2.5f * d);
c.drawLine(x0, y, x1, y, p);
// The head: at the far end drawing, at the car regenerating.
float hx = kw >= 0f ? x1 : x0, dir = kw >= 0f ? 1f : -1f, hs = 10f * d;
line.setStrokeWidth(2.5f * d);
c.drawLine(hx, y, hx - dir * hs, y - hs * 0.6f, line);
c.drawLine(hx, y, hx - dir * hs, y + hs * 0.6f, line);
c.drawText(String.format(Locale.US, "P = %.1f kW", kw), x0, y - 10f * d, text);
if (kw < 0f) c.drawText("(REGEN)", x0, y + 22f * d, text);
}
/** Dimension lines: the range under the car, the trip above it. */
private void drawDimensions(Canvas c, float l, float t, float r, float b, float d) {
float wid = b - t;
String range = v.rangeKm == null ? "RANGE —" : String.format(Locale.US, "RANGE %.0f km", v.rangeKm);
dimension(c, l, r, b + wid * 0.50f, range, d);
String trip = v.tripKm == null ? "TRIP —" : String.format(Locale.US, "TRIP %.1f km", v.tripKm);
if (v.usageTrip != null) trip += String.format(Locale.US, " @ %.1f kWh/100 km", v.usageTrip);
dimension(c, l, r, t - wid * 0.50f, trip, d);
}
private void dimension(Canvas c, float x0, float x1, float y, String label, float d) {
faint.setStrokeWidth(1.5f * d);
c.drawLine(x0, y - 10f * d, x0, y + 10f * d, faint);
c.drawLine(x1, y - 10f * d, x1, y + 10f * d, faint);
c.drawLine(x0, y, x1, y, faint);
float hs = 9f * d;
c.drawLine(x0, y, x0 + hs, y - hs * 0.45f, faint);
c.drawLine(x0, y, x0 + hs, y + hs * 0.45f, faint);
c.drawLine(x1, y, x1 - hs, y - hs * 0.45f, faint);
c.drawLine(x1, y, x1 - hs, y + hs * 0.45f, faint);
text.setTextAlign(Paint.Align.CENTER);
text.setTextSize(15f * d);
float tw = text.measureText(label);
float mid = (x0 + x1) / 2f;
rect.set(mid - tw / 2f - 8f * d, y - 12f * d, mid + tw / 2f + 8f * d, y + 12f * d);
c.drawRect(rect, sheet);
c.drawText(label, mid, y + 5f * d, text);
}
/** The drawing's title block: rows of fields, the speed large. */
private void drawTitleBlock(Canvas c, float l, float t, float r, float b, float d) {
line.setStrokeWidth(2f * d);
rect.set(l, t, r, b);
c.drawRect(rect, sheet);
c.drawRect(rect, line);
// Eight rows: the speed takes two, the last holds scale and time.
float rowH = (b - t) / 8f;
String speed = v.speedKmh == null ? "—" : String.format(Locale.US, "%.0f", v.speedKmh);
String[][] rows = {
{ "DRAWING", "NILS' MG4 · TOP VIEW" },
{ "SPEED", null },
{ "POWER", v.powerKw == null ? "—" : String.format(Locale.US, "%.1f kW", v.powerKw) },
{ "GEAR", Gear.label(v.gear).isEmpty() ? "—" : Gear.label(v.gear) },
{ "USAGE 5 MIN", v.usage5Min == null ? "—" : String.format(Locale.US, "%.1f kWh/100 km", v.usage5Min) },
{ "ODOMETER", v.odometerKm == null ? "—" : String.format(Locale.US, "%,.0f km", v.odometerKm) },
};
float split = l + (r - l) * 0.34f;
float y = t;
for (int i = 0; i < rows.length; i++) {
float rh = i == 1 ? rowH * 2f : rowH;
if (i > 0) c.drawLine(l, y, r, y, faint);
text.setTextAlign(Paint.Align.LEFT);
text.setTextSize(11f * d);
c.drawText(rows[i][0], l + 10f * d, y + 18f * d, text);
if (i == 1) {
text.setTextSize(rh * 0.62f);
text.setTextAlign(Paint.Align.RIGHT);
c.drawText(speed, r - (r - l) * 0.22f, y + rh * 0.80f, text);
text.setTextSize(14f * d);
text.setTextAlign(Paint.Align.LEFT);
c.drawText("km/h", r - (r - l) * 0.20f, y + rh * 0.80f, text);
} else {
text.setTextSize(Math.min(rh * 0.42f, 18f * d));
c.drawText(rows[i][1], i == 0 ? l + 10f * d : split + 12f * d, y + rh * 0.72f, text);
}
y += rh;
}
// The last row: scale and time, split in two.
c.drawLine(l, y, r, y, faint);
c.drawLine(split, y, split, b, faint);
text.setTextSize(11f * d);
text.setTextAlign(Paint.Align.LEFT);
c.drawText("SCALE 1:20", l + 10f * d, y + rowH * 0.62f, text);
c.drawText("TIME", split + 10f * d, y + 18f * d, text);
text.setTextSize(Math.min(rowH * 0.5f, 22f * d));
text.setTextAlign(Paint.Align.RIGHT);
c.drawText(v.clock.isEmpty() ? "—" : v.clock, r - 12f * d, y + rowH * 0.72f, text);
c.drawLine(split, t + rowH * 3f, split, t + rowH * 7f, faint);
}
}
@@ -1,316 +0,0 @@
package com.evsuite.abrp;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.RectF;
/**
* What the chibi Mascot figures ({@link ChibiFigure}, {@link FoxFigure}) share: the frame
* (scaled down and moved left so the big head stays clear of the speed, a shadow, a hop while
* cheering, a lean), their big eyes and mouths per {@link MascotMood}, limbs placed by the mood,
* and the effects around them (Zs, sparkles, wind, "?", drops). Sticker style: dark outlines.
*
* Draws on {@link MascotView}'s 100 × 100 grid; holds no state.
*/
abstract class ChibiBase {
protected final Paint fill = new Paint(Paint.ANTI_ALIAS_FLAG);
protected final Paint line = new Paint(Paint.ANTI_ALIAS_FLAG);
protected final Paint text = new Paint(Paint.ANTI_ALIAS_FLAG);
protected final Path p = new Path();
protected final Path q = new Path();
protected final RectF r = new RectF();
protected final int accent, accentLight, regen;
protected final int ink = 0xFF3A2A33, blush = 0x66FF7A8A, mouth = 0xFF8E3A48, tongue = 0xFFE88A95;
/** The eyes' colours, and what a narrowed lid is filled with (skin or fur). */
private final int iris, irisLight, lid;
/** How far left the figure moves, in grid units. */
private final float shiftX;
ChibiBase(int accent, int accentLight, int regen, int iris, int irisLight, int lid, float shiftX) {
this.accent = accent;
this.accentLight = accentLight;
this.regen = regen;
this.iris = iris;
this.irisLight = irisLight;
this.lid = lid;
this.shiftX = shiftX;
line.setStyle(Paint.Style.STROKE);
line.setStrokeCap(Paint.Cap.ROUND);
line.setStrokeJoin(Paint.Join.ROUND);
line.setColor(ink);
text.setTextAlign(Paint.Align.CENTER);
text.setFakeBoldText(true);
}
/** Where the speech bubble's tail points, on the grid. */
abstract float tipX();
abstract float tipY();
/** The figure itself, standing or sitting on y = 96, centred on x = 50. */
protected abstract void drawFigure(Canvas c, MascotMood mood, long t, boolean blink);
final void draw(Canvas c, MascotMood mood, long t, boolean blink) {
c.save();
c.translate(-shiftX, 0f);
c.scale(0.92f, 0.92f, 50f, 100f);
// A hop while cheering.
float hop = mood == MascotMood.CHEERING ? -3.5f * (float) Math.abs(Math.sin(t / 180.0)) : 0f;
fill.setColor(0x40000000);
float shadow = 18f + hop * 0.8f;
c.drawOval(oval(50f - shadow, 96.5f, 50f + shadow, 100f), fill);
c.save();
c.translate(0f, hop);
float lean = mood == MascotMood.FAST ? 5f : mood == MascotMood.SLEEPING ? -6f : 0f;
c.rotate(lean, 50f, 96f);
drawFigure(c, mood, t, blink);
c.restore();
switch (mood) {
case SLEEPING: zs(c, t); break;
case CHEERING: sparkles(c, t); break;
case FAST: wind(c, t); break;
case WAITING: question(c, t); break;
default: break;
}
c.restore();
}
/** Head tilt per mood, degrees. */
static float tilt(MascotMood mood) {
return mood == MascotMood.REVERSE ? -10f : mood == MascotMood.SLEEPING ? 10f
: mood == MascotMood.WAITING ? 6f : 0f;
}
/** Hands on the cheeks and a fist at the chest are drawn in front of the head. */
static boolean handsInFront(MascotMood mood) {
return mood == MascotMood.WORRIED || mood == MascotMood.DETERMINED;
}
/** Left and right hand positions for the mood: {lx, ly, rx, ry}. */
static float[] hands(MascotMood mood, long t) {
float wave = (float) Math.sin(t / 220.0) * 3f;
switch (mood) {
case CONTENT: return new float[] { 33f, 82f, 82f + wave * 0.5f, 64f + wave };
case CHEERING: return new float[] { 16f, 56f, 84f, 56f };
case DETERMINED: return new float[] { 33f, 82f, 57f, 72f };
case WORRIED: return new float[] { 34f, 55f, 66f, 55f };
// Pointing back.
case REVERSE: return new float[] { 20f, 66f, 67f, 82f };
case FAST: return new float[] { 30f, 76f, 70f, 76f };
default: return new float[] { 33f, 82f, 67f, 82f };
}
}
/** A limb from the shoulder to a round hand or paw, outlined. */
protected void limb(Canvas c, float sx, float sy, float hx, float hy, int sleeve, int hand) {
line.setStrokeWidth(7.4f);
c.drawLine(sx, sy, hx, hy, line);
line.setColor(sleeve);
line.setStrokeWidth(5f);
c.drawLine(sx, sy, hx, hy, line);
line.setColor(ink);
fill.setColor(hand);
line.setStrokeWidth(1.1f);
c.drawCircle(hx, hy, 3.2f, fill);
c.drawCircle(hx, hy, 3.2f, line);
}
/** Cheeks, eyes at y 44, brows, mouth around y 55 and the worried drops. */
protected void face(Canvas c, MascotMood mood, long t, boolean blink) {
if (mood == MascotMood.CHEERING || mood == MascotMood.FAST || mood == MascotMood.CONTENT
|| mood == MascotMood.SLEEPING || mood == MascotMood.WORRIED) {
fill.setColor(blush);
c.drawOval(oval(29.5f, 50f, 37f, 54f), fill);
c.drawOval(oval(63f, 50f, 70.5f, 54f), fill);
}
eye(c, 39f, 44f, mood, blink);
eye(c, 61f, 44f, mood, blink);
brows(c, mood);
mouth(c, mood, t);
if (mood == MascotMood.WORRIED) {
fill.setColor(0xFF8FD3FF);
drop(c, 72f, 30f, 3f);
drop(c, 34.5f, 52f, 1.8f);
}
}
private void eye(Canvas c, float cx, float cy, MascotMood mood, boolean blink) {
line.setColor(ink);
line.setStrokeWidth(1.5f);
if (mood == MascotMood.SLEEPING || (blink && mood != MascotMood.CHEERING && mood != MascotMood.FAST)) {
c.drawArc(oval(cx - 4.5f, cy - 3f, cx + 4.5f, cy + 2f), 20f, 140f, false, line);
return;
}
if (mood == MascotMood.FAST) {
// ^ ^
c.drawArc(oval(cx - 4.5f, cy - 2f, cx + 4.5f, cy + 5f), 200f, 140f, false, line);
return;
}
float look = mood == MascotMood.REVERSE ? -2f : 0f;
oval(cx - 4.6f, cy - 5.6f, cx + 4.6f, cy + 5.6f);
if (mood == MascotMood.REVERSE) {
fill.setColor(Color.WHITE);
c.drawOval(r, fill);
fill.setColor(iris);
c.drawOval(oval(cx - 3f + look, cy - 4f, cx + 3f + look, cy + 4.5f), fill);
} else {
fill.setColor(iris);
c.drawOval(r, fill);
fill.setColor(irisLight);
c.drawOval(oval(cx - 3.6f, cy + 0.5f, cx + 3.6f, cy + 5f), fill);
}
fill.setColor(Color.WHITE);
if (mood == MascotMood.CHEERING) {
star(c, cx, cy - 0.5f, 3.6f);
} else {
c.drawCircle(cx - 1.6f + look, cy - 2.4f, 2f, fill);
c.drawCircle(cx + 1.8f + look, cy + 2.4f, 0.9f, fill);
}
if (mood == MascotMood.DETERMINED) {
// A flat lid over the top: focused.
fill.setColor(lid);
c.drawRect(cx - 5.2f, cy - 6.4f, cx + 5.2f, cy - 2f, fill);
line.setStrokeWidth(1.6f);
c.drawLine(cx - 5f, cy - 2f, cx + 5f, cy - 2f, line);
return;
}
line.setStrokeWidth(1.6f);
c.drawArc(oval(cx - 5f, cy - 5.9f, cx + 5f, cy + 5.6f), 200f, 140f, false, line);
}
private void brows(Canvas c, MascotMood mood) {
line.setColor(ink);
line.setStrokeWidth(1.4f);
if (mood == MascotMood.DETERMINED) {
c.drawLine(34f, 33.5f, 43.5f, 36.5f, line);
c.drawLine(56.5f, 36.5f, 66f, 33.5f, line);
} else if (mood == MascotMood.WORRIED) {
c.drawLine(34f, 36f, 43f, 33f, line);
c.drawLine(57f, 33f, 66f, 36f, line);
}
}
private void mouth(Canvas c, MascotMood mood, long t) {
line.setColor(ink);
line.setStrokeWidth(1.2f);
switch (mood) {
case CHEERING:
case FAST:
p.reset();
p.moveTo(45f, 53.5f);
p.quadTo(50f, 61.5f, 55f, 53.5f);
p.close();
fill.setColor(mouth);
c.drawPath(p, fill);
fill.setColor(tongue);
c.drawOval(oval(47.5f, 56.4f, 52.5f, 58.4f), fill);
c.drawPath(p, line);
break;
case DETERMINED:
fill.setColor(Color.WHITE);
oval(44.5f, 53f, 55.5f, 57f);
c.drawRoundRect(r, 1.5f, 1.5f, fill);
c.drawRoundRect(r, 1.5f, 1.5f, line);
c.drawLine(44.8f, 55f, 55.2f, 55f, line);
break;
case WORRIED:
p.reset();
p.moveTo(45.5f, 56f);
p.quadTo(47f, 54.3f, 48.5f, 56f);
p.quadTo(50f, 57.7f, 51.5f, 56f);
p.quadTo(53f, 54.3f, 54.5f, 56f);
c.drawPath(p, line);
break;
case SLEEPING: {
fill.setColor(mouth);
c.drawOval(oval(48.6f, 54.5f, 51.4f, 57f), fill);
// A nose bubble that swells and pops.
float grow = (t % 2400) / 2400f;
fill.setColor(0x7090D8FF);
c.drawCircle(53.5f, 51f, 1f + grow * 4f, fill);
break;
}
case REVERSE:
fill.setColor(mouth);
c.drawOval(oval(48.6f, 54f, 51.4f, 57f), fill);
break;
case WAITING:
c.drawLine(47.5f, 55.5f, 52.5f, 55.5f, line);
break;
default:
// A cat smile: w.
p.reset();
p.moveTo(45.5f, 54f);
p.quadTo(47.75f, 57.5f, 50f, 54.5f);
p.quadTo(52.25f, 57.5f, 54.5f, 54f);
c.drawPath(p, line);
}
}
private void zs(Canvas c, long t) {
float phase = (t % 3000) / 3000f;
float[][] zs = { { 20f, 24f, 6f }, { 13f, 14f, 7.5f }, { 5f, 4f, 9f } };
for (float[] z : zs) {
text.setTextSize(z[2]);
text.setColor(AccentColor.withAlpha(accentLight, Math.round(255 * (1f - phase * 0.5f))));
c.drawText("Z", z[0] - phase * 2f, z[1] + z[2] - phase * 3f, text);
}
}
private void sparkles(Canvas c, long t) {
float[][] at = { { 14f, 30f }, { 86f, 26f }, { 12f, 62f }, { 88f, 60f }, { 50f, 1.5f } };
for (int i = 0; i < at.length; i++) {
float s = 2.4f + 1.8f * (float) Math.abs(Math.sin(t / 260.0 + i * 1.3));
fill.setColor(i % 2 == 0 ? regen : Color.WHITE);
star(c, at[i][0], at[i][1], s);
}
}
private void wind(Canvas c, long t) {
line.setStrokeWidth(1f);
line.setColor(0x88FFFFFF);
for (int i = 0; i < 5; i++) {
float x = 18f - ((t / 6f + i * 37f) % 20f);
float y = 20f + i * 14f;
c.drawLine(x - 10f, y, x, y, line);
}
line.setColor(ink);
}
private void question(Canvas c, long t) {
text.setTextSize(14f);
text.setColor(accentLight);
float bob = (float) Math.sin(t / 400.0) * 1.2f;
c.drawText("?", 16f, 18f + bob, text);
}
protected void drop(Canvas c, float x, float y, float s) {
p.reset();
p.moveTo(x, y - s * 1.6f);
p.quadTo(x + s * 1.2f, y + s * 0.2f, x, y + s);
p.quadTo(x - s * 1.2f, y + s * 0.2f, x, y - s * 1.6f);
c.drawPath(p, fill);
}
/** A four-pointed sparkle; leaves its outline in {@link #p}. */
protected void star(Canvas c, float x, float y, float s) {
p.reset();
p.moveTo(x, y - s);
p.quadTo(x, y, x + s, y);
p.quadTo(x, y, x, y + s);
p.quadTo(x, y, x - s, y);
p.quadTo(x, y, x, y - s);
p.close();
c.drawPath(p, fill);
}
protected RectF oval(float left, float top, float right, float bottom) {
r.set(left, top, right, bottom);
return r;
}
}
@@ -1,142 +0,0 @@
package com.evsuite.abrp;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Path;
/**
* The chibi co-pilot for the Mascot Overview style ({@link MascotCharacter#CHIBI}): the small,
* full-figure version — a big round head with a coral bob and a star clip, a hoodie in the
* accent colour, short legs, white trainers — drawn with dark outlines like a sticker.
*
* Her whole body acts the mood: waves while content, throws both arms up and hops while
* cheering (star eyes), pumps a fist when determined, holds her cheeks with a tear when worried,
* sleeps with a nose bubble in P, glances and points back in R, leans into the wind when
* fast, and wonders "?" while waiting. The shared parts are in {@link ChibiBase}.
*/
final class ChibiFigure extends ChibiBase {
private final int hair = 0xFFF0855C, hairLight = 0xFFFFB08A, skin = 0xFFFFE6D6, trousers = 0xFF2D3142,
shoe = 0xFFF6F6F8;
ChibiFigure(int accent, int accentLight, int regen) {
super(accent, accentLight, regen, 0xFF6E45B8, 0xFFA27BE6, 0xFFFFE6D6, 6f);
}
@Override
float tipX() {
return 70f;
}
@Override
float tipY() {
return 34f;
}
@Override
protected void drawFigure(Canvas c, MascotMood mood, long t, boolean blink) {
drawLegs(c);
drawBody(c);
boolean inFront = handsInFront(mood);
if (!inFront) drawArms(c, mood, t);
drawHead(c, mood, t, blink);
if (inFront) drawArms(c, mood, t);
}
private void drawLegs(Canvas c) {
fill.setColor(trousers);
line.setStrokeWidth(1.2f);
for (float x : new float[] { 42f, 52f }) {
oval(x, 84f, x + 6f, 95f);
c.drawRect(r, fill);
c.drawRect(r, line);
}
fill.setColor(shoe);
for (float x : new float[] { 39.5f, 50.5f }) {
oval(x, 92.5f, x + 10f, 98.5f);
c.drawOval(r, fill);
c.drawOval(r, line);
}
}
private void drawBody(Canvas c) {
p.reset();
p.moveTo(35.5f, 88f);
p.lineTo(38f, 67f);
p.quadTo(50f, 60f, 62f, 67f);
p.lineTo(64.5f, 88f);
p.quadTo(50f, 91f, 35.5f, 88f);
p.close();
fill.setColor(accent);
c.drawPath(p, fill);
line.setStrokeWidth(1.2f);
c.drawPath(p, line);
// The pocket and the drawstrings.
line.setStrokeWidth(0.9f);
p.reset();
p.moveTo(42.5f, 88.5f);
p.lineTo(43.5f, 79.5f);
p.quadTo(50f, 78f, 56.5f, 79.5f);
p.lineTo(57.5f, 88.5f);
c.drawPath(p, line);
line.setColor(Color.WHITE);
line.setStrokeWidth(1f);
c.drawLine(46.5f, 66f, 46f, 74f, line);
c.drawLine(53.5f, 66f, 54f, 74f, line);
line.setColor(ink);
}
private void drawArms(Canvas c, MascotMood mood, long t) {
float[] h = hands(mood, t);
limb(c, 39.5f, 69f, h[0], h[1], accent, skin);
limb(c, 60.5f, 69f, h[2], h[3], accent, skin);
}
private void drawHead(Canvas c, MascotMood mood, long t, boolean blink) {
c.save();
c.rotate(tilt(mood), 50f, 62f);
// The bob behind the face.
fill.setColor(hair);
line.setStrokeWidth(1.3f);
p.reset();
p.addOval(oval(20f, 5f, 80f, 62f), Path.Direction.CW);
q.reset();
q.addRoundRect(oval(19.5f, 30f, 80.5f, 64f), 7f, 7f, Path.Direction.CW);
p.op(q, Path.Op.UNION);
c.drawPath(p, fill);
c.drawPath(p, line);
// The face.
fill.setColor(skin);
oval(25f, 15f, 75f, 65f);
c.drawOval(r, fill);
c.drawOval(r, line);
// Bangs.
p.reset();
p.moveTo(22.5f, 40f);
p.cubicTo(19f, 7f, 81f, 7f, 77.5f, 40f);
p.lineTo(73f, 27f);
p.lineTo(66f, 34f);
p.lineTo(60f, 23f);
p.lineTo(52f, 32f);
p.lineTo(45f, 22f);
p.lineTo(38f, 32f);
p.lineTo(32f, 25f);
p.lineTo(27f, 35f);
p.close();
fill.setColor(hair);
c.drawPath(p, fill);
c.drawPath(p, line);
line.setColor(hairLight);
line.setStrokeWidth(1.6f);
c.drawArc(oval(30f, 10f, 56f, 30f), 200f, 60f, false, line);
line.setColor(ink);
// The star clip.
fill.setColor(accentLight);
star(c, 70f, 19f, 4.5f);
line.setStrokeWidth(0.8f);
c.drawPath(p, line);
face(c, mood, t, blink);
c.restore();
}
}
@@ -1,7 +1,7 @@
package com.evsuite.abrp;
/**
* One reading for the drawn Overview styles (Retro, Futuristic, Sci-fi, Cartoon, Blueprint):
* One reading for the drawn Overview styles (Retro, Futuristic, Sci-fi, Cartoon, MG and the canvas styles):
* 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 {
@@ -1,206 +0,0 @@
package com.evsuite.abrp;
import android.graphics.Canvas;
import android.graphics.Path;
/**
* The chibi fox for the Mascot Overview style ({@link MascotCharacter#FOX}): a sitting fox with
* a big head, pointed ears, cream cheeks and chest, dark paws, a scarf in the accent colour and
* a large brush tail with a cream tip.
*
* Ears and tail act the mood with the face ({@link ChibiBase}): the tail wags while content and
* fast while cheering (paws up, star eyes), stands stiff when determined (paw at the chest),
* hangs low with drooping ears when worried (paws on the cheeks), lies flat in P (ears down,
* nose bubble); ears go back and the scarf flies when fast; a paw points back in R.
*/
final class FoxFigure extends ChibiBase {
private final int fur = 0xFFF08A3C, furLight = 0xFFFFB46E, cream = 0xFFFFF4E6, paw = 0xFF4A3028,
earInside = 0xFF5A3A30;
FoxFigure(int accent, int accentLight, int regen) {
super(accent, accentLight, regen, 0xFF9A5A1A, 0xFFE0A040, 0xFFF08A3C, 10f);
}
@Override
float tipX() {
return 66f;
}
@Override
float tipY() {
return 38f;
}
@Override
protected void drawFigure(Canvas c, MascotMood mood, long t, boolean blink) {
drawTail(c, mood, t);
drawBody(c);
drawScarf(c, mood, t);
boolean inFront = handsInFront(mood);
if (!inFront) drawPaws(c, mood, t);
drawHead(c, mood, t, blink);
if (inFront) drawPaws(c, mood, t);
}
/** The brush, rotated about its root by the mood; wagging is a swing over time. */
private void drawTail(Canvas c, MascotMood mood, long t) {
// Kept within about 18° to the right, so the tail stays clear of the values.
float angle;
switch (mood) {
case CONTENT: angle = (float) Math.sin(t / 250.0) * 8f; break;
case CHEERING: angle = (float) Math.sin(t / 110.0) * 14f; break;
case DETERMINED: angle = -8f; break;
case WORRIED: angle = 15f; break;
case SLEEPING: angle = 18f; break;
case FAST: angle = 16f; break;
case REVERSE: angle = (float) Math.sin(t / 400.0) * 5f; break;
default: angle = (float) Math.sin(t / 600.0) * 4f;
}
c.save();
c.rotate(angle, 60f, 90f);
p.reset();
p.moveTo(57f, 92f);
p.cubicTo(80f, 96f, 94f, 78f, 88f, 56f);
p.cubicTo(86f, 46f, 78f, 40f, 73f, 44f);
p.cubicTo(78f, 56f, 76f, 74f, 57f, 86f);
p.close();
fill.setColor(fur);
c.drawPath(p, fill);
c.save();
c.clipPath(p);
fill.setColor(cream);
c.drawCircle(83f, 46f, 10f, fill);
c.restore();
line.setStrokeWidth(1.3f);
c.drawPath(p, line);
c.restore();
}
private void drawBody(Canvas c) {
p.reset();
p.moveTo(37f, 93f);
p.cubicTo(33f, 80f, 40f, 66f, 50f, 66f);
p.cubicTo(60f, 66f, 67f, 80f, 63f, 93f);
p.quadTo(50f, 96f, 37f, 93f);
p.close();
fill.setColor(fur);
c.drawPath(p, fill);
line.setStrokeWidth(1.2f);
c.drawPath(p, line);
fill.setColor(cream);
c.drawOval(oval(43f, 71f, 57f, 91f), fill);
// Hind paws.
fill.setColor(paw);
for (float x : new float[] { 37.5f, 52.5f }) {
oval(x, 90f, x + 10f, 97.5f);
c.drawOval(r, fill);
c.drawOval(r, line);
}
}
/** The scarf round the neck; its end hangs, or flies back when fast. */
private void drawScarf(Canvas c, MascotMood mood, long t) {
fill.setColor(accent);
line.setStrokeWidth(1.1f);
if (mood == MascotMood.FAST) {
float flap = (float) Math.sin(t / 90.0) * 2f;
p.reset();
p.moveTo(40f, 64f);
p.quadTo(30f, 60f + flap, 20f, 63f - flap);
p.lineTo(21f, 69f - flap);
p.quadTo(30f, 67f + flap, 40f, 70f);
p.close();
} else {
p.reset();
p.addRoundRect(oval(54f, 65f, 61f, 80f), 2f, 2f, Path.Direction.CW);
}
c.drawPath(p, fill);
c.drawPath(p, line);
oval(36f, 62f, 64f, 70f);
c.drawRoundRect(r, 4f, 4f, fill);
c.drawRoundRect(r, 4f, 4f, line);
}
private void drawPaws(Canvas c, MascotMood mood, long t) {
float[] h = hands(mood, t);
limb(c, 41f, 72f, h[0], h[1], fur, paw);
limb(c, 59f, 72f, h[2], h[3], fur, paw);
}
private void drawHead(Canvas c, MascotMood mood, long t, boolean blink) {
c.save();
c.rotate(tilt(mood), 50f, 62f);
// Ears behind the head, drooping or laid back by the mood; the right one mirrored.
float droop = mood == MascotMood.WORRIED ? 38f : mood == MascotMood.SLEEPING ? 26f
: mood == MascotMood.FAST ? 22f : mood == MascotMood.CONTENT ? (float) Math.sin(t / 900.0) * 3f : 0f;
ear(c, droop);
c.save();
c.scale(-1f, 1f, 50f, 0f);
ear(c, droop);
c.restore();
// The head: round on top, a tuft at each cheek.
p.reset();
p.moveTo(50f, 16f);
p.cubicTo(66f, 16f, 78f, 26f, 79f, 40f);
p.lineTo(86f, 52f);
p.lineTo(77f, 53f);
p.cubicTo(70f, 63f, 60f, 66f, 50f, 66f);
p.cubicTo(40f, 66f, 30f, 63f, 23f, 53f);
p.lineTo(14f, 52f);
p.lineTo(21f, 40f);
p.cubicTo(22f, 26f, 34f, 16f, 50f, 16f);
p.close();
fill.setColor(fur);
c.drawPath(p, fill);
// Cream cheeks and muzzle, inside the head.
c.save();
c.clipPath(p);
fill.setColor(cream);
c.drawOval(oval(22f, 47f, 78f, 84f), fill);
c.drawOval(oval(12f, 45f, 34f, 60f), fill);
c.drawOval(oval(66f, 45f, 88f, 60f), fill);
c.restore();
line.setStrokeWidth(1.3f);
c.drawPath(p, line);
line.setColor(furLight);
line.setStrokeWidth(1.6f);
c.drawArc(oval(30f, 19f, 56f, 36f), 200f, 60f, false, line);
line.setColor(ink);
// Whiskers, then the nose and the face.
line.setStrokeWidth(0.6f);
c.drawLine(28f, 53f, 19f, 51f, line);
c.drawLine(28f, 55.5f, 19f, 56.5f, line);
c.drawLine(72f, 53f, 81f, 51f, line);
c.drawLine(72f, 55.5f, 81f, 56.5f, line);
face(c, mood, t, blink);
fill.setColor(ink);
c.drawOval(oval(47.6f, 49f, 52.4f, 52f), fill);
c.restore();
}
/** The left ear, rotated outwards about its root by {@code droop} degrees. */
private void ear(Canvas c, float droop) {
c.save();
c.rotate(-droop, 35f, 23f);
p.reset();
p.moveTo(23f, 34f);
p.lineTo(26f, 3f);
p.lineTo(47f, 20f);
p.close();
fill.setColor(fur);
c.drawPath(p, fill);
line.setStrokeWidth(1.3f);
c.drawPath(p, line);
p.reset();
p.moveTo(27.5f, 27f);
p.lineTo(29f, 10f);
p.lineTo(40f, 20f);
p.close();
fill.setColor(earInside);
c.drawPath(p, fill);
c.restore();
}
}
@@ -64,12 +64,10 @@ public class MainActivity extends AppCompatActivity {
private FlowView flowView;
private SciFiView sciFiView;
private CartoonView cartoonView;
private BlueprintView blueprintView;
private OctagonView octagonView;
/** The styles built on {@link ClusterCanvasView}, fed together. */
private final java.util.List<ClusterCanvasView> canvasStyles = new java.util.ArrayList<>();
private TerminalView terminalView;
private MascotView mascotView;
/** The pane of each Overview style, by its {@link OverviewStyle} ordinal. */
private View[] stylePanes;
private View consumptionPane;
@@ -316,8 +314,10 @@ public class MainActivity extends AppCompatActivity {
getDelegate().setLocalNightMode(AppCompatDelegate.MODE_NIGHT_YES);
super.onCreate(savedInstanceState);
prefs = getSharedPreferences("abrp_prefs", MODE_PRIVATE);
// Before anything is inflated: every ?attr/evAccent… resolves against this overlay.
getTheme().applyStyle(AccentColor.fromPref(prefs.getString(AccentColor.PREF_KEY, null)).overlay, true);
// Before anything is inflated: every ?attr/evAccent… resolves against this overlay — the
// accent colour, or the Overview style's own palette while it shows that (StylePalette).
appliedOverlay = appOverlay();
getTheme().applyStyle(appliedOverlay, true);
setContentView(R.layout.activity_main);
@@ -338,21 +338,17 @@ public class MainActivity extends AppCompatActivity {
flowView = futuristicPane.findViewById(R.id.flow_view);
sciFiView = new SciFiView(this, null);
cartoonView = new CartoonView(this, null);
blueprintView = new BlueprintView(this, null);
octagonView = new OctagonView(this, null);
View sciFiPane = drawnPane(sciFiView), cartoonPane = drawnPane(cartoonView),
blueprintPane = drawnPane(blueprintView), mgPane = drawnPane(octagonView);
mgPane = drawnPane(octagonView);
terminalView = new TerminalView(this);
mascotView = new MascotView(this);
mascotView.setCharacter(MascotCharacter.fromPref(prefs.getString(MascotCharacter.PREF_KEY, null)));
java.util.Collections.addAll(canvasStyles, new MinimalView(this), terminalView,
new AviationView(this), new NixieView(this), new ChronographView(this), mascotView);
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,
futuristicPane, sciFiPane, cartoonPane, blueprintPane, mgPane,
canvasPanes[0], canvasPanes[1], canvasPanes[2], canvasPanes[3], canvasPanes[4],
canvasPanes[5] };
futuristicPane, sciFiPane, cartoonPane, mgPane,
canvasPanes[0], canvasPanes[1], canvasPanes[2], canvasPanes[3], canvasPanes[4] };
// The Overview page holds every style; Settings → Overview style picks the one shown.
android.widget.FrameLayout overviewPage = new android.widget.FrameLayout(this);
overviewPage.setLayoutParams(new android.view.ViewGroup.LayoutParams(
@@ -365,7 +361,6 @@ public class MainActivity extends AppCompatActivity {
overviewPage.addView(futuristicPane);
overviewPage.addView(sciFiPane);
overviewPage.addView(cartoonPane);
overviewPage.addView(blueprintPane);
overviewPage.addView(mgPane);
for (View pane : canvasPanes) overviewPage.addView(pane);
consumptionPane = inflatePane(R.layout.pane_consumption);
@@ -1189,7 +1184,6 @@ public class MainActivity extends AppCompatActivity {
voiceKeySwitch.setOnCheckedChangeListener((btn, checked) ->
prefs.edit().putBoolean(UploadSettings.KEY_VOICE_KEY_OPENS, checked).apply());
bindTireUnit();
bindMascotCharacter();
bindDriveMode();
bindOverviewStyle();
bindAccentColor();
@@ -1294,7 +1288,7 @@ public class MainActivity extends AppCompatActivity {
/**
* The accent colour; a change recreates the activity so every view takes it. For a style
* with an original palette, "Original colours" comes first: it keeps the style in its own
* colours and leaves the app's accent as it is.
* colours, and the app takes them on too (StylePalette).
*/
private void bindAccentColor() {
OverviewStyle style = overviewStyle();
@@ -1307,17 +1301,10 @@ public class MainActivity extends AppCompatActivity {
int selected = originalColours(style) ? 0 : current.ordinal() + offset;
bindChoices(settingsPane.findViewById(R.id.accent_choices), labels, selected, position -> {
if (style.hasOriginal) prefs.edit().putBoolean(style.originalKey(), position == 0).apply();
if (position < offset) {
applyOverviewStyle();
return;
if (position >= offset) {
prefs.edit().putString(AccentColor.PREF_KEY, colors[position - offset].name()).apply();
}
AccentColor picked = colors[position - offset];
if (picked == current) {
applyOverviewStyle();
return;
}
prefs.edit().putString(AccentColor.PREF_KEY, picked.name()).apply();
recreate();
applyStyleOrRecreate();
});
applyAccentAvailability();
}
@@ -1332,7 +1319,6 @@ public class MainActivity extends AppCompatActivity {
prefs.edit().putString(PressureUnit.PREF_KEY, units[position].name()).apply());
}
/** Who sits in the Mascot style; the drop-down shows only while that style is chosen. */
private static final int[] DRIVE_MODE_IDS = { R.id.drive_mode_eco, R.id.drive_mode_normal,
R.id.drive_mode_sport, R.id.drive_mode_snow, R.id.drive_mode_custom };
private static final int[] DRIVE_MODE_LABELS = { R.string.drive_mode_eco, R.string.drive_mode_normal,
@@ -1443,17 +1429,6 @@ public class MainActivity extends AppCompatActivity {
}
}
private void bindMascotCharacter() {
MascotCharacter[] characters = MascotCharacter.values();
String[] labels = { getString(R.string.mascot_character_classic), getString(R.string.mascot_character_chibi),
getString(R.string.mascot_character_fox) };
int current = MascotCharacter.fromPref(prefs.getString(MascotCharacter.PREF_KEY, null)).ordinal();
bindChoices(settingsPane.findViewById(R.id.mascot_choices), labels, current, position -> {
prefs.edit().putString(MascotCharacter.PREF_KEY, characters[position].name()).apply();
mascotView.setCharacter(characters[position]);
});
}
/** The service reads these settings on every tick, so a change applies without a restart. */
private void bindCadenceControls() {
UploadSettings current = UploadSettings.from(prefs);
@@ -1712,6 +1687,24 @@ public class MainActivity extends AppCompatActivity {
return style;
}
/** The theme overlay applied in onCreate; a choice that changes it recreates the activity. */
private int appliedOverlay;
private int appOverlay() {
OverviewStyle style = overviewStyle();
return StylePalette.appOverlay(style, originalColours(style),
AccentColor.fromPref(prefs.getString(AccentColor.PREF_KEY, null)));
}
/**
* After a style or colour choice: the app's colours follow at once (the activity is
* recreated when they change, as for the accent colour), otherwise the Overview is redrawn.
*/
private void applyStyleOrRecreate() {
if (appOverlay() != appliedOverlay) recreate();
else applyOverviewStyle();
}
/** Whether a style with an original palette shows it (the default) or the accent colour. */
private boolean originalColours(OverviewStyle style) {
return style.hasOriginal && prefs.getBoolean(style.originalKey(), true);
@@ -1742,10 +1735,6 @@ public class MainActivity extends AppCompatActivity {
TextView note = settingsPane.findViewById(R.id.accent_note);
note.setVisibility(style.usesAccent ? View.GONE : View.VISIBLE);
note.setText(getString(R.string.accent_not_used, styleLabels()[style.ordinal()]));
// The wide layout keeps the Mascot field's place so the page does not jump.
View mascot = settingsPane.findViewById(R.id.mascot_field);
boolean wide = getResources().getConfiguration().screenWidthDp >= 1000;
mascot.setVisibility(style == OverviewStyle.MASCOT ? View.VISIBLE : wide ? View.INVISIBLE : View.GONE);
}
/** The drop-down's labels, by {@link OverviewStyle} ordinal. */
@@ -1754,10 +1743,10 @@ public class MainActivity extends AppCompatActivity {
getString(R.string.overview_style_cockpit), getString(R.string.overview_style_retro),
getString(R.string.overview_style_futuristic),
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_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), getString(R.string.overview_style_mascot) };
getString(R.string.overview_style_chronograph) };
}
/** The style drop-down; every style is fed on every reading, only the chosen one is shown. */
@@ -1770,7 +1759,7 @@ public class MainActivity extends AppCompatActivity {
bindChoices(field, styleLabels(), overviewStyle().ordinal(),
position -> {
prefs.edit().putString(PREF_OVERVIEW_STYLE, styles[position].name()).apply();
applyOverviewStyle();
applyStyleOrRecreate();
});
}
@@ -1827,7 +1816,6 @@ public class MainActivity extends AppCompatActivity {
flowView.setValues(v);
sciFiView.setValues(v);
cartoonView.setValues(v);
blueprintView.setValues(v);
octagonView.setValues(v);
for (ClusterCanvasView view : canvasStyles) view.setValues(v);
}
@@ -1,24 +0,0 @@
package com.evsuite.abrp;
/**
* Who sits in the Mascot Overview style, in the order of the Settings drop-down: the classic
* co-pilot (head and shoulders, headset, racing jacket), the chibi (small, full figure, big
* head, hoodie, bubbly) or the chibi fox (sitting, scarf, a tail that wags). Each has their
* own drawing and lines; all follow {@link MascotMood}.
* Pure Java, unit-tested.
*/
enum MascotCharacter {
CLASSIC,
CHIBI,
FOX;
static final String PREF_KEY = "mascot_character";
/** The stored character; anything unknown is the classic one. */
static MascotCharacter fromPref(String stored) {
for (MascotCharacter character : values()) {
if (character.name().equals(stored)) return character;
}
return CLASSIC;
}
}
@@ -1,70 +0,0 @@
package com.evsuite.abrp;
/**
* What the Mascot Overview style's co-pilot feels, from one reading ({@link #of}), and a
* {@link Holder} that keeps her from flickering between moods when a value hovers at a
* threshold. Pure Java, unit-tested.
*/
enum MascotMood {
/** Nothing read yet (the emulator, or the car's services not up). */
WAITING,
/** Reversing: looks back, careful. */
REVERSE,
/** Battery under {@link #LOW_SOC} %: worried, a sweat drop. */
WORRIED,
/** In P: asleep. */
SLEEPING,
/** Regenerating more than {@link #REGEN_KW} kW: cheering. */
CHEERING,
/** Drawing more than {@link #BOOST_KW} kW: determined. */
DETERMINED,
/** {@link #FAST_KMH} km/h and more: enjoying the speed, wind in her hair. */
FAST,
/** Anything else: content. */
CONTENT;
static final float LOW_SOC = 20f, REGEN_KW = 5f, BOOST_KW = 60f, FAST_KMH = 100f;
/** How long a new mood must last before she shows it. */
static final long HOLD_MS = 1500;
/** The mood for one reading; unread values never count as a reason. */
static MascotMood of(ClusterValues v) {
if (v == null || (v.gear == null && v.speedKmh == null && v.socPercent == null && v.powerKw == null)) {
return WAITING;
}
if (v.gear != null && v.gear == 2) return REVERSE;
if (v.socPercent != null && v.socPercent < LOW_SOC) return WORRIED;
if (v.gear != null && v.gear == 1) return SLEEPING;
if (v.powerKw != null && v.powerKw < -REGEN_KW) return CHEERING;
if (v.powerKw != null && v.powerKw > BOOST_KW) return DETERMINED;
if (v.speedKmh != null && v.speedKmh >= FAST_KMH) return FAST;
return CONTENT;
}
/** Moods that show at once: shifting into R and losing the data are not to be waited out. */
boolean immediate() {
return this == REVERSE || this == WAITING;
}
/** The shown mood: a new one takes over after it has lasted {@link #HOLD_MS}. */
static final class Holder {
private MascotMood shown, candidate;
private long candidateSince;
MascotMood update(MascotMood now, long nowMs) {
if (shown == null || now == shown || now.immediate()) {
shown = now;
candidate = null;
return shown;
}
if (now != candidate) {
candidate = now;
candidateSince = nowMs;
} else if (nowMs - candidateSince >= HOLD_MS) {
shown = now;
candidate = null;
}
return shown;
}
}
}
@@ -1,536 +0,0 @@
package com.evsuite.abrp;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.RectF;
import android.graphics.Typeface;
import android.os.SystemClock;
/**
* The Mascot Overview style: an anime co-pilot beside the values, who reacts to the drive.
*
* On the left, head and shoulders: a young woman with dark plum hair, a headset with a boom mic
* and a racing jacket in the accent colour with an octagon badge. Her face follows
* {@link MascotMood} (held for a moment so it does not flicker): content, enjoying the speed
* with wind lines, cheering with sparkles while regenerating, determined under hard
* acceleration, worried with a sweat drop on a low battery, asleep with drifting Zs in P,
* glancing back in R, and patient while nothing is read. She blinks now and then and says a
* line for her mood in a speech bubble.
*
* On the right: speed large, the gear in a ring, battery as a bar with percent and range, then
* power, 5-minute usage and the trip; the clock top right, left of the full-screen button.
* Unread values show "—".
*
* Three characters ({@link MascotCharacter}): this classic co-pilot, the chibi
* ({@link ChibiFigure}) or the chibi fox ({@link FoxFigure}), each with their own lines.
*
* Drawn on a 100 × 100 grid scaled to the pane. Nothing animates on its own: blinking, the Zs,
* sparkles and wind move because readings redraw the view every 0.2 s.
*/
final class MascotView extends ClusterCanvasView {
/** Her lines per {@link MascotCharacter}, then per {@link MascotMood}, in their orders. */
private static final int[][] LINES = {
{ R.array.mascot_waiting, R.array.mascot_reverse, R.array.mascot_worried, R.array.mascot_sleeping,
R.array.mascot_cheering, R.array.mascot_determined, R.array.mascot_fast, R.array.mascot_content },
{ R.array.chibi_waiting, R.array.chibi_reverse, R.array.chibi_worried, R.array.chibi_sleeping,
R.array.chibi_cheering, R.array.chibi_determined, R.array.chibi_fast, R.array.chibi_content },
{ R.array.fox_waiting, R.array.fox_reverse, R.array.fox_worried, R.array.fox_sleeping,
R.array.fox_cheering, R.array.fox_determined, R.array.fox_fast, R.array.fox_content } };
private final Paint fill = new Paint(Paint.ANTI_ALIAS_FLAG);
private final Paint stroke = new Paint(Paint.ANTI_ALIAS_FLAG);
private final Paint text = new Paint(Paint.ANTI_ALIAS_FLAG);
private final Path p = new Path();
private final RectF r = new RectF();
private final MascotMood.Holder holder = new MascotMood.Holder();
private final String[][][] lines = new String[LINES.length][][];
/** The drawn figures by {@link MascotCharacter} ordinal; null for the classic one, drawn here. */
private final ChibiBase[] figures;
private MascotCharacter character = MascotCharacter.CLASSIC;
private final int accent, accentDark, accentLight, regen, warn, white, grey, track;
private final int hair = 0xFF2E2440, hairLight = 0xFF5A4880, skin = 0xFFFBE2D0, skinShade = 0xFFE9BFA9,
iris = 0xFF2F8F9D, pupil = 0xFF123238, lash = 0xFF221A2E, lips = 0xFFC2606A, mouth = 0xFF8E3A48,
shirt = 0xFFF4F4F6, headset = 0xFF2A2D33, cup = 0xFF3C414A;
MascotView(Context context) {
super(context);
accent = AccentColor.resolve(context, R.attr.evAccent);
accentLight = AccentColor.resolve(context, R.attr.evAccentLight);
accentDark = Color.rgb(Math.round(Color.red(accent) * 0.6f), Math.round(Color.green(accent) * 0.6f),
Math.round(Color.blue(accent) * 0.6f));
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);
for (int i = 0; i < LINES.length; i++) {
lines[i] = new String[LINES[i].length][];
for (int j = 0; j < LINES[i].length; j++) lines[i][j] = context.getResources().getStringArray(LINES[i][j]);
}
figures = new ChibiBase[] { null, new ChibiFigure(accent, accentLight, regen),
new FoxFigure(accent, accentLight, regen) };
stroke.setStyle(Paint.Style.STROKE);
stroke.setStrokeCap(Paint.Cap.ROUND);
stroke.setStrokeJoin(Paint.Join.ROUND);
text.setTypeface(Typeface.create("sans-serif-medium", Typeface.NORMAL));
text.setFontFeatureSettings("tnum");
}
/** Who sits beside the values (Settings → Mascot, shown for this style only). */
void setCharacter(MascotCharacter character) {
this.character = character == null ? MascotCharacter.CLASSIC : character;
invalidate();
}
@Override
protected void onDraw(Canvas c) {
super.onDraw(c);
float w = getWidth(), h = getHeight();
if (w <= 0 || h <= 0) return;
long t = SystemClock.uptimeMillis();
MascotMood mood = holder.update(MascotMood.of(v), t);
// A soft halo behind her.
float k = h * 0.98f / 100f, ox = w * 0.02f, oy = h - 100f * k;
fill.setColor(AccentColor.withAlpha(accent, 0x30));
c.drawCircle(ox + 50f * k, oy + 46f * k, 46f * k, fill);
c.save();
c.translate(ox, oy);
c.scale(k, k);
ChibiBase figure = figures[character.ordinal()];
if (figure != null) figure.draw(c, mood, t, t % 4200 < 260);
else drawCharacter(c, mood, t);
c.restore();
float x0 = ox + 78f * k;
float tipX = figure != null ? figure.tipX() : 70f, tipY = figure != null ? figure.tipY() : 46f;
drawBubble(c, mood, t, x0, h, ox + tipX * k, oy + tipY * k, w);
drawValues(c, x0, ox + 93f * k, w, h);
}
// ---------- The co-pilot, on the 100 × 100 grid ----------
private void drawCharacter(Canvas c, MascotMood mood, long t) {
float tilt = mood == MascotMood.SLEEPING ? 8f : mood == MascotMood.REVERSE ? -6f
: mood == MascotMood.CHEERING ? -4f : 0f;
// Hair behind the head, falling behind the shoulders.
c.save();
c.rotate(tilt, 50f, 62f);
fill.setColor(hair);
c.drawOval(oval(27f, 9f, 73f, 57f), fill);
p.reset();
p.moveTo(28.5f, 28f);
p.cubicTo(26f, 46f, 25f, 64f, 22f, 84f);
p.lineTo(78f, 84f);
p.cubicTo(75f, 64f, 74f, 46f, 71.5f, 28f);
p.close();
c.drawPath(p, fill);
c.restore();
// Neck, jacket, shirt collar, badge.
fill.setColor(skin);
c.drawRect(44.5f, 48f, 55.5f, 70f, fill);
fill.setColor(skinShade);
c.drawOval(oval(44f, 50f, 56f, 57f), fill);
p.reset();
p.moveTo(10f, 100f);
p.lineTo(14f, 80f);
p.cubicTo(16f, 71f, 26f, 68f, 40f, 66f);
p.lineTo(60f, 66f);
p.cubicTo(74f, 68f, 84f, 71f, 86f, 80f);
p.lineTo(90f, 100f);
p.close();
fill.setColor(accent);
c.drawPath(p, fill);
triangle(c, 40.5f, 66f, 59.5f, 66f, 50f, 85f, shirt);
triangle(c, 45f, 66f, 55f, 66f, 50f, 74f, skin);
stroke.setColor(accentDark);
stroke.setStrokeWidth(2.2f);
c.drawLine(40.5f, 66.5f, 49f, 90f, stroke);
c.drawLine(59.5f, 66.5f, 51f, 90f, stroke);
stroke.setColor(shirt);
stroke.setStrokeWidth(1.6f);
c.drawLine(16f, 79f, 38f, 70f, stroke);
c.drawLine(84f, 79f, 62f, 70f, stroke);
badge(c, 68f, 86f, 4f);
// The head.
c.save();
c.rotate(tilt, 50f, 62f);
p.reset();
p.moveTo(35f, 28f);
p.cubicTo(35f, 44f, 42f, 55f, 50f, 57f);
p.cubicTo(58f, 55f, 65f, 44f, 65f, 28f);
p.cubicTo(65f, 14f, 35f, 14f, 35f, 28f);
p.close();
fill.setColor(skin);
c.drawPath(p, fill);
boolean blink = t % 4200 < 260;
boolean blush = mood == MascotMood.CHEERING || mood == MascotMood.FAST || mood == MascotMood.SLEEPING;
if (blush) {
fill.setColor(0x55FF7A8A);
c.drawOval(oval(37.5f, 43.2f, 43.5f, 46f), fill);
c.drawOval(oval(56.5f, 43.2f, 62.5f, 46f), fill);
}
eye(c, 43f, 38f, mood, blink, true);
eye(c, 57f, 38f, mood, blink, false);
stroke.setColor(skinShade);
stroke.setStrokeWidth(0.8f);
c.drawLine(50f, 43.5f, 49.3f, 45.2f, stroke);
mouth(c, mood);
drawFrontHair(c);
brows(c, mood);
drawHeadset(c);
if (mood == MascotMood.WORRIED) sweatDrop(c);
c.restore();
if (mood == MascotMood.SLEEPING) sleepZs(c, t);
if (mood == MascotMood.CHEERING) sparkles(c, t);
if (mood == MascotMood.FAST) wind(c, t);
}
private void drawFrontHair(Canvas c) {
fill.setColor(hair);
p.reset();
p.moveTo(33f, 36f);
p.lineTo(33.5f, 24f);
p.cubicTo(36f, 10f, 64f, 10f, 66.5f, 24f);
p.lineTo(67f, 36f);
p.lineTo(64f, 28f);
p.lineTo(61.5f, 33f);
p.lineTo(58.5f, 26.5f);
p.lineTo(55f, 32f);
p.lineTo(51.5f, 25.5f);
p.lineTo(48f, 31.5f);
p.lineTo(44.5f, 26f);
p.lineTo(41.5f, 32.5f);
p.lineTo(38.5f, 27f);
p.lineTo(36f, 34f);
p.close();
c.drawPath(p, fill);
// Side locks framing the face, the right one mirrored.
p.reset();
p.moveTo(33.5f, 26f);
p.cubicTo(31f, 38f, 31f, 50f, 33f, 61f);
p.lineTo(36.2f, 52f);
p.cubicTo(35.5f, 44f, 36f, 36f, 37.5f, 30f);
p.close();
c.drawPath(p, fill);
c.save();
c.scale(-1f, 1f, 50f, 0f);
c.drawPath(p, fill);
c.restore();
stroke.setColor(hairLight);
stroke.setStrokeWidth(1.3f);
c.drawArc(oval(38f, 13.5f, 62f, 30f), 205f, 55f, false, stroke);
}
private void drawHeadset(Canvas c) {
stroke.setColor(headset);
stroke.setStrokeWidth(2.2f);
c.drawArc(oval(31f, 9.5f, 69f, 48f), 180f, 180f, false, stroke);
fill.setColor(cup);
c.drawRoundRect(oval(28.5f, 29f, 36f, 42f), 2.5f, 2.5f, fill);
c.drawRoundRect(oval(64f, 29f, 71.5f, 42f), 2.5f, 2.5f, fill);
fill.setColor(accent);
c.drawRect(29.5f, 33.5f, 30.6f, 37.5f, fill);
c.drawRect(69.4f, 33.5f, 70.5f, 37.5f, fill);
stroke.setStrokeWidth(1f);
p.reset();
p.moveTo(67.5f, 41f);
p.quadTo(66f, 50f, 58.5f, 50.5f);
c.drawPath(p, stroke);
fill.setColor(headset);
c.drawCircle(57.6f, 50.5f, 1.3f, fill);
}
private void eye(Canvas c, float cx, float cy, MascotMood mood, boolean blink, boolean left) {
stroke.setColor(lash);
if (mood == MascotMood.SLEEPING || (blink && mood != MascotMood.CHEERING)) {
// Closed, calm: a lid line curving down.
stroke.setStrokeWidth(1.2f);
c.drawArc(oval(cx - 4f, cy - 3.2f, cx + 4f, cy + 1.6f), 20f, 140f, false, stroke);
return;
}
if (mood == MascotMood.CHEERING) {
// Happy, squeezed shut: ^ ^.
stroke.setStrokeWidth(1.4f);
c.drawArc(oval(cx - 4f, cy - 1.5f, cx + 4f, cy + 4.5f), 200f, 140f, false, stroke);
return;
}
float look = mood == MascotMood.REVERSE ? -1.6f : 0f;
float irisW = mood == MascotMood.WORRIED ? 1.7f : 2.3f;
fill.setColor(Color.WHITE);
oval(cx - 4f, cy - 2.6f, cx + 4f, cy + 2.8f);
c.drawOval(r, fill);
c.save();
p.reset();
p.addOval(r, Path.Direction.CW);
c.clipPath(p);
fill.setColor(iris);
c.drawOval(oval(cx - irisW + look, cy - 2.8f, cx + irisW + look, cy + 2.8f), fill);
fill.setColor(pupil);
c.drawOval(oval(cx - irisW * 0.5f + look, cy - 1.5f, cx + irisW * 0.5f + look, cy + 1.6f), fill);
fill.setColor(Color.WHITE);
c.drawCircle(cx - 0.8f + look, cy - 1.2f, 0.75f, fill);
c.drawCircle(cx + 0.8f + look, cy + 1.2f, 0.35f, fill);
if (mood == MascotMood.DETERMINED) {
// Narrowed: the lid comes down over the top of the eye.
fill.setColor(skin);
c.drawRect(cx - 4.5f, cy - 3.2f, cx + 4.5f, cy - 1.0f, fill);
}
c.restore();
stroke.setStrokeWidth(1.25f);
if (mood == MascotMood.DETERMINED) {
c.drawLine(cx - 4.3f, cy - 0.9f, cx + 4.3f, cy - 1.1f, stroke);
} else {
c.drawArc(oval(cx - 4.4f, cy - 3f, cx + 4.4f, cy + 3.4f), 195f, 150f, false, stroke);
}
// The flick at the outer corner.
float outer = left ? cx - 4.2f : cx + 4.2f, dir = left ? -1f : 1f;
c.drawLine(outer, cy - 0.6f, outer + dir * 1.3f, cy - 1.6f, stroke);
}
private void brows(Canvas c, MascotMood mood) {
stroke.setColor(hair);
stroke.setStrokeWidth(1.1f);
switch (mood) {
case WORRIED:
c.drawLine(39f, 31.5f, 46.5f, 29.3f, stroke);
c.drawLine(53.5f, 29.3f, 61f, 31.5f, stroke);
break;
case DETERMINED:
c.drawLine(39f, 29.6f, 46.8f, 32f, stroke);
c.drawLine(53.2f, 32f, 61f, 29.6f, stroke);
break;
default:
float lift = mood == MascotMood.CHEERING || mood == MascotMood.FAST ? -0.8f : 0f;
c.drawArc(oval(39f, 30f + lift, 47f, 33.5f + lift), 200f, 120f, false, stroke);
c.drawArc(oval(53f, 30f + lift, 61f, 33.5f + lift), 220f, 120f, false, stroke);
}
}
private void mouth(Canvas c, MascotMood mood) {
stroke.setColor(lips);
stroke.setStrokeWidth(1f);
fill.setColor(mouth);
switch (mood) {
case CHEERING:
case FAST:
p.reset();
p.moveTo(45.5f, 48.8f);
p.quadTo(50f, 56f, 54.5f, 48.8f);
p.close();
c.drawPath(p, fill);
fill.setColor(0xFFE88A95);
c.drawOval(oval(47.8f, 51.2f, 52.2f, 52.8f), fill);
break;
case SLEEPING:
case REVERSE:
c.drawOval(oval(48.8f, 49.3f, 51.2f, 51.6f), fill);
break;
case WORRIED:
p.reset();
p.moveTo(46.5f, 51f);
p.quadTo(47.8f, 49.6f, 49.2f, 51f);
p.quadTo(50.6f, 52.4f, 52f, 51f);
p.quadTo(53.2f, 49.8f, 54f, 50.8f);
c.drawPath(p, stroke);
break;
case DETERMINED:
p.reset();
p.moveTo(46f, 50.6f);
p.quadTo(51f, 52.6f, 55f, 49f);
c.drawPath(p, stroke);
break;
default:
c.drawArc(oval(46f, 46.5f, 54f, 51.2f), 25f, 130f, false, stroke);
}
}
private void sweatDrop(Canvas c) {
fill.setColor(0xFF8FD3FF);
p.reset();
p.moveTo(63f, 21f);
p.quadTo(66.5f, 26.5f, 63f, 28f);
p.quadTo(59.5f, 26.5f, 63f, 21f);
c.drawPath(p, fill);
}
/** Three Zs drifting up and to the left, over three seconds. */
private void sleepZs(Canvas c, long t) {
float phase = (t % 3000) / 3000f;
text.setTextAlign(Paint.Align.CENTER);
float[][] zs = { { 24f, 22f, 5f }, { 17f, 14f, 6.5f }, { 9f, 5.5f, 8f } };
for (float[] z : zs) {
text.setTextSize(z[2]);
text.setColor(AccentColor.withAlpha(accentLight, Math.round(255 * (1f - phase * 0.5f))));
c.drawText("Z", z[0] - phase * 2f, z[1] + z[2] - phase * 3f, text);
}
}
/** Four-pointed sparkles around her head, twinkling. */
private void sparkles(Canvas c, long t) {
float[][] at = { { 22f, 20f }, { 78f, 14f }, { 18f, 46f }, { 84f, 40f }, { 50f, 3f } };
for (int i = 0; i < at.length; i++) {
float s = 2.2f + 1.6f * (float) Math.abs(Math.sin(t / 260.0 + i * 1.3));
fill.setColor(i % 2 == 0 ? regen : Color.WHITE);
star(c, at[i][0], at[i][1], s);
}
}
/** Streaks rushing past on her left. */
private void wind(Canvas c, long t) {
stroke.setStrokeWidth(0.9f);
stroke.setColor(0x88FFFFFF);
for (int i = 0; i < 4; i++) {
float x = 26f - ((t / 6f + i * 37f) % 24f);
float y = 16f + i * 10f;
c.drawLine(x - 9f, y, x, y, stroke);
}
}
private void star(Canvas c, float x, float y, float s) {
p.reset();
p.moveTo(x, y - s);
p.quadTo(x, y, x + s, y);
p.quadTo(x, y, x, y + s);
p.quadTo(x, y, x - s, y);
p.quadTo(x, y, x, y - s);
p.close();
c.drawPath(p, fill);
}
/** A small silver octagon on the jacket. */
private void badge(Canvas c, float x, float y, float s) {
p.reset();
for (int i = 0; i < 8; i++) {
double a = Math.toRadians(22.5 + i * 45);
float px = x + s * (float) Math.cos(a), py = y + s * (float) Math.sin(a);
if (i == 0) p.moveTo(px, py);
else p.lineTo(px, py);
}
p.close();
fill.setColor(0xFFD7DBE0);
c.drawPath(p, fill);
text.setTextAlign(Paint.Align.CENTER);
text.setTextSize(3.4f);
text.setColor(accentDark);
c.drawText("MG", x, y + 1.2f, text);
}
private void triangle(Canvas c, float x1, float y1, float x2, float y2, float x3, float y3, int colour) {
p.reset();
p.moveTo(x1, y1);
p.lineTo(x2, y2);
p.lineTo(x3, y3);
p.close();
fill.setColor(colour);
c.drawPath(p, fill);
}
private RectF oval(float left, float top, float right, float bottom) {
r.set(left, top, right, bottom);
return r;
}
// ---------- Speech bubble and values, in pixels ----------
private void drawBubble(Canvas c, MascotMood mood, long t, float x0, float h, float tipX, float tipY, float w) {
String[] options = lines[character.ordinal()][mood.ordinal()];
String line = options.length == 0 ? "" : options[(int) ((t / 30000 + mood.ordinal()) % options.length)];
text.setTextAlign(Paint.Align.LEFT);
float pad = h * 0.045f, room = w - 140f * dp - x0 - 2f * pad;
// Shrink a long line to fit the room left of the clock.
text.setTextSize(h * 0.07f);
float width = text.measureText(line);
if (width > room && room > 0f) {
text.setTextSize(h * 0.07f * room / width);
width = room;
}
float right = x0 + width + 2f * pad;
float top = h * 0.05f, bottom = top + h * 0.07f + 2f * pad;
fill.setColor(0xFFF7F7FA);
c.drawRoundRect(oval(x0, top, right, bottom), h * 0.05f, h * 0.05f, fill);
triangle(c, x0 + h * 0.03f, bottom - 2f, x0 + h * 0.11f, bottom - 2f, tipX, tipY, 0xFFF7F7FA);
text.setColor(0xFF1C1A24);
c.drawText(line, x0 + pad, bottom - pad - h * 0.012f, text);
}
/** Speed and gear from x0; the battery and the bottom row from xLow, clear of her shoulder. */
private void drawValues(Canvas c, float x0, float xLow, float w, float h) {
float right = w - 16f * dp;
// The clock, left of the full-screen button.
text.setTextAlign(Paint.Align.RIGHT);
text.setTextSize(20f * dp);
text.setColor(grey);
c.drawText(v.clock.isEmpty() ? "—" : v.clock, w - 72f * dp, 30f * dp, text);
// Speed, large, and the gear in a ring at the right.
text.setTextAlign(Paint.Align.LEFT);
text.setTextSize(h * 0.34f);
text.setColor(white);
String speed = fmt(v.speedKmh, "%.0f", "—");
float baseline = h * 0.64f;
c.drawText(speed, x0, baseline, text);
float after = x0 + text.measureText(speed) + 14f * dp;
text.setTextSize(h * 0.065f);
text.setColor(grey);
c.drawText("km/h", after, baseline, text);
float gr = h * 0.11f, gx = right - gr, gy = h * 0.50f;
stroke.setColor(accent);
stroke.setStrokeWidth(4f * dp);
c.drawCircle(gx, gy, gr, stroke);
String gear = Gear.label(v.gear);
text.setTextAlign(Paint.Align.CENTER);
text.setTextSize(gr * 1.1f);
text.setColor(white);
c.drawText(gear.isEmpty() ? " " : gear, gx, gy + gr * 0.38f, text);
// Battery: percent and range over a bar.
float barY = h * 0.77f, barH = 12f * dp;
boolean low = v.socPercent != null && v.socPercent < MascotMood.LOW_SOC;
text.setTextSize(h * 0.055f);
text.setTextAlign(Paint.Align.LEFT);
text.setColor(low ? warn : white);
c.drawText(fmt(v.socPercent, "%.0f %%", "— %"), xLow, barY - 10f * dp, text);
text.setTextAlign(Paint.Align.RIGHT);
text.setColor(white);
c.drawText(fmt(v.rangeKm, "%.0f km", "— km"), right, barY - 10f * dp, text);
fill.setColor(track);
c.drawRoundRect(oval(xLow, barY, right, barY + barH), barH / 2f, barH / 2f, fill);
if (v.socPercent != null) {
float soc = Math.max(0f, Math.min(100f, v.socPercent)) / 100f;
fill.setColor(low ? warn : accent);
c.drawRoundRect(oval(xLow, barY, xLow + Math.max(barH, (right - xLow) * soc), barY + barH), barH / 2f, barH / 2f, fill);
}
// Power, usage, trip.
float col = (right - xLow) / 3f, labelY = h * 0.885f, valueY = h * 0.965f;
boolean regenerating = v.powerKw != null && v.powerKw < 0f;
cell(c, xLow, labelY, valueY, "POWER", fmt(v.powerKw, "%.1f kW", "— kW"), regenerating ? regen : white);
cell(c, xLow + col, labelY, valueY, "USAGE · 5 MIN", fmt(v.usage5Min, "%.1f kWh/100 km", "—"), white);
cell(c, xLow + 2f * col, labelY, valueY, "TRIP", fmt(v.tripKm, "%.1f km", "— km"), white);
}
private void cell(Canvas c, float x, float labelY, float valueY, String label, String value, int colour) {
text.setTextAlign(Paint.Align.LEFT);
text.setTextSize(13f * dp);
text.setLetterSpacing(0.12f);
text.setColor(grey);
c.drawText(label, x, labelY, text);
text.setLetterSpacing(0f);
text.setTextSize(getHeight() * 0.055f);
text.setColor(colour);
c.drawText(value, x, valueY, text);
}
}
@@ -6,7 +6,7 @@ package com.evsuite.abrp;
*
* - {@link #usesAccent}: drawn in the chosen accent colour, so Settings offers the accent
* colour for it. Styles with only their own fixed palette (the starship console, the cartoon,
* the blueprint, the flight display, the nixie tubes) ignore it and Settings greys it out.
* the flight display, the nixie tubes) ignore it and Settings greys it out.
* - {@link #hasOriginal}: has an original palette of its own as well (Retro's turquoise
* display, the terminal's green phosphor); Settings then offers "Original colours" above the
* accent colours, remembered per style under {@link #originalKey()}.
@@ -21,14 +21,12 @@ enum OverviewStyle {
FUTURISTIC(true, false),
SCIFI(false, false),
CARTOON(false, false),
BLUEPRINT(false, false),
MG(true, false),
MINIMAL(true, false),
TERMINAL(true, true),
AVIATION(false, false),
NIXIE(false, false),
CHRONOGRAPH(true, false),
MASCOT(true, false);
CHRONOGRAPH(true, false);
static final String PREF_KEY = "overview_style";
@@ -40,6 +38,15 @@ enum OverviewStyle {
this.hasOriginal = hasOriginal;
}
/**
* Whether the style is shown in its own palette: always for one with no accent colour, and
* for Retro and Terminal when "Original colours" is chosen. The app then matches it
* (StylePalette).
*/
boolean showsOwnPalette(boolean originalChosen) {
return !usesAccent || (hasOriginal && originalChosen);
}
/** Where the choice between the original palette and the accent colour is kept. */
String originalKey() {
return "original_colours_" + name();
@@ -48,7 +55,8 @@ enum OverviewStyle {
/**
* The stored style. Older values: "DASHBOARD" (Gauges before 2.22.3), and "RETRO_ACCENT" /
* "TERMINAL_ACCENT" (separate entries before 2.27.2, now the style in the accent colour —
* see {@link #wasAccentEntry}). Anything unknown is Tiles.
* see {@link #wasAccentEntry}). Anything unknown is Tiles, including "BLUEPRINT" and
* "MASCOT", the styles removed in 2.35.0.
*/
static OverviewStyle fromPref(String stored) {
if ("DASHBOARD".equals(stored)) return GAUGES;
@@ -0,0 +1,31 @@
package com.evsuite.abrp;
/**
* The app's colours while an Overview style shows its own palette: the theme overlay
* (res/values/style_palettes.xml) that MainActivity lays over the accent colour, so the
* navigation and the rest of the app match the style — Retro's turquoise, Terminal's green
* phosphor, and so on. Styles drawn in the accent colour have none.
*/
final class StylePalette {
private StylePalette() {}
/** The overlay for a style's own palette, or 0 for a style that has none. */
static int overlay(OverviewStyle style) {
switch (style) {
case RETRO: return R.style.ThemeOverlay_EV_Style_Retro;
case TERMINAL: return R.style.ThemeOverlay_EV_Style_Terminal;
case SCIFI: return R.style.ThemeOverlay_EV_Style_SciFi;
case CARTOON: return R.style.ThemeOverlay_EV_Style_Cartoon;
case AVIATION: return R.style.ThemeOverlay_EV_Style_Aviation;
case NIXIE: return R.style.ThemeOverlay_EV_Style_Nixie;
default: return 0;
}
}
/** The overlay the app wears: the style's own when it shows its palette, else the accent's. */
static int appOverlay(OverviewStyle style, boolean originalChosen, AccentColor accent) {
int own = overlay(style);
return own != 0 && style.showsOwnPalette(originalChosen) ? own : accent.overlay;
}
}
@@ -2,7 +2,7 @@
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" android:id="@+id/app_root" android:layout_width="match_parent" android:layout_height="match_parent" android:fitsSystemWindows="true" android:background="@color/ev_background">
<com.evsuite.abrp.PremiumBackdropView android:layout_width="match_parent" android:layout_height="match_parent" />
<LinearLayout android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="horizontal">
<ScrollView android:id="@+id/nav_bar" android:layout_width="192dp" android:layout_height="match_parent" android:layout_margin="16dp" android:fillViewport="true" android:background="@drawable/bg_sidebar" android:scrollbars="none">
<ScrollView android:id="@+id/nav_bar" android:saveEnabled="false" android:layout_width="192dp" android:layout_height="match_parent" android:layout_margin="16dp" android:fillViewport="true" android:background="@drawable/bg_sidebar" android:scrollbars="none">
<LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="vertical" android:padding="12dp">
<!-- The brand, and beside it the button that hides this panel on the Overview. -->
<LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="horizontal" android:gravity="center_vertical" android:layout_marginBottom="24dp">
@@ -33,13 +33,7 @@
<com.google.android.material.textfield.MaterialAutoCompleteTextView android:id="@+id/tire_unit_choices" android:layout_width="match_parent" android:layout_height="wrap_content" android:minHeight="72dp" android:inputType="none" android:textSize="20sp" app:simpleItemLayout="@layout/dropdown_item" />
</com.google.android.material.textfield.TextInputLayout>
</LinearLayout>
<!-- Only for the Mascot style: who sits in it. -->
<LinearLayout android:id="@+id/mascot_field" android:layout_width="0dp" android:layout_weight="1" android:layout_height="wrap_content" android:orientation="vertical" android:visibility="invisible">
<TextView android:layout_width="match_parent" android:layout_height="wrap_content" android:text="@string/label_mascot_character" android:textAppearance="@style/Text.EV.Body" android:layout_marginTop="8dp" />
<com.google.android.material.textfield.TextInputLayout style="@style/Widget.EV.Dropdown" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginTop="4dp">
<com.google.android.material.textfield.MaterialAutoCompleteTextView android:id="@+id/mascot_choices" android:layout_width="match_parent" android:layout_height="wrap_content" android:minHeight="72dp" android:inputType="none" android:maxLines="1" android:ellipsize="end" android:textSize="20sp" app:simpleItemLayout="@layout/dropdown_item" />
</com.google.android.material.textfield.TextInputLayout>
</LinearLayout>
<Space android:layout_width="0dp" android:layout_weight="1" android:layout_height="1dp" />
</LinearLayout>
</LinearLayout>
</LinearLayout>
+1 -1
View File
@@ -9,7 +9,7 @@
<View android:id="@+id/edge_handle" android:layout_width="5dp" android:layout_height="96dp" android:layout_gravity="start|center_vertical" android:layout_marginStart="4dp" android:background="@drawable/bg_edge_handle" android:alpha="0.35" android:visibility="gone"/>
<ImageButton android:id="@+id/edge_tab" android:layout_width="48dp" android:layout_height="96dp" android:layout_gravity="start|center_vertical" android:background="@drawable/bg_edge_tab" android:src="@drawable/ic_chevron_right" android:paddingEnd="16dp" android:paddingStart="4dp" android:scaleType="center" android:visibility="gone" android:contentDescription="@string/overview_fullscreen_exit"/>
</FrameLayout>
<LinearLayout android:id="@+id/nav_bar" android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="horizontal" android:padding="8dp" android:background="@drawable/bg_sidebar">
<LinearLayout android:id="@+id/nav_bar" android:saveEnabled="false" android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="horizontal" android:padding="8dp" android:background="@drawable/bg_sidebar">
<com.google.android.material.button.MaterialButton android:id="@+id/tabVehicle" style="@style/Widget.EV.NavTab.Compact" android:layout_width="0dp" android:layout_height="76dp" android:layout_weight="1" android:text="@string/tab_vehicle" app:icon="@drawable/ic_tab_vehicle"/>
<com.google.android.material.button.MaterialButton android:id="@+id/tabConsumption" style="@style/Widget.EV.NavTab.Compact" android:layout_width="0dp" android:layout_height="76dp" android:layout_weight="1" android:text="@string/tab_consumption" app:icon="@drawable/ic_tab_vehicle"/>
<com.google.android.material.button.MaterialButton android:id="@+id/tabCharging" style="@style/Widget.EV.NavTab.Compact" android:layout_width="0dp" android:layout_height="76dp" android:layout_weight="1" android:text="@string/tab_charging" app:icon="@drawable/ic_tab_charging"/>
@@ -21,13 +21,6 @@
<com.google.android.material.textfield.TextInputLayout style="@style/Widget.EV.Dropdown" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginTop="12dp">
<com.google.android.material.textfield.MaterialAutoCompleteTextView android:id="@+id/tire_unit_choices" android:layout_width="match_parent" android:layout_height="wrap_content" android:minHeight="72dp" android:inputType="none" android:textSize="20sp" app:simpleItemLayout="@layout/dropdown_item" />
</com.google.android.material.textfield.TextInputLayout>
<!-- Only for the Mascot style: who sits in it. -->
<LinearLayout android:id="@+id/mascot_field" android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="vertical" android:visibility="gone">
<TextView android:layout_width="match_parent" android:layout_height="wrap_content" android:text="@string/label_mascot_character" android:textAppearance="@style/Text.EV.Body" android:layout_marginTop="20dp" />
<com.google.android.material.textfield.TextInputLayout style="@style/Widget.EV.Dropdown" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginTop="12dp">
<com.google.android.material.textfield.MaterialAutoCompleteTextView android:id="@+id/mascot_choices" android:layout_width="match_parent" android:layout_height="wrap_content" android:minHeight="72dp" android:inputType="none" android:maxLines="1" android:ellipsize="end" android:textSize="20sp" app:simpleItemLayout="@layout/dropdown_item" />
</com.google.android.material.textfield.TextInputLayout>
</LinearLayout>
</LinearLayout>
<!-- The drive mode: the one vehicle setting the app writes, only in P (DriveModeWrite). -->
<LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="vertical" android:padding="24dp" android:background="@drawable/bg_glass" android:layout_marginTop="16dp">
-3
View File
@@ -42,9 +42,6 @@
<color name="cartoon_pink">#EE4266</color>
<color name="cartoon_cream">#FFF5D6</color>
<color name="cartoon_ink">#111111</color>
<!-- The Blueprint Overview style: white lines on a navy drawing sheet. -->
<color name="blueprint_sheet">#0A2240</color>
<color name="blueprint_line">#E3EEFF</color>
<!-- The MG Overview style: octagonal dials after the classic MG dashboards. -->
<color name="mg_face">#0D0D0F</color>
<color name="mg_cream">#F2E8D5</color>
+1 -109
View File
@@ -128,9 +128,8 @@
<string name="overview_style_futuristic">Futuristic</string>
<string name="overview_style_scifi">Sci-fi</string>
<string name="overview_style_cartoon">Cartoon</string>
<string name="overview_style_blueprint">Blueprint</string>
<string name="overview_style_mg">MG</string>
<string name="accent_not_used">%1$s has its own colours. To change the accent colour, pick a style that uses it.</string>
<string name="accent_not_used">%1$s has its own colours, and the app takes them on. To change the accent colour, pick a style that uses it.</string>
<string name="overview_fullscreen_enter">Overview full screen: hide the navigation</string>
<string name="overview_fullscreen_exit">Show the navigation again</string>
<string name="overview_style_minimal">Minimal</string>
@@ -139,113 +138,6 @@
<string name="overview_style_aviation">Aviation</string>
<string name="overview_style_nixie">Nixie tubes</string>
<string name="overview_style_chronograph">Chronograph</string>
<string name="overview_style_mascot">Mascot</string>
<string name="label_mascot_character">Mascot</string>
<string name="mascot_character_classic">Classic</string>
<string name="mascot_character_chibi">Chibi</string>
<string name="mascot_character_fox">Fox</string>
<!-- The fox's lines, per mood like mascot_*. -->
<string-array name="fox_waiting">
<item>Kon? Anyone there?</item>
<item>Sniff sniff… no car yet.</item>
</string-array>
<string-array name="fox_reverse">
<item>Backing up, ears up!</item>
<item>Mind my tail!</item>
</string-array>
<string-array name="fox_worried">
<item>Battery\'s low, kon…</item>
<item>Find a charger, please!</item>
</string-array>
<string-array name="fox_sleeping">
<item>Curled up in P… zzz</item>
<item>Fox nap time.</item>
</string-array>
<string-array name="fox_cheering">
<item>Kon kon! Free energy!</item>
<item>Tail-wagging regen!</item>
</string-array>
<string-array name="fox_determined">
<item>Pounce!</item>
<item>Full speed, kon!</item>
</string-array>
<string-array name="fox_fast">
<item>Ears back, here we go!</item>
<item>Zoomies!</item>
</string-array>
<string-array name="fox_content">
<item>Kon! Nice drive.</item>
<item>Happy tail, happy fox.</item>
<item>Sniffing the road ahead.</item>
</string-array>
<!-- The chibi's lines, per mood like mascot_*. -->
<string-array name="chibi_waiting">
<item>Hellooo? Car?</item>
<item>Is the car awake yet?</item>
</string-array>
<string-array name="chibi_reverse">
<item>Beep beep, backing up!</item>
<item>Eyes on the back!</item>
</string-array>
<string-array name="chibi_worried">
<item>The battery\'s hungry!</item>
<item>Snack time for the car?</item>
</string-array>
<string-array name="chibi_sleeping">
<item>Zzz… five more minutes…</item>
<item>Napping in P…</item>
</string-array>
<string-array name="chibi_cheering">
<item>Free energy! Yay!</item>
<item>Regen power!</item>
</string-array>
<string-array name="chibi_determined">
<item>Zoom zoom!</item>
<item>Full throttle!</item>
</string-array>
<string-array name="chibi_fast">
<item>Weeee, so fast!</item>
<item>Wind in my hair!</item>
</string-array>
<string-array name="chibi_content">
<item>La la la, driving~</item>
<item>Comfy cruising!</item>
<item>Vroom vroom!</item>
</string-array>
<!-- The Mascot style's co-pilot: a line per mood (MascotMood), one picked every 30 s. -->
<string-array name="mascot_waiting">
<item>Waiting for the car…</item>
<item>Hello! Is anyone there?</item>
</string-array>
<string-array name="mascot_reverse">
<item>Careful behind us…</item>
<item>I\'ll watch the back!</item>
</string-array>
<string-array name="mascot_worried">
<item>Battery\'s getting low…</item>
<item>Maybe a charger soon?</item>
</string-array>
<string-array name="mascot_sleeping">
<item>Zz… wake me when we drive.</item>
<item>Parked. Nap time.</item>
</string-array>
<string-array name="mascot_cheering">
<item>Free energy, yay!</item>
<item>Charging on the go!</item>
</string-array>
<string-array name="mascot_determined">
<item>Full power!</item>
<item>Let\'s go!</item>
</string-array>
<string-array name="mascot_fast">
<item>Wheee, motorway!</item>
<item>I love this speed!</item>
</string-array>
<string-array name="mascot_content">
<item>Nice and easy.</item>
<item>Enjoying the drive.</item>
<item>Smooth driving!</item>
</string-array>
<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,67 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- The app's accent palette while an Overview style shows its own colours (StylePalette.java):
the navigation, buttons, switches and titles take the style's main colour instead of the
MG4 paint colour. Same items as values/accents.xml; the colours come from each style's
drawing (colors.xml and the views). -->
<resources>
<!-- Retro: the turquoise of the vacuum fluorescent display (retro_lit). -->
<style name="ThemeOverlay.EV.Style.Retro" parent="">
<item name="evAccent">#4DF5D0</item>
<item name="evOnAccent">#021A14</item>
<item name="evAccentLight">#A8FFEC</item>
<item name="evAccentDeep">#0B2E27</item>
<item name="evAccentSurface">#0E1C19</item>
<item name="evAccentStroke">#80306E62</item>
<item name="evAccentTrack">#2E6A5E</item>
</style>
<!-- Terminal: green phosphor. -->
<style name="ThemeOverlay.EV.Style.Terminal" parent="">
<item name="evAccent">#33FF66</item>
<item name="evOnAccent">#021A08</item>
<item name="evAccentLight">#9DFFB5</item>
<item name="evAccentDeep">#0A2E14</item>
<item name="evAccentSurface">#0E1C12</item>
<item name="evAccentStroke">#80296B3A</item>
<item name="evAccentTrack">#1C8A3A</item>
</style>
<!-- Sci-fi: the starship console's orange, peach for the light tone (scifi_orange, scifi_peach). -->
<style name="ThemeOverlay.EV.Style.SciFi" parent="">
<item name="evAccent">#FF9966</item>
<item name="evOnAccent">#1F0E04</item>
<item name="evAccentLight">#FFCC99</item>
<item name="evAccentDeep">#3D2214</item>
<item name="evAccentSurface">#2A1B13</item>
<item name="evAccentStroke">#80775040</item>
<item name="evAccentTrack">#6A4A36</item>
</style>
<!-- Cartoon: its sunny orange. -->
<style name="ThemeOverlay.EV.Style.Cartoon" parent="">
<item name="evAccent">#FF9F1C</item>
<item name="evOnAccent">#1F1202</item>
<item name="evAccentLight">#FFC97A</item>
<item name="evAccentDeep">#3D2606</item>
<item name="evAccentSurface">#2B1D0B</item>
<item name="evAccentStroke">#80775320</item>
<item name="evAccentTrack">#6A4A1C</item>
</style>
<!-- Aviation: the flight display's cyan for selected values. -->
<style name="ThemeOverlay.EV.Style.Aviation" parent="">
<item name="evAccent">#4FD8F0</item>
<item name="evOnAccent">#021418</item>
<item name="evAccentLight">#A8EEFA</item>
<item name="evAccentDeep">#0C2E36</item>
<item name="evAccentSurface">#0F1F24</item>
<item name="evAccentStroke">#80306A76</item>
<item name="evAccentTrack">#2C6470</item>
</style>
<!-- Nixie tubes: the neon's orange glow. -->
<style name="ThemeOverlay.EV.Style.Nixie" parent="">
<item name="evAccent">#FF8A1F</item>
<item name="evOnAccent">#1F0E02</item>
<item name="evAccentLight">#FFC08A</item>
<item name="evAccentDeep">#3D1E06</item>
<item name="evAccentSurface">#2A190B</item>
<item name="evAccentStroke">#80774A26</item>
<item name="evAccentTrack">#6A4224</item>
</style>
</resources>
@@ -1,72 +0,0 @@
package com.evsuite.abrp;
import static org.junit.Assert.assertEquals;
import org.junit.Test;
/** The Mascot style's moods — pure JVM. */
public class MascotMoodTest {
private static ClusterValues values(Integer gear, Float speed, Float soc, Float power) {
ClusterValues v = new ClusterValues();
v.gear = gear;
v.speedKmh = speed;
v.socPercent = soc;
v.powerKw = power;
return v;
}
@Test
public void nothingReadIsWaiting() {
assertEquals(MascotMood.WAITING, MascotMood.of(null));
assertEquals(MascotMood.WAITING, MascotMood.of(new ClusterValues()));
}
@Test
public void moodsByPriority() {
assertEquals(MascotMood.REVERSE, MascotMood.of(values(2, 3f, 10f, 5f)));
assertEquals(MascotMood.WORRIED, MascotMood.of(values(4, 50f, 15f, 10f)));
assertEquals(MascotMood.WORRIED, MascotMood.of(values(1, 0f, 15f, 0f)));
assertEquals(MascotMood.SLEEPING, MascotMood.of(values(1, 0f, 80f, 0f)));
assertEquals(MascotMood.CHEERING, MascotMood.of(values(4, 120f, 80f, -20f)));
assertEquals(MascotMood.DETERMINED, MascotMood.of(values(4, 60f, 80f, 90f)));
assertEquals(MascotMood.FAST, MascotMood.of(values(4, 110f, 80f, 25f)));
assertEquals(MascotMood.CONTENT, MascotMood.of(values(4, 50f, 80f, 10f)));
assertEquals(MascotMood.CONTENT, MascotMood.of(values(3, 0f, 80f, 0f)));
}
@Test
public void unreadValuesAreNoReason() {
// Speed unread, battery unread: in D she is content, not worried or fast.
assertEquals(MascotMood.CONTENT, MascotMood.of(values(4, null, null, 10f)));
assertEquals(MascotMood.CONTENT, MascotMood.of(values(null, 50f, null, null)));
}
@Test
public void holderWaitsOutShortChanges() {
MascotMood.Holder holder = new MascotMood.Holder();
assertEquals(MascotMood.CONTENT, holder.update(MascotMood.CONTENT, 0));
assertEquals(MascotMood.CONTENT, holder.update(MascotMood.CHEERING, 100));
assertEquals(MascotMood.CONTENT, holder.update(MascotMood.CONTENT, 300));
assertEquals(MascotMood.CONTENT, holder.update(MascotMood.CHEERING, 400));
assertEquals(MascotMood.CONTENT, holder.update(MascotMood.CHEERING, 1800));
assertEquals(MascotMood.CHEERING, holder.update(MascotMood.CHEERING, 1900));
}
@Test
public void reverseAndWaitingShowAtOnce() {
MascotMood.Holder holder = new MascotMood.Holder();
holder.update(MascotMood.SLEEPING, 0);
assertEquals(MascotMood.REVERSE, holder.update(MascotMood.REVERSE, 10));
assertEquals(MascotMood.WAITING, holder.update(MascotMood.WAITING, 20));
}
@Test
public void characterFromPref() {
assertEquals(MascotCharacter.CHIBI, MascotCharacter.fromPref("CHIBI"));
assertEquals(MascotCharacter.FOX, MascotCharacter.fromPref("FOX"));
assertEquals(MascotCharacter.CLASSIC, MascotCharacter.fromPref("CLASSIC"));
assertEquals(MascotCharacter.CLASSIC, MascotCharacter.fromPref(null));
assertEquals(MascotCharacter.CLASSIC, MascotCharacter.fromPref("NOPE"));
}
}
@@ -36,7 +36,31 @@ public class OverviewStyleTest {
assertTrue(OverviewStyle.TERMINAL.hasOriginal);
assertFalse(OverviewStyle.CARTOON.usesAccent);
assertFalse(OverviewStyle.NIXIE.usesAccent);
assertTrue(OverviewStyle.MASCOT.usesAccent);
assertTrue(OverviewStyle.CHRONOGRAPH.usesAccent);
}
@Test
public void removedStylesFallBackToTiles() {
assertEquals(OverviewStyle.TILES, OverviewStyle.fromPref("MASCOT"));
assertEquals(OverviewStyle.TILES, OverviewStyle.fromPref("BLUEPRINT"));
}
@Test
public void ownPaletteForTheApp() {
// Fixed palettes always; Retro and Terminal only with "Original colours".
assertTrue(OverviewStyle.CARTOON.showsOwnPalette(false));
assertTrue(OverviewStyle.RETRO.showsOwnPalette(true));
assertFalse(OverviewStyle.RETRO.showsOwnPalette(false));
assertFalse(OverviewStyle.CHRONOGRAPH.showsOwnPalette(true));
// Every style shown in its own palette has the app's matching colours.
for (OverviewStyle style : OverviewStyle.values()) {
if (style.showsOwnPalette(true)) assertTrue(style.name(), StylePalette.overlay(style) != 0);
else assertEquals(style.name(), 0, StylePalette.overlay(style));
}
assertEquals(AccentColor.HOLBORN_BLUE.overlay,
StylePalette.appOverlay(OverviewStyle.RETRO, false, AccentColor.HOLBORN_BLUE));
assertEquals(R.style.ThemeOverlay_EV_Style_Retro,
StylePalette.appOverlay(OverviewStyle.RETRO, true, AccentColor.HOLBORN_BLUE));
assertEquals("original_colours_RETRO", OverviewStyle.RETRO.originalKey());
}
}