App 2.24.0: Overview styles Sci-fi, Cartoon and Blueprint

Sci-fi: a starship console with elbow frames, pill labels and block
graphs, red alert below 20 %. Cartoon: comic panels, a battery with a
face, a springy speedometer and sound words for power. Blueprint: a
top-view drawing with the battery hatched to the charge, tyre pressures
on leader lines, power as a force arrow and a title block. Styles are
now mapped to panes in a table. Emulator only.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LhzAVaUyqcw5PkGjz4mWW6
This commit is contained in:
2026-10-24 20:08:24 +02:00
co-authored by Claude Opus 5.5
parent 2879c43e26
commit 5fb367d7cd
14 changed files with 926 additions and 20 deletions
@@ -0,0 +1,283 @@
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);
}
}
@@ -0,0 +1,353 @@
package com.evsuite.abrp;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.RectF;
import android.graphics.Typeface;
import android.os.SystemClock;
import android.util.AttributeSet;
import android.view.View;
import java.util.Locale;
/**
* The Cartoon Overview style: comic panels in flat colours with thick ink outlines and hard
* shadows, on a night-blue page (no white page at night).
*
* Left, a battery with a face: happy above 50 %, unsure from 20 %, worried and sweating below.
* Centre, a chunky speedometer whose needle springs and wobbles into place, the speed in a
* speech bubble and the gear on a sticker. Right, the "zap-o-meter" for power with a sound word
* in a burst — PURR, VROOM, ZOOM! or RECHARGE! while regenerating. Below, stats on pills.
* Unread values show "—", a face with no expression and no needle.
*
* The needle animates (about 30 frames a second) only until it settles, and only on screen.
*/
public final class CartoonView extends View {
private static final float START_DEG = 150f, SWEEP_DEG = 240f, MAX_KMH = 180f;
private static final long FRAME_MS = 33;
private final Paint fill = new Paint(Paint.ANTI_ALIAS_FLAG);
private final Paint ink = new Paint(Paint.ANTI_ALIAS_FLAG);
private final Paint text = new Paint(Paint.ANTI_ALIAS_FLAG);
private final Paint textInk = new Paint(Paint.ANTI_ALIAS_FLAG);
private final Path path = new Path();
private final RectF rect = new RectF();
private final int night, yellow, cyan, pink, cream, inkColour, green, orange, red;
private ClusterValues v = new ClusterValues();
/** The needle in km/h and its speed, for the spring. */
private float needle = Float.NaN, needleVelocity;
private long lastFrame;
public CartoonView(Context context, AttributeSet attrs) {
super(context, attrs);
night = context.getColor(R.color.cartoon_night);
yellow = context.getColor(R.color.cartoon_yellow);
cyan = context.getColor(R.color.cartoon_cyan);
pink = context.getColor(R.color.cartoon_pink);
cream = context.getColor(R.color.cartoon_cream);
inkColour = context.getColor(R.color.cartoon_ink);
green = 0xFF5BD35B;
orange = 0xFFFF9F1C;
red = 0xFFE63946;
ink.setStyle(Paint.Style.STROKE);
ink.setStrokeJoin(Paint.Join.ROUND);
ink.setStrokeCap(Paint.Cap.ROUND);
ink.setColor(inkColour);
Typeface black = Typeface.create("sans-serif-black", Typeface.NORMAL);
text.setTypeface(black);
text.setFontFeatureSettings("tnum");
textInk.setTypeface(black);
textInk.setFontFeatureSettings("tnum");
textInk.setStyle(Paint.Style.STROKE);
textInk.setStrokeJoin(Paint.Join.ROUND);
textInk.setColor(inkColour);
}
void setValues(ClusterValues values) {
v = values == null ? new ClusterValues() : values;
if (v.speedKmh != null && Float.isNaN(needle)) needle = v.speedKmh;
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;
rect.set(0, 0, w, h);
fill.setColor(night);
c.drawRoundRect(rect, 24f * d, 24f * d, fill);
boolean moving = stepNeedle();
drawBattery(c, w * 0.02f, h * 0.05f, w * 0.27f, h * 0.76f, d);
drawSpeedometer(c, w * 0.50f, h * 0.43f, h * 0.38f, d);
drawZap(c, w * 0.73f, h * 0.05f, w * 0.98f, h * 0.76f, d);
drawPills(c, w, h, d);
if (moving && isShown()) postInvalidateDelayed(FRAME_MS);
else lastFrame = 0;
}
/** A spring towards the speed: overshoots a little and wobbles to rest. True while moving. */
private boolean stepNeedle() {
if (v.speedKmh == null || Float.isNaN(needle)) return false;
long now = SystemClock.uptimeMillis();
float dt = lastFrame == 0 ? FRAME_MS / 1000f : Math.min(100, now - lastFrame) / 1000f;
lastFrame = now;
float target = Math.max(0f, Math.min(MAX_KMH, v.speedKmh));
needleVelocity += (target - needle) * 60f * dt;
needleVelocity *= (float) Math.pow(0.02, dt);
needle += needleVelocity * dt;
if (Math.abs(target - needle) < 0.2f && Math.abs(needleVelocity) < 0.5f) {
needle = target;
needleVelocity = 0f;
return false;
}
return true;
}
// ---------- Panels ----------
/** A comic panel: a hard shadow, the colour, a thick outline. */
private void panel(Canvas c, float l, float t, float r, float b, int colour, float d) {
float off = 6f * d, radius = 18f * d;
fill.setColor(inkColour);
rect.set(l + off, t + off, r + off, b + off);
c.drawRoundRect(rect, radius, radius, fill);
fill.setColor(colour);
rect.set(l, t, r, b);
c.drawRoundRect(rect, radius, radius, fill);
ink.setStrokeWidth(4f * d);
c.drawRoundRect(rect, radius, radius, ink);
}
private void drawBattery(Canvas c, float l, float t, float r, float b, float d) {
panel(c, l, t, r, b, yellow, d);
Float soc = v.socPercent;
// The body: a battery standing up, filled to the charge.
float bw = (r - l) * 0.46f, bl = (l + r) / 2f - bw / 2f, bt = t + (b - t) * 0.14f, bb = t + (b - t) * 0.66f;
fill.setColor(inkColour);
rect.set(bl + bw * 0.32f, bt - 12f * d, bl + bw * 0.68f, bt + 2f * d);
c.drawRoundRect(rect, 4f * d, 4f * d, fill);
fill.setColor(cream);
rect.set(bl, bt, bl + bw, bb);
c.drawRoundRect(rect, 16f * d, 16f * d, fill);
if (soc != null) {
float level = bb - (bb - bt) * Math.max(0f, Math.min(100f, soc)) / 100f;
fill.setColor(soc < 20f ? red : soc < 50f ? orange : green);
c.save();
c.clipRect(bl, level, bl + bw, bb);
c.drawRoundRect(rect, 16f * d, 16f * d, fill);
c.restore();
}
ink.setStrokeWidth(4f * d);
c.drawRoundRect(rect, 16f * d, 16f * d, ink);
// The face.
float fx = bl + bw / 2f, fy = bt + (bb - bt) * 0.42f, eye = bw * 0.09f;
fill.setColor(inkColour);
c.drawOval(fx - bw * 0.22f - eye, fy - eye * 1.4f, fx - bw * 0.22f + eye, fy + eye * 1.4f, fill);
c.drawOval(fx + bw * 0.22f - eye, fy - eye * 1.4f, fx + bw * 0.22f + eye, fy + eye * 1.4f, fill);
fill.setColor(0xFFFFFFFF);
c.drawCircle(fx - bw * 0.22f + eye * 0.35f, fy - eye * 0.6f, eye * 0.38f, fill);
c.drawCircle(fx + bw * 0.22f + eye * 0.35f, fy - eye * 0.6f, eye * 0.38f, fill);
float my = fy + bw * 0.30f, mw = bw * 0.26f;
ink.setStrokeWidth(4f * d);
path.reset();
path.moveTo(fx - mw, my);
if (soc == null) path.lineTo(fx + mw, my);
else if (soc >= 50f) path.quadTo(fx, my + mw * 0.9f, fx + mw, my); // smile
else if (soc >= 20f) path.quadTo(fx, my + mw * 0.15f, fx + mw, my); // unsure
else path.quadTo(fx, my - mw * 0.8f, fx + mw, my); // worried
c.drawPath(path, ink);
if (soc != null && soc < 20f) {
// A drop of sweat.
float sx = bl + bw + 10f * d, sy = bt + 14f * d;
path.reset();
path.moveTo(sx, sy);
path.quadTo(sx + 12f * d, sy + 18f * d, sx, sy + 24f * d);
path.quadTo(sx - 12f * d, sy + 18f * d, sx, sy);
fill.setColor(0xFF7FD3FF);
c.drawPath(path, fill);
ink.setStrokeWidth(2.5f * d);
c.drawPath(path, ink);
}
outlined(c, soc == null ? "—" : String.format(Locale.US, "%.0f%%", soc), (l + r) / 2f,
t + (b - t) * 0.84f, (b - t) * 0.15f, 0xFFFFFFFF, d);
outlined(c, v.rangeKm == null ? "— km to go" : String.format(Locale.US, "%.0f km to go!", v.rangeKm),
(l + r) / 2f, t + (b - t) * 0.95f, (b - t) * 0.065f, 0xFFFFFFFF, d);
}
private void drawSpeedometer(Canvas c, float cx, float cy, float r, float d) {
// Shadow, face, coloured zones, outline.
float off = 6f * d;
fill.setColor(inkColour);
c.drawCircle(cx + off, cy + off, r, fill);
fill.setColor(cream);
c.drawCircle(cx, cy, r, fill);
float zr = r * 0.80f;
rect.set(cx - zr, cy - zr, cx + zr, cy + zr);
ink.setStrokeCap(Paint.Cap.BUTT);
int[] zones = { green, orange, red };
for (int i = 0; i < 3; i++) {
ink.setColor(zones[i]);
ink.setStrokeWidth(r * 0.14f);
c.drawArc(rect, START_DEG + i * SWEEP_DEG / 3f, SWEEP_DEG / 3f, false, ink);
}
ink.setColor(inkColour);
ink.setStrokeCap(Paint.Cap.ROUND);
ink.setStrokeWidth(5f * d);
c.drawCircle(cx, cy, r, ink);
// Chunky ticks and numbers every 30 km/h.
text.setTextAlign(Paint.Align.CENTER);
text.setColor(inkColour);
text.setTextSize(r * 0.12f);
for (int kmh = 0; kmh <= MAX_KMH; kmh += 30) {
double a = Math.toRadians(START_DEG + SWEEP_DEG * kmh / MAX_KMH);
float cos = (float) Math.cos(a), sin = (float) Math.sin(a);
ink.setStrokeWidth(5f * d);
c.drawLine(cx + r * 0.88f * cos, cy + r * 0.88f * sin, cx + r * 0.72f * cos, cy + r * 0.72f * sin, ink);
c.drawText(String.valueOf(kmh), cx + r * 0.56f * cos, cy + r * 0.56f * sin + text.getTextSize() * 0.36f, text);
}
// The needle, springy.
if (v.speedKmh != null && !Float.isNaN(needle)) {
double a = Math.toRadians(START_DEG + SWEEP_DEG * Math.max(-4f, Math.min(MAX_KMH + 4f, needle)) / MAX_KMH);
float nx = cx + r * 0.78f * (float) Math.cos(a), ny = cy + r * 0.78f * (float) Math.sin(a);
ink.setStrokeWidth(12f * d);
c.drawLine(cx, cy, nx, ny, ink);
fill.setColor(red);
ink.setStrokeWidth(7f * d);
ink.setColor(red);
c.drawLine(cx, cy, nx, ny, ink);
ink.setColor(inkColour);
}
fill.setColor(pink);
c.drawCircle(cx, cy, r * 0.09f, fill);
ink.setStrokeWidth(4f * d);
c.drawCircle(cx, cy, r * 0.09f, ink);
// The speed in a speech bubble below the hub.
String speed = v.speedKmh == null ? "—" : String.format(Locale.US, "%.0f", v.speedKmh);
text.setTextSize(r * 0.30f);
float tw = Math.max(text.measureText(speed), r * 0.4f);
float bt = cy + r * 0.22f, bb = cy + r * 0.62f;
rect.set(cx - tw / 2f - r * 0.12f, bt, cx + tw / 2f + r * 0.12f, bb);
fill.setColor(0xFFFFFFFF);
c.drawRoundRect(rect, 14f * d, 14f * d, fill);
ink.setStrokeWidth(4f * d);
c.drawRoundRect(rect, 14f * d, 14f * d, ink);
text.setColor(inkColour);
c.drawText(speed, cx, bb - (bb - bt) * 0.22f, text);
text.setTextSize(r * 0.09f);
c.drawText("km/h", cx, cy + r * 0.76f, text);
// The gear on a round sticker at the top right of the dial.
String gear = Gear.label(v.gear);
if (!gear.isEmpty()) {
float gx = cx + r * 0.80f, gy = cy - r * 0.78f, gr = r * 0.20f;
fill.setColor(inkColour);
c.drawCircle(gx + 4f * d, gy + 4f * d, gr, fill);
fill.setColor(pink);
c.drawCircle(gx, gy, gr, fill);
c.drawCircle(gx, gy, gr, ink);
outlined(c, gear, gx, gy + gr * 0.40f, gr * 1.1f, 0xFFFFFFFF, d);
}
}
private void drawZap(Canvas c, float l, float t, float r, float b, float d) {
panel(c, l, t, r, b, cyan, d);
Float kw = v.powerKw;
outlined(c, "ZAP-O-METER", (l + r) / 2f, t + (b - t) * 0.13f, (b - t) * 0.07f, yellow, d);
// The sound word in a burst.
String word = kw == null ? "…" : kw < -5f ? "RECHARGE!" : kw < 0.5f ? "PSST" : kw < 10f ? "PURR"
: kw < 40f ? "VROOM" : "ZOOM!";
float bx = (l + r) / 2f, by = t + (b - t) * 0.40f, br = Math.min(r - l, b - t) * 0.30f;
path.reset();
for (int i = 0; i < 24; i++) {
double a = Math.PI * 2 * i / 24;
float rr = i % 2 == 0 ? br : br * 0.74f;
float x = bx + rr * 1.25f * (float) Math.cos(a), y = by + rr * 0.85f * (float) Math.sin(a);
if (i == 0) path.moveTo(x, y); else path.lineTo(x, y);
}
path.close();
fill.setColor(kw != null && kw < -5f ? green : pink);
c.drawPath(path, fill);
ink.setStrokeWidth(4f * d);
c.drawPath(path, ink);
text.setTextSize(br * 0.42f);
float size = br * 0.42f * Math.min(1f, (br * 2.1f) / Math.max(1f, text.measureText(word)));
c.save();
c.rotate(-8f, bx, by);
outlined(c, word, bx, by + size * 0.36f, size, yellow, d);
c.restore();
// The tube: regeneration fills left of the middle mark in green, power right of it.
float tl = l + (r - l) * 0.10f, tr = r - (r - l) * 0.10f, tt = t + (b - t) * 0.70f, tb = t + (b - t) * 0.80f;
float zero = tl + (tr - tl) * 0.25f;
rect.set(tl, tt, tr, tb);
fill.setColor(cream);
c.drawRoundRect(rect, (tb - tt) / 2f, (tb - tt) / 2f, fill);
if (kw != null && Math.abs(kw) >= 0.1f) {
float x = kw >= 0f ? zero + (tr - zero) * Math.min(150f, kw) / 150f : zero - (zero - tl) * Math.min(50f, -kw) / 50f;
c.save();
c.clipRect(Math.min(zero, x), tt, Math.max(zero, x), tb);
fill.setColor(kw >= 0f ? orange : green);
c.drawRoundRect(rect, (tb - tt) / 2f, (tb - tt) / 2f, fill);
c.restore();
}
ink.setStrokeWidth(4f * d);
c.drawRoundRect(rect, (tb - tt) / 2f, (tb - tt) / 2f, ink);
ink.setStrokeWidth(3f * d);
c.drawLine(zero, tt - 6f * d, zero, tb + 6f * d, ink);
outlined(c, kw == null ? "— kW" : String.format(Locale.US, "%.1f kW", kw), (l + r) / 2f,
t + (b - t) * 0.94f, (b - t) * 0.10f, 0xFFFFFFFF, d);
}
private void drawPills(Canvas c, float w, float h, float d) {
String[] items = {
v.usage5Min == null ? "— kWh/100" : String.format(Locale.US, "%.1f kWh/100", v.usage5Min),
v.tripKm == null ? "Trip —" : String.format(Locale.US, "Trip %.1f km", v.tripKm),
v.climateCelsius == null ? "— °C" : String.format(Locale.US, "%.0f °C", v.climateCelsius),
v.odometerKm == null ? "— km" : String.format(Locale.US, "%,.0f km", v.odometerKm),
v.clock.isEmpty() ? "—" : v.clock,
};
int[] colours = { pink, yellow, cyan, yellow, pink };
float t = h * 0.84f, b = h * 0.97f, gap = 16f * d, x = w * 0.02f;
float pw = (w * 0.96f - gap * (items.length - 1)) / items.length;
for (int i = 0; i < items.length; i++) {
float off = 4f * d;
fill.setColor(inkColour);
rect.set(x + off, t + off, x + pw + off, b + off);
c.drawRoundRect(rect, (b - t) / 2f, (b - t) / 2f, fill);
fill.setColor(colours[i]);
rect.set(x, t, x + pw, b);
c.drawRoundRect(rect, (b - t) / 2f, (b - t) / 2f, fill);
ink.setStrokeWidth(3.5f * d);
c.drawRoundRect(rect, (b - t) / 2f, (b - t) / 2f, ink);
text.setTextAlign(Paint.Align.CENTER);
text.setTextSize((b - t) * 0.46f);
text.setColor(inkColour);
c.drawText(items[i], x + pw / 2f, b - (b - t) * 0.30f, text);
x += pw + gap;
}
}
/** Comic lettering: the text in ink, thick, then the colour on top. */
private void outlined(Canvas c, String s, float x, float baseline, float size, int colour, float d) {
textInk.setTextSize(size);
textInk.setTextAlign(Paint.Align.CENTER);
textInk.setStrokeWidth(Math.max(3f * d, size * 0.12f));
c.drawText(s, x, baseline, textInk);
text.setTextSize(size);
text.setTextAlign(Paint.Align.CENTER);
text.setColor(colour);
c.drawText(s, x, baseline, text);
}
}
@@ -1,8 +1,8 @@
package com.evsuite.abrp;
/**
* One reading for the drawn Overview styles (Retro, Futuristic): what they show, already in
* display units. Every field is null when unread, and the styles show that as dashes, never 0.
* One reading for the drawn Overview styles (Retro, Futuristic, Sci-fi, Cartoon, Blueprint):
* what they show, already in display units. Every field is null when unread, and the styles show that as dashes, never 0.
*/
final class ClusterValues {
Float socPercent;
@@ -19,4 +19,6 @@ final class ClusterValues {
Float odometerKm;
/** "HH:mm", never null. */
String clock = "";
/** Tyre pressures formatted in the chosen unit, "—" when unread; never null. */
String tyreFl = "—", tyreFr = "—", tyreRl = "—", tyreRr = "—";
}
@@ -69,6 +69,14 @@ public class MainActivity extends AppCompatActivity {
private View gaugesPane;
private View cockpitPane;
private View retroPane, futuristicPane;
/** The drawn styles' views, each fed the same {@link ClusterValues} per reading. */
private RetroView retroView;
private FlowView flowView;
private SciFiView sciFiView;
private CartoonView cartoonView;
private BlueprintView blueprintView;
/** The pane of each Overview style, by its index in {@link #OVERVIEW_STYLES}. */
private View[] stylePanes;
private View consumptionPane;
private View chargingPane;
/** Display, battery, charging price and Wi-Fi. */
@@ -249,7 +257,16 @@ public class MainActivity extends AppCompatActivity {
cockpitPane = inflatePane(R.layout.pane_cockpit);
retroPane = inflatePane(R.layout.pane_retro);
futuristicPane = inflatePane(R.layout.pane_futuristic);
// The Overview page holds both styles; Settings → Overview style picks the one shown.
retroView = retroPane.findViewById(R.id.retro_view);
flowView = futuristicPane.findViewById(R.id.flow_view);
sciFiView = new SciFiView(this, null);
cartoonView = new CartoonView(this, null);
blueprintView = new BlueprintView(this, null);
View sciFiPane = drawnPane(sciFiView), cartoonPane = drawnPane(cartoonView),
blueprintPane = drawnPane(blueprintView);
stylePanes = new View[] { vehiclePane, gaugesPane, cockpitPane, retroPane, retroPane,
futuristicPane, sciFiPane, cartoonPane, blueprintPane };
// 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(
android.view.ViewGroup.LayoutParams.MATCH_PARENT,
@@ -259,6 +276,9 @@ public class MainActivity extends AppCompatActivity {
overviewPage.addView(cockpitPane);
overviewPage.addView(retroPane);
overviewPage.addView(futuristicPane);
overviewPage.addView(sciFiPane);
overviewPage.addView(cartoonPane);
overviewPage.addView(blueprintPane);
consumptionPane = inflatePane(R.layout.pane_consumption);
chargingPane = inflatePane(R.layout.pane_charging);
settingsPane = inflatePane(R.layout.pane_settings);
@@ -1442,7 +1462,8 @@ public class MainActivity extends AppCompatActivity {
private Double dashTripKm;
/** The stored values of the Overview styles, in the order of the drop-down. */
private static final String[] OVERVIEW_STYLES = { "TILES", "GAUGES", "COCKPIT", "RETRO", "RETRO_ACCENT", "FUTURISTIC" };
private static final String[] OVERVIEW_STYLES = { "TILES", "GAUGES", "COCKPIT", "RETRO", "RETRO_ACCENT", "FUTURISTIC",
"SCIFI", "CARTOON", "BLUEPRINT" };
/** The chosen style's index in {@link #OVERVIEW_STYLES}; anything unknown is Tiles. */
private int overviewStyle() {
@@ -1454,20 +1475,18 @@ public class MainActivity extends AppCompatActivity {
private void applyOverviewStyle() {
int style = overviewStyle();
vehiclePane.setVisibility(style == 0 ? View.VISIBLE : View.GONE);
gaugesPane.setVisibility(style == 1 ? View.VISIBLE : View.GONE);
cockpitPane.setVisibility(style == 2 ? View.VISIBLE : View.GONE);
for (View pane : stylePanes) pane.setVisibility(pane == stylePanes[style] ? View.VISIBLE : View.GONE);
// Retro in either palette: one pane, the view told which.
retroPane.setVisibility(style == 3 || style == 4 ? View.VISIBLE : View.GONE);
((RetroView) retroPane.findViewById(R.id.retro_view)).setAccentPalette(style == 4);
futuristicPane.setVisibility(style == 5 ? View.VISIBLE : View.GONE);
retroView.setAccentPalette("RETRO_ACCENT".equals(OVERVIEW_STYLES[style]));
}
/** Tiles, Gauges, Cockpit, Retro (classic or accent colour) or Futuristic; all are fed on every reading. */
/** The style drop-down; every style is fed on every reading, only the chosen one is shown. */
private void bindOverviewStyle() {
String[] labels = { getString(R.string.overview_style_tiles), getString(R.string.overview_style_gauges),
getString(R.string.overview_style_cockpit), getString(R.string.overview_style_retro),
getString(R.string.overview_style_retro_accent), getString(R.string.overview_style_futuristic) };
getString(R.string.overview_style_retro_accent), getString(R.string.overview_style_futuristic),
getString(R.string.overview_style_scifi), getString(R.string.overview_style_cartoon),
getString(R.string.overview_style_blueprint) };
bindChoices(settingsPane.findViewById(R.id.overview_style_choices), labels, overviewStyle(),
position -> {
prefs.edit().putString(PREF_OVERVIEW_STYLE, OVERVIEW_STYLES[position]).apply();
@@ -1496,9 +1515,17 @@ public class MainActivity extends AppCompatActivity {
private final java.text.SimpleDateFormat clockFormat =
new java.text.SimpleDateFormat("HH:mm", java.util.Locale.US);
/** A drawn style's view in a pane with the Overview's margins. */
private View drawnPane(View view) {
android.widget.FrameLayout pane = new android.widget.FrameLayout(this);
float dp = getResources().getDisplayMetrics().density;
pane.setPadding(Math.round(24 * dp), Math.round(12 * dp), Math.round(24 * dp), Math.round(12 * dp));
pane.addView(view, new android.widget.FrameLayout.LayoutParams(
android.view.ViewGroup.LayoutParams.MATCH_PARENT, android.view.ViewGroup.LayoutParams.MATCH_PARENT));
return pane;
}
private void showDrawnStyles(EnergySnapshot s) {
RetroView retro = retroPane.findViewById(R.id.retro_view);
FlowView flow = futuristicPane.findViewById(R.id.flow_view);
ClusterValues v = new ClusterValues();
v.socPercent = s.getSocPercent();
v.rangeKm = s.getRangeKm();
@@ -1511,8 +1538,16 @@ public class MainActivity extends AppCompatActivity {
v.climateCelsius = s.getClimate().getDriverTargetCelsius();
v.odometerKm = s.getOdometerKm();
v.clock = clockFormat.format(new java.util.Date());
retro.setValues(v);
flow.setValues(v);
PressureUnit unit = PressureUnit.fromPref(prefs.getString(PressureUnit.PREF_KEY, null));
v.tyreFl = unit.format(s.getTirePressures().getFrontLeftKpa());
v.tyreFr = unit.format(s.getTirePressures().getFrontRightKpa());
v.tyreRl = unit.format(s.getTirePressures().getRearLeftKpa());
v.tyreRr = unit.format(s.getTirePressures().getRearRightKpa());
retroView.setValues(v);
flowView.setValues(v);
sciFiView.setValues(v);
cartoonView.setValues(v);
blueprintView.setValues(v);
}
// ---------- Cockpit (Overview style) ----------
@@ -0,0 +1,194 @@
package com.evsuite.abrp;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.RectF;
import android.graphics.Typeface;
import android.util.AttributeSet;
import android.view.View;
import java.util.Locale;
/**
* The Sci-fi Overview style: a starship console. Rounded "elbow" frames in orange, lilac and
* blue, pill-shaped labels, condensed capitals and blocky bar graphs on black.
*
* Left: velocity (the speed) and the power transfer as a bar of blocks, regeneration in blue to
* the left of zero. Middle: labelled rows — reserves, range, efficiency, trip, climate. Right:
* the reserves as a stack of blocks. The gears are blocks in the left frame, the odometer and
* clock run along the bottom frame. Below 20 % charge the frames turn red ("red alert").
* Unread values show "—" and light no blocks. Redrawn only when a reading arrives.
*/
public final class SciFiView extends View {
private final Paint fill = 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 orange, peach, lilac, blue, alert, off;
private ClusterValues v = new ClusterValues();
public SciFiView(Context context, AttributeSet attrs) {
super(context, attrs);
orange = context.getColor(R.color.scifi_orange);
peach = context.getColor(R.color.scifi_peach);
lilac = context.getColor(R.color.scifi_lilac);
blue = context.getColor(R.color.scifi_blue);
alert = context.getColor(R.color.scifi_alert);
off = context.getColor(R.color.scifi_off);
text.setTypeface(Typeface.create("sans-serif-condensed", Typeface.BOLD));
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;
boolean redAlert = v.socPercent != null && v.socPercent < 20f;
int frame = redAlert ? alert : orange, frame2 = redAlert ? alert : lilac;
// Frames: an elbow at the top left and one at the bottom left.
float side = w * 0.085f, bar = h * 0.055f, gap = 6f * d;
float split = h * 0.50f;
// Each elbow is only its corner; blocks fill the side between them.
float corner = bar + side * 0.9f + 12f * d;
elbow(c, 0, 0, w * 0.60f, corner, side, bar, true, frame);
elbow(c, 0, h - corner, w * 0.70f, h, side, bar, false, frame2);
// The top bar's end: the title, then a pill.
label(c, redAlert ? "RED ALERT · LOW RESERVES" : "PROPULSION STATUS", w * 0.61f + 10f * d,
bar * 0.86f, bar * 1.05f, redAlert ? alert : orange, Paint.Align.LEFT);
pill(c, w * 0.94f, 0, w, bar, frame2);
// The bottom bar's end: odometer and time.
String odo = v.odometerKm == null ? "—" : String.format(Locale.US, "%06.0f", v.odometerKm);
label(c, "ODO " + odo + " · " + (v.clock.isEmpty() ? "—" : v.clock),
w - 4f * d, h - bar * 0.14f, bar * 0.95f, peach, Paint.Align.RIGHT);
// The gears as blocks down the lower elbow.
String[] gears = { "P", "R", "N", "D" };
float gy = split + gap / 2f, gh = (h - corner - gap - gy - 3 * gap) / 4f;
for (int i = 0; i < 4; i++) {
boolean on = v.gear != null && v.gear == i + 1;
float top = gy + i * (gh + gap);
fill.setColor(on ? peach : off);
c.drawRect(0, top, side, top + gh, fill);
label(c, gears[i], side - 8f * d, top + gh - 6f * d, gh * 0.7f, on ? 0xFF000000 : lilac, Paint.Align.RIGHT);
}
// Upper elbow: three numbered blocks, the console's decoration.
float by = corner + gap, bh = (split - gap / 2f - by - 2 * gap) / 3f;
int[] colours = { lilac, blue, peach };
String[] codes = { "01-4471", "02-0815", "03-2026" };
for (int i = 0; i < 3; i++) {
float top = by + i * (bh + gap);
fill.setColor(redAlert ? alert : colours[i]);
c.drawRect(0, top, side, top + bh, fill);
label(c, codes[i], side - 8f * d, top + bh - 6f * d, Math.min(bh * 0.4f, 16f * d), 0xFF000000, Paint.Align.RIGHT);
}
float x0 = side + w * 0.035f;
// Velocity, large.
label(c, "VELOCITY · KM/H", x0, h * 0.17f, h * 0.045f, lilac, Paint.Align.LEFT);
label(c, v.speedKmh == null ? "—" : String.format(Locale.US, "%.0f", v.speedKmh), x0 - 4f * d,
h * 0.45f, h * 0.30f, orange, Paint.Align.LEFT);
// Power transfer: blocks from −50 to 150 kW in 10 kW steps.
label(c, "POWER TRANSFER · KW", x0, h * 0.62f, h * 0.045f, lilac, Paint.Align.LEFT);
String kw = v.powerKw == null ? "—" : String.format(Locale.US, "%.1f", v.powerKw);
label(c, kw, w * 0.42f, h * 0.62f, h * 0.06f, v.powerKw != null && v.powerKw < 0f ? blue : peach, Paint.Align.RIGHT);
int blocks = 20, zero = 5;
float bx0 = x0, bx1 = w * 0.42f, pitch = (bx1 - bx0) / blocks;
int litTo = v.powerKw == null ? zero : zero + Math.round(Math.max(-50f, Math.min(150f, v.powerKw)) / 10f);
for (int i = 0; i < blocks; i++) {
boolean on = v.powerKw != null && (litTo > zero ? i >= zero && i < litTo : i >= litTo && i < zero);
fill.setColor(on ? (i < zero ? blue : i >= 15 ? alert : orange) : off);
float x = bx0 + i * pitch;
rect.set(x, h * 0.65f, x + pitch - 4f * d, h * 0.74f);
c.drawRoundRect(rect, i == 0 || i == blocks - 1 ? 10f * d : 0f, 10f * d, fill);
}
label(c, "−50", bx0, h * 0.79f, h * 0.035f, lilac, Paint.Align.LEFT);
label(c, "0", bx0 + zero * pitch, h * 0.79f, h * 0.035f, lilac, Paint.Align.LEFT);
label(c, "150", bx1, h * 0.79f, h * 0.035f, lilac, Paint.Align.RIGHT);
// Middle: labelled rows.
String[][] rows = {
{ "RESERVES", v.socPercent == null ? "—" : String.format(Locale.US, "%.0f %%", v.socPercent) },
{ "RANGE", v.rangeKm == null ? "—" : String.format(Locale.US, "%.0f KM", v.rangeKm) },
{ "EFFICIENCY 5 MIN", v.usage5Min == null ? "—" : String.format(Locale.US, "%.1f", v.usage5Min) },
{ "SINCE START", v.usageTrip == null ? "—" : String.format(Locale.US, "%.1f", v.usageTrip) },
{ "TRIP", v.tripKm == null ? "—" : String.format(Locale.US, "%.1f KM", v.tripKm) },
{ "CLIMATE", v.climateCelsius == null ? "—" : String.format(Locale.US, "%.0f °C", v.climateCelsius) },
};
float rx = w * 0.47f, rw = w * 0.15f, ry = h * 0.11f, rh = h * 0.085f, rg = h * 0.035f;
for (int i = 0; i < rows.length; i++) {
float top = ry + i * (rh + rg);
int colour = redAlert && i == 0 ? alert : i % 2 == 0 ? lilac : blue;
pill(c, rx, top, rx + rw, top + rh, colour);
label(c, rows[i][0], rx + rw - 14f * d, top + rh * 0.72f, rh * 0.48f, 0xFF000000, Paint.Align.RIGHT);
label(c, rows[i][1], rx + rw + 16f * d, top + rh * 0.82f, rh * 0.85f, i == 0 && redAlert ? alert : peach, Paint.Align.LEFT);
}
// Right: the reserves as ten blocks, filled from the bottom.
float sx0 = w * 0.84f, sx1 = w * 0.93f, sTop = h * 0.11f, sBottom = h * 0.80f;
label(c, "RESERVES", (sx0 + sx1) / 2f, sTop - 8f * d, h * 0.04f, lilac, Paint.Align.CENTER);
int on = v.socPercent == null ? 0 : Math.round(Math.max(0f, Math.min(100f, v.socPercent)) / 10f);
float sh = (sBottom - sTop) / 10f;
for (int i = 0; i < 10; i++) {
float bottom = sBottom - i * sh;
fill.setColor(i < on ? (redAlert ? alert : i % 2 == 0 ? blue : lilac) : off);
rect.set(sx0, bottom - sh + 3f * d, sx1, bottom);
c.drawRoundRect(rect, i == 9 || i == 0 ? sh * 0.4f : 0f, sh * 0.4f, fill);
}
}
/**
* A frame: a vertical bar down the left joined by a rounded outer corner to a horizontal
* bar along the top (or the bottom), with a smaller rounded inner corner.
*/
private void elbow(Canvas c, float l, float t, float r, float b, float side, float bar, boolean top, int colour) {
float outer = side * 0.9f, inner = bar * 1.2f;
path.reset();
if (top) {
path.moveTo(l, b);
path.lineTo(l, t + outer);
path.quadTo(l, t, l + outer, t);
path.lineTo(r, t);
path.lineTo(r, t + bar);
path.lineTo(l + side + inner, t + bar);
path.quadTo(l + side, t + bar, l + side, t + bar + inner);
path.lineTo(l + side, b);
} else {
path.moveTo(l, t);
path.lineTo(l, b - outer);
path.quadTo(l, b, l + outer, b);
path.lineTo(r, b);
path.lineTo(r, b - bar);
path.lineTo(l + side + inner, b - bar);
path.quadTo(l + side, b - bar, l + side, b - bar - inner);
path.lineTo(l + side, t);
}
path.close();
fill.setColor(colour);
c.drawPath(path, fill);
}
private void pill(Canvas c, float l, float t, float r, float b, int colour) {
fill.setColor(colour);
rect.set(l, t, r, b);
c.drawRoundRect(rect, (b - t) / 2f, (b - t) / 2f, fill);
}
private void label(Canvas c, String s, float x, float baseline, float size, int colour, Paint.Align align) {
text.setTextSize(size);
text.setColor(colour);
text.setTextAlign(align);
c.drawText(s, x, baseline, text);
}
}