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
195 lines
9.8 KiB
Java
195 lines
9.8 KiB
Java
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);
|
||
}
|
||
}
|