App 2.37.0: Overview styles Time circuits and Quest
Time circuits, after Back to the Future: three LED rows (the car as the destination, the present time, the last departure from P), the speed in red digits and a flux capacitor that flows with the power; at 88 km/h it all flashes. Letters on fourteen segments (Segments14, tested). Quest, after World of Warcraft's interface: player frame, minimap, gold speed, cast bar, quest tracker, chat, action bar and the odometer as experience. Both in their own colours, the app with them. In THEMES.md with screenshots. Emulator only; not tried on the car. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LhzAVaUyqcw5PkGjz4mWW6
This commit is contained in:
+2
-2
@@ -34,8 +34,8 @@ android {
|
||||
applicationId "com.stinnesbeck.nilsmg4"
|
||||
minSdk 27
|
||||
targetSdk 36
|
||||
versionCode 139
|
||||
versionName "2.36.0"
|
||||
versionCode 140
|
||||
versionName "2.37.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.
|
||||
|
||||
@@ -345,12 +345,14 @@ public class MainActivity extends AppCompatActivity {
|
||||
terminalView = new TerminalView(this);
|
||||
gridView = new GridView(this);
|
||||
java.util.Collections.addAll(canvasStyles, new MinimalView(this), terminalView,
|
||||
new NixieView(this), new ChronographView(this), gridView);
|
||||
new NixieView(this), new ChronographView(this), gridView, new TimeCircuitsView(this),
|
||||
new QuestView(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, mgPane,
|
||||
canvasPanes[0], canvasPanes[1], canvasPanes[2], canvasPanes[3], canvasPanes[4] };
|
||||
canvasPanes[0], canvasPanes[1], canvasPanes[2], canvasPanes[3], canvasPanes[4],
|
||||
canvasPanes[5], canvasPanes[6] };
|
||||
// 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(
|
||||
@@ -1749,7 +1751,8 @@ public class MainActivity extends AppCompatActivity {
|
||||
getString(R.string.overview_style_mg),
|
||||
getString(R.string.overview_style_minimal), getString(R.string.overview_style_terminal),
|
||||
getString(R.string.overview_style_nixie),
|
||||
getString(R.string.overview_style_chronograph), getString(R.string.overview_style_grid) };
|
||||
getString(R.string.overview_style_chronograph), getString(R.string.overview_style_grid),
|
||||
getString(R.string.overview_style_time_circuits), getString(R.string.overview_style_quest) };
|
||||
}
|
||||
|
||||
/** The style drop-down; every style is fed on every reading, only the chosen one is shown. */
|
||||
|
||||
@@ -26,7 +26,9 @@ enum OverviewStyle {
|
||||
TERMINAL(true, true),
|
||||
NIXIE(false, false),
|
||||
CHRONOGRAPH(true, false),
|
||||
GRID(true, true);
|
||||
GRID(true, true),
|
||||
TIME_CIRCUITS(false, false),
|
||||
QUEST(false, false);
|
||||
|
||||
static final String PREF_KEY = "overview_style";
|
||||
|
||||
|
||||
@@ -0,0 +1,430 @@
|
||||
package com.evsuite.abrp;
|
||||
|
||||
import android.content.Context;
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.LinearGradient;
|
||||
import android.graphics.Paint;
|
||||
import android.graphics.Path;
|
||||
import android.graphics.RectF;
|
||||
import android.graphics.Shader;
|
||||
import android.graphics.Typeface;
|
||||
import android.os.SystemClock;
|
||||
|
||||
import java.util.Locale;
|
||||
|
||||
/**
|
||||
* The Quest Overview style, after the interface of a fantasy online game like World of Warcraft:
|
||||
* gold-trimmed frames over a dusk landscape, the car as the player character.
|
||||
*
|
||||
* Top left, the player frame: a round portrait, the gear in the level badge, the battery as the
|
||||
* health bar and power as the energy bar. Top right, the minimap with the clock. In the middle
|
||||
* the speed in large gold numbers, and under it the cast bar — "Accelerating" while drawing
|
||||
* power, "Regenerating" while recovering it, "Coasting" in between and "Resting" in P. Right,
|
||||
* the quest tracker: a road trip whose objectives are the trip, the usage and the climate.
|
||||
* Bottom left, a chat window with system lines; bottom, P R N D as action buttons and the
|
||||
* odometer as the experience bar (a level per 1000 km). Below 20 % a raid warning flashes.
|
||||
* Unread values show "—" and fill nothing. Drawn per reading; only the raid warning blinks, by
|
||||
* the readings that redraw it.
|
||||
*/
|
||||
final class QuestView extends ClusterCanvasView {
|
||||
|
||||
private static final int GOLD = 0xFFC9A24A, GOLD_DARK = 0xFF7A5C24, YELLOW = 0xFFFFD100,
|
||||
PANEL = 0xE01A130D, HEALTH = 0xFF26B33A, ENERGY = 0xFFF2D21B, REGEN = 0xFF3FBF6F,
|
||||
XP = 0xFF8E44C9, RED = 0xFFFF3B30, SYSTEM = 0xFFFFE96A;
|
||||
|
||||
private final Paint line = new Paint(Paint.ANTI_ALIAS_FLAG);
|
||||
private final Paint fill = new Paint(Paint.ANTI_ALIAS_FLAG);
|
||||
private final Paint text = new Paint(Paint.ANTI_ALIAS_FLAG);
|
||||
private final Paint skyPaint = new Paint();
|
||||
private final Path path = new Path();
|
||||
private final RectF r = new RectF();
|
||||
private final Typeface serif = Typeface.create(Typeface.SERIF, Typeface.BOLD);
|
||||
private final Typeface sans = Typeface.create("sans-serif-condensed", Typeface.BOLD);
|
||||
private int skyFor = -1;
|
||||
|
||||
QuestView(Context context) {
|
||||
super(context);
|
||||
line.setStyle(Paint.Style.STROKE);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDraw(Canvas c) {
|
||||
super.onDraw(c);
|
||||
float w = getWidth(), h = getHeight();
|
||||
if (w <= 0 || h <= 0) return;
|
||||
float u = Math.min(h / 640f, w / 1100f);
|
||||
drawLandscape(c, w, h);
|
||||
drawPlayer(c, u);
|
||||
drawMinimap(c, w, u);
|
||||
drawSpeed(c, w, u);
|
||||
drawCastBar(c, w, u);
|
||||
drawQuests(c, w, u);
|
||||
drawChat(c, u);
|
||||
drawActionBar(c, w, h, u);
|
||||
drawXp(c, w, h, u);
|
||||
}
|
||||
|
||||
// ---------- World ----------
|
||||
|
||||
private void drawLandscape(Canvas c, float w, float h) {
|
||||
if (skyFor != (int) h) {
|
||||
skyPaint.setShader(new LinearGradient(0, 0, 0, h, new int[] { 0xFF0B1424, 0xFF1C2A3A, 0xFF2A2418 },
|
||||
new float[] { 0f, 0.55f, 1f }, Shader.TileMode.CLAMP));
|
||||
skyFor = (int) h;
|
||||
}
|
||||
c.drawRect(0, 0, w, h, skyPaint);
|
||||
// Two ranges of hills, the near one darker.
|
||||
hills(c, w, h, h * 0.62f, 0xFF16202A, 0.9f);
|
||||
hills(c, w, h, h * 0.72f, 0xFF10171D, 1.6f);
|
||||
}
|
||||
|
||||
private void hills(Canvas c, float w, float h, float base, int colour, float wave) {
|
||||
path.reset();
|
||||
path.moveTo(0, h);
|
||||
for (int i = 0; i <= 24; i++) {
|
||||
float x = w * i / 24f;
|
||||
float y = base - (float) (Math.sin(i * wave) * 0.5 + Math.sin(i * wave * 0.37 + 1) * 0.5) * h * 0.06f;
|
||||
path.lineTo(x, y);
|
||||
}
|
||||
path.lineTo(w, h);
|
||||
path.close();
|
||||
fill.setColor(colour);
|
||||
c.drawPath(path, fill);
|
||||
}
|
||||
|
||||
// ---------- Frames ----------
|
||||
|
||||
/** A dark panel with a gold rim. */
|
||||
private void panel(Canvas c, float l, float t, float rr, float b, float u) {
|
||||
r.set(l, t, rr, b);
|
||||
fill.setColor(PANEL);
|
||||
c.drawRoundRect(r, 6f * u, 6f * u, fill);
|
||||
line.setColor(GOLD_DARK);
|
||||
line.setStrokeWidth(4f * u);
|
||||
c.drawRoundRect(r, 6f * u, 6f * u, line);
|
||||
line.setColor(GOLD);
|
||||
line.setStrokeWidth(1.5f * u);
|
||||
c.drawRoundRect(r, 6f * u, 6f * u, line);
|
||||
}
|
||||
|
||||
/** Text with a dark outline, as game interfaces write over the world. */
|
||||
private void outlined(Canvas c, String s, float x, float y, float size, int colour, Typeface face,
|
||||
Paint.Align align, float u) {
|
||||
text.setTypeface(face);
|
||||
text.setTextSize(size);
|
||||
text.setTextAlign(align);
|
||||
text.setStyle(Paint.Style.STROKE);
|
||||
text.setStrokeWidth(Math.max(2f * u, size * 0.08f));
|
||||
text.setStrokeJoin(Paint.Join.ROUND);
|
||||
text.setColor(0xFF000000);
|
||||
c.drawText(s, x, y, text);
|
||||
text.setStyle(Paint.Style.FILL);
|
||||
text.setColor(colour);
|
||||
c.drawText(s, x, y, text);
|
||||
}
|
||||
|
||||
/** A bar with a dark track, a fill of [frac] and centred text. */
|
||||
private void bar(Canvas c, float l, float t, float rr, float b, float frac, int colour, String label, float u) {
|
||||
r.set(l, t, rr, b);
|
||||
fill.setColor(0xFF0A0A0A);
|
||||
c.drawRect(r, fill);
|
||||
if (frac > 0f) {
|
||||
r.set(l, t, l + (rr - l) * Math.min(1f, frac), b);
|
||||
fill.setColor(colour);
|
||||
c.drawRect(r, fill);
|
||||
fill.setColor(0x33FFFFFF);
|
||||
c.drawRect(r.left, t, r.right, t + (b - t) * 0.35f, fill);
|
||||
}
|
||||
line.setColor(GOLD_DARK);
|
||||
line.setStrokeWidth(2f * u);
|
||||
c.drawRect(l, t, rr, b, line);
|
||||
outlined(c, label, (l + rr) / 2f, b - (b - t) * 0.22f, (b - t) * 0.72f, 0xFFFFFFFF, sans,
|
||||
Paint.Align.CENTER, u);
|
||||
}
|
||||
|
||||
// ---------- Player ----------
|
||||
|
||||
private void drawPlayer(Canvas c, float u) {
|
||||
float px = 86f * u, py = 86f * u, pr = 60f * u;
|
||||
// Name plate and bars, behind the portrait.
|
||||
float l = px + 20f * u, rr = px + 380f * u;
|
||||
panel(c, l, py - 56f * u, rr, py + 46f * u, u);
|
||||
outlined(c, "MG4", l + 60f * u, py - 28f * u, 24f * u, YELLOW, serif, Paint.Align.LEFT, u);
|
||||
float soc = v.socPercent == null ? 0f : v.socPercent / 100f;
|
||||
boolean low = v.socPercent != null && v.socPercent < 20f;
|
||||
bar(c, l + 56f * u, py - 18f * u, rr - 12f * u, py + 10f * u, soc, low ? RED : HEALTH,
|
||||
fmt(v.socPercent, "%.0f %%", "—") + " · " + fmt(v.rangeKm, "%.0f km", "—"), u);
|
||||
boolean regen = v.powerKw != null && v.powerKw < -0.5f;
|
||||
float energy = v.powerKw == null ? 0f : Math.abs(v.powerKw) / 150f;
|
||||
bar(c, l + 56f * u, py + 14f * u, rr - 12f * u, py + 36f * u, energy, regen ? REGEN : ENERGY,
|
||||
v.powerKw == null ? "—" : String.format(Locale.US, regen ? "+%.1f kW" : "%.1f kW", Math.abs(v.powerKw)), u);
|
||||
|
||||
// The portrait: a gold ring round the car's name.
|
||||
fill.setColor(0xFF140F0A);
|
||||
c.drawCircle(px, py, pr, fill);
|
||||
line.setColor(GOLD_DARK);
|
||||
line.setStrokeWidth(10f * u);
|
||||
c.drawCircle(px, py, pr, line);
|
||||
line.setColor(GOLD);
|
||||
line.setStrokeWidth(3f * u);
|
||||
c.drawCircle(px, py, pr + 3f * u, line);
|
||||
drawCarGlyph(c, px, py + 6f * u, pr * 0.62f, u);
|
||||
// The level badge: the gear.
|
||||
float bx = px - pr * 0.72f, by = py + pr * 0.72f, br = 20f * u;
|
||||
fill.setColor(0xFF140F0A);
|
||||
c.drawCircle(bx, by, br, fill);
|
||||
line.setColor(GOLD);
|
||||
line.setStrokeWidth(3f * u);
|
||||
c.drawCircle(bx, by, br, line);
|
||||
String gear = Gear.label(v.gear);
|
||||
outlined(c, gear.isEmpty() ? "?" : gear, bx, by + 8f * u, 22f * u, YELLOW, serif, Paint.Align.CENTER, u);
|
||||
}
|
||||
|
||||
/** A small car in profile for the portrait. */
|
||||
private void drawCarGlyph(Canvas c, float cx, float cy, float s, float u) {
|
||||
path.reset();
|
||||
path.moveTo(cx - s, cy + s * 0.15f);
|
||||
path.lineTo(cx - s * 0.92f, cy - s * 0.12f);
|
||||
path.quadTo(cx - s * 0.55f, cy - s * 0.2f, cx - s * 0.35f, cy - s * 0.48f);
|
||||
path.lineTo(cx + s * 0.3f, cy - s * 0.5f);
|
||||
path.quadTo(cx + s * 0.62f, cy - s * 0.22f, cx + s * 0.95f, cy - s * 0.1f);
|
||||
path.lineTo(cx + s, cy + s * 0.15f);
|
||||
path.close();
|
||||
fill.setColor(GOLD);
|
||||
c.drawPath(path, fill);
|
||||
fill.setColor(0xFF140F0A);
|
||||
c.drawCircle(cx - s * 0.55f, cy + s * 0.18f, s * 0.2f, fill);
|
||||
c.drawCircle(cx + s * 0.55f, cy + s * 0.18f, s * 0.2f, fill);
|
||||
fill.setColor(GOLD);
|
||||
c.drawCircle(cx - s * 0.55f, cy + s * 0.18f, s * 0.11f, fill);
|
||||
c.drawCircle(cx + s * 0.55f, cy + s * 0.18f, s * 0.11f, fill);
|
||||
}
|
||||
|
||||
// ---------- Minimap ----------
|
||||
|
||||
private void drawMinimap(Canvas c, float w, float u) {
|
||||
float cx = w - 120f * u, cy = 128f * u, mr = 84f * u;
|
||||
outlined(c, "The Open Road", cx, 30f * u, 20f * u, YELLOW, serif, Paint.Align.CENTER, u);
|
||||
fill.setColor(0xFF1E3524);
|
||||
c.drawCircle(cx, cy, mr, fill);
|
||||
// Roads and a river, clipped to the map.
|
||||
c.save();
|
||||
path.reset();
|
||||
path.addCircle(cx, cy, mr, Path.Direction.CW);
|
||||
c.clipPath(path);
|
||||
line.setColor(0xFF8A7650);
|
||||
line.setStrokeWidth(5f * u);
|
||||
c.drawLine(cx, cy + mr, cx, cy - mr, line);
|
||||
c.drawLine(cx - mr, cy - mr * 0.35f, cx + mr, cy + mr * 0.1f, line);
|
||||
line.setColor(0xFF2E5C8A);
|
||||
line.setStrokeWidth(7f * u);
|
||||
path.reset();
|
||||
path.moveTo(cx - mr, cy + mr * 0.5f);
|
||||
path.quadTo(cx - mr * 0.2f, cy + mr * 0.2f, cx + mr, cy + mr * 0.7f);
|
||||
c.drawPath(path, line);
|
||||
c.restore();
|
||||
// The player arrow.
|
||||
path.reset();
|
||||
path.moveTo(cx, cy - 13f * u);
|
||||
path.lineTo(cx + 9f * u, cy + 10f * u);
|
||||
path.lineTo(cx, cy + 5f * u);
|
||||
path.lineTo(cx - 9f * u, cy + 10f * u);
|
||||
path.close();
|
||||
fill.setColor(YELLOW);
|
||||
c.drawPath(path, fill);
|
||||
line.setColor(0xFF000000);
|
||||
line.setStrokeWidth(1.5f * u);
|
||||
c.drawPath(path, line);
|
||||
// The stone rim.
|
||||
line.setColor(0xFF4A4036);
|
||||
line.setStrokeWidth(12f * u);
|
||||
c.drawCircle(cx, cy, mr + 5f * u, line);
|
||||
line.setColor(GOLD);
|
||||
line.setStrokeWidth(2f * u);
|
||||
c.drawCircle(cx, cy, mr + 11f * u, line);
|
||||
c.drawCircle(cx, cy, mr - 1f * u, line);
|
||||
// The clock under it.
|
||||
panel(c, cx - 50f * u, cy + mr + 16f * u, cx + 50f * u, cy + mr + 48f * u, u);
|
||||
outlined(c, v.clock.isEmpty() ? "—" : v.clock, cx, cy + mr + 40f * u, 22f * u, 0xFFFFFFFF, sans,
|
||||
Paint.Align.CENTER, u);
|
||||
}
|
||||
|
||||
// ---------- Speed and cast bar ----------
|
||||
|
||||
private void drawSpeed(Canvas c, float w, float u) {
|
||||
boolean low = v.socPercent != null && v.socPercent < 20f;
|
||||
if (low && (SystemClock.uptimeMillis() / 600) % 2 == 0) {
|
||||
outlined(c, "Low battery!", w / 2f, 64f * u, 34f * u, RED, serif, Paint.Align.CENTER, u);
|
||||
}
|
||||
outlined(c, fmt(v.speedKmh, "%.0f", "—"), w / 2f, 236f * u, 150f * u, YELLOW, serif, Paint.Align.CENTER, u);
|
||||
outlined(c, "km/h", w / 2f, 270f * u, 24f * u, GOLD, serif, Paint.Align.CENTER, u);
|
||||
}
|
||||
|
||||
private void drawCastBar(Canvas c, float w, float u) {
|
||||
boolean parked = v.gear != null && v.gear == 1;
|
||||
Float p = v.powerKw;
|
||||
String spell;
|
||||
int colour;
|
||||
float frac;
|
||||
if (parked) {
|
||||
spell = "Resting";
|
||||
colour = 0xFF5B7BD9;
|
||||
frac = 1f;
|
||||
} else if (p == null) {
|
||||
spell = "—";
|
||||
colour = ENERGY;
|
||||
frac = 0f;
|
||||
} else if (p > 0.5f) {
|
||||
spell = "Accelerating";
|
||||
colour = 0xFFFFB300;
|
||||
frac = p / 150f;
|
||||
} else if (p < -0.5f) {
|
||||
spell = "Regenerating";
|
||||
colour = REGEN;
|
||||
frac = -p / 50f;
|
||||
} else {
|
||||
spell = "Coasting";
|
||||
colour = 0xFF8F8F8F;
|
||||
frac = 0.02f;
|
||||
}
|
||||
float bw = Math.min(520f * u, w * 0.36f), l = (w - bw) / 2f + 18f * u, rr = l + bw, t = 296f * u, b = t + 30f * u;
|
||||
// The spell icon.
|
||||
float il = l - 40f * u;
|
||||
r.set(il, t - 3f * u, il + 36f * u, b + 3f * u);
|
||||
fill.setColor(0xFF140F0A);
|
||||
c.drawRect(r, fill);
|
||||
line.setColor(GOLD);
|
||||
line.setStrokeWidth(2f * u);
|
||||
c.drawRect(r, line);
|
||||
drawSpellIcon(c, r.centerX(), r.centerY(), 13f * u, spell, colour, u);
|
||||
r.set(l, t, rr, b);
|
||||
fill.setColor(0xFF0A0A0A);
|
||||
c.drawRect(r, fill);
|
||||
r.set(l, t, l + bw * Math.max(0f, Math.min(1f, frac)), b);
|
||||
fill.setColor(colour);
|
||||
c.drawRect(r, fill);
|
||||
fill.setColor(0x33FFFFFF);
|
||||
c.drawRect(r.left, t, r.right, t + 10f * u, fill);
|
||||
line.setColor(GOLD);
|
||||
line.setStrokeWidth(2.5f * u);
|
||||
c.drawRect(l, t, rr, b, line);
|
||||
outlined(c, spell, l + 10f * u, b - 8f * u, 20f * u, 0xFFFFFFFF, sans, Paint.Align.LEFT, u);
|
||||
outlined(c, p == null ? "—" : String.format(Locale.US, "%.1f kW", Math.abs(p)), rr - 10f * u, b - 8f * u,
|
||||
20f * u, 0xFFFFFFFF, sans, Paint.Align.RIGHT, u);
|
||||
}
|
||||
|
||||
private void drawSpellIcon(Canvas c, float cx, float cy, float s, String spell, int colour, float u) {
|
||||
fill.setColor(colour);
|
||||
path.reset();
|
||||
switch (spell) {
|
||||
case "Accelerating": // a lightning bolt
|
||||
path.moveTo(cx + s * 0.2f, cy - s);
|
||||
path.lineTo(cx - s * 0.6f, cy + s * 0.1f);
|
||||
path.lineTo(cx, cy + s * 0.1f);
|
||||
path.lineTo(cx - s * 0.2f, cy + s);
|
||||
path.lineTo(cx + s * 0.6f, cy - s * 0.15f);
|
||||
path.lineTo(cx, cy - s * 0.15f);
|
||||
path.close();
|
||||
c.drawPath(path, fill);
|
||||
break;
|
||||
case "Regenerating": // a leaf
|
||||
path.moveTo(cx - s * 0.8f, cy + s * 0.8f);
|
||||
path.quadTo(cx - s, cy - s, cx + s * 0.8f, cy - s * 0.8f);
|
||||
path.quadTo(cx + s, cy + s * 0.9f, cx - s * 0.8f, cy + s * 0.8f);
|
||||
c.drawPath(path, fill);
|
||||
break;
|
||||
case "Resting":
|
||||
outlined(c, "Zz", cx, cy + s * 0.5f, s * 1.5f, colour, serif, Paint.Align.CENTER, u);
|
||||
break;
|
||||
default:
|
||||
c.drawCircle(cx, cy, s * 0.5f, fill);
|
||||
}
|
||||
}
|
||||
|
||||
// ---------- Quest tracker and chat ----------
|
||||
|
||||
private void drawQuests(Canvas c, float w, float u) {
|
||||
float x = w - 400f * u, y = 310f * u;
|
||||
fill.setColor(YELLOW);
|
||||
c.drawCircle(x - 18f * u, y - 9f * u, 11f * u, fill);
|
||||
outlined(c, "!", x - 18f * u, y - 1f * u, 18f * u, 0xFF000000, serif, Paint.Align.CENTER, u);
|
||||
outlined(c, "Road trip", x, y, 26f * u, YELLOW, serif, Paint.Align.LEFT, u);
|
||||
String[] objectives = {
|
||||
"Distance: " + fmt(v.tripKm, "%.1f km", "—"),
|
||||
"Usage, 5 min: " + fmt(v.usage5Min, "%.1f kWh/100 km", "—"),
|
||||
"Since start: " + fmt(v.usageTrip, "%.1f kWh/100 km", "—"),
|
||||
"Climate: " + fmt(v.climateCelsius, "%.0f °C", "—"),
|
||||
};
|
||||
for (int i = 0; i < objectives.length; i++) {
|
||||
outlined(c, "– " + objectives[i], x + 8f * u, y + (34f + i * 30f) * u, 21f * u, 0xFFE8E8E8, sans,
|
||||
Paint.Align.LEFT, u);
|
||||
}
|
||||
}
|
||||
|
||||
private void drawChat(Canvas c, float u) {
|
||||
float l = 22f * u, t = 368f * u, rr = l + 470f * u, b = 520f * u;
|
||||
r.set(l, t, rr, b);
|
||||
fill.setColor(0x66000000);
|
||||
c.drawRoundRect(r, 6f * u, 6f * u, fill);
|
||||
String state = v.gear == null ? "The car is not answering."
|
||||
: v.gear == 1 ? "You feel rested."
|
||||
: v.gear == 2 ? "You are moving backwards."
|
||||
: "You are on the road.";
|
||||
String[] lines = {
|
||||
"Range remaining: " + fmt(v.rangeKm, "%.0f km", "—") + ".",
|
||||
"Odometer: " + fmt(v.odometerKm, "%,.0f km", "—") + ".",
|
||||
state,
|
||||
};
|
||||
for (int i = 0; i < lines.length; i++) {
|
||||
outlined(c, lines[i], l + 14f * u, t + (38f + i * 40f) * u, 21f * u, SYSTEM, sans, Paint.Align.LEFT, u);
|
||||
}
|
||||
}
|
||||
|
||||
// ---------- Action bar and experience ----------
|
||||
|
||||
private void drawActionBar(Canvas c, float w, float h, float u) {
|
||||
String[] gears = { "P", "R", "N", "D" };
|
||||
String now = Gear.label(v.gear);
|
||||
float s = 64f * u, gap = 8f * u, total = gears.length * s + (gears.length - 1) * gap;
|
||||
float x = (w - total) / 2f, top = h - 104f * u;
|
||||
panel(c, x - 12f * u, top - 10f * u, x + total + 12f * u, top + s + 10f * u, u);
|
||||
for (int i = 0; i < gears.length; i++) {
|
||||
boolean on = gears[i].equals(now);
|
||||
r.set(x, top, x + s, top + s);
|
||||
fill.setColor(on ? 0xFF3A2A12 : 0xFF141414);
|
||||
c.drawRect(r, fill);
|
||||
line.setColor(on ? YELLOW : GOLD_DARK);
|
||||
line.setStrokeWidth((on ? 4f : 2f) * u);
|
||||
c.drawRect(r, line);
|
||||
outlined(c, gears[i], x + s / 2f, top + s * 0.72f, 36f * u, on ? YELLOW : 0xFF9A9A9A, serif,
|
||||
Paint.Align.CENTER, u);
|
||||
outlined(c, String.valueOf(i + 1), x + s - 6f * u, top + 16f * u, 13f * u, 0xFFD0D0D0, sans,
|
||||
Paint.Align.RIGHT, u);
|
||||
x += s + gap;
|
||||
}
|
||||
}
|
||||
|
||||
/** The odometer as experience: a level per 1000 km, the bar the way through this one. */
|
||||
private void drawXp(Canvas c, float w, float h, float u) {
|
||||
float l = 22f * u, rr = w - 22f * u, t = h - 26f * u, b = h - 10f * u;
|
||||
r.set(l, t, rr, b);
|
||||
fill.setColor(0xFF0A0A0A);
|
||||
c.drawRect(r, fill);
|
||||
String label = "—";
|
||||
if (v.odometerKm != null) {
|
||||
float into = v.odometerKm % 1000f;
|
||||
r.set(l, t, l + (rr - l) * into / 1000f, b);
|
||||
fill.setColor(XP);
|
||||
c.drawRect(r, fill);
|
||||
label = String.format(Locale.US, "Level %d · %.0f / 1000 km", (int) (v.odometerKm / 1000f) + 1, into);
|
||||
}
|
||||
line.setColor(GOLD_DARK);
|
||||
line.setStrokeWidth(1.5f * u);
|
||||
for (int i = 1; i < 20; i++) {
|
||||
float x = l + (rr - l) * i / 20f;
|
||||
c.drawLine(x, t, x, b, line);
|
||||
}
|
||||
c.drawRect(l, t, rr, b, line);
|
||||
outlined(c, label, w / 2f, b - 3f * u, 13f * u, 0xFFFFFFFF, sans, Paint.Align.CENTER, u);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
package com.evsuite.abrp;
|
||||
|
||||
/**
|
||||
* Fourteen-segment letters, as on the time circuits the Time circuits style is drawn after: the
|
||||
* months and the gear words. Pure Java so it can be tested off the car.
|
||||
*
|
||||
* Segments as bits: the outer seven like {@link SevenSegment} (a top, b upper right, c lower
|
||||
* right, d bottom, e lower left, f upper left), the middle bar in two halves (g1 left, g2
|
||||
* right), and the inner six: h from the upper left corner to the centre, i up the middle, j to
|
||||
* the upper right corner, k to the lower left, l down the middle, m to the lower right.
|
||||
*/
|
||||
final class Segments14 {
|
||||
|
||||
static final int A = 1, B = 2, C = 4, D = 8, E = 16, F = 32, G1 = 64, G2 = 128,
|
||||
H = 256, I = 512, J = 1024, K = 2048, L = 4096, M = 8192;
|
||||
|
||||
private Segments14() {}
|
||||
|
||||
/** The segments a letter, digit or '-' lights; anything else (a space) lights none. */
|
||||
static int mask(char ch) {
|
||||
switch (Character.toUpperCase(ch)) {
|
||||
case 'A': return A | B | C | E | F | G1 | G2;
|
||||
case 'B': return A | B | C | D | I | L | G2;
|
||||
case 'C': return A | D | E | F;
|
||||
case 'D': return A | B | C | D | I | L;
|
||||
case 'E': return A | D | E | F | G1 | G2;
|
||||
case 'F': return A | E | F | G1;
|
||||
case 'G': return A | C | D | E | F | G2;
|
||||
case 'H': return B | C | E | F | G1 | G2;
|
||||
case 'I': return A | D | I | L;
|
||||
case 'J': return B | C | D | E;
|
||||
case 'K': return E | F | G1 | J | M;
|
||||
case 'L': return D | E | F;
|
||||
case 'M': return B | C | E | F | H | J;
|
||||
case 'N': return B | C | E | F | H | M;
|
||||
case 'O': return A | B | C | D | E | F;
|
||||
case 'P': return A | B | E | F | G1 | G2;
|
||||
case 'R': return A | B | E | F | G1 | G2 | M;
|
||||
case 'S': return A | C | D | F | G1 | G2;
|
||||
case 'T': return A | I | L;
|
||||
case 'U': return B | C | D | E | F;
|
||||
case 'V': return E | F | J | K;
|
||||
case 'W': return B | C | E | F | K | M;
|
||||
case 'Y': return H | J | L;
|
||||
case '0': return A | B | C | D | E | F | J | K;
|
||||
case '1': return B | C;
|
||||
case '2': return A | B | D | E | G1 | G2;
|
||||
case '3': return A | B | C | D | G2;
|
||||
case '4': return B | C | F | G1 | G2;
|
||||
case '5': return A | C | D | F | G1 | G2;
|
||||
case '6': return A | C | D | E | F | G1 | G2;
|
||||
case '7': return A | B | C;
|
||||
case '8': return A | B | C | D | E | F | G1 | G2;
|
||||
case '9': return A | B | C | D | F | G1 | G2;
|
||||
case '-': return G1 | G2;
|
||||
default: return 0;
|
||||
}
|
||||
}
|
||||
|
||||
/** The three-letter month as the time circuits show it: JAN … DEC; "---" out of range. */
|
||||
static String month(int month0) {
|
||||
String[] m = { "JAN", "FEB", "MAR", "APR", "MAY", "JUN", "JUL", "AUG", "SEP", "OCT", "NOV", "DEC" };
|
||||
return month0 >= 0 && month0 < 12 ? m[month0] : "---";
|
||||
}
|
||||
|
||||
/** The gear as a three-letter word for the time circuits: PRK, REV, NTL, DRV; "---" unread. */
|
||||
static String gearWord(Integer gear) {
|
||||
if (gear == null) return "---";
|
||||
switch (gear) {
|
||||
case 1: return "PRK";
|
||||
case 2: return "REV";
|
||||
case 3: return "NTL";
|
||||
case 4: return "DRV";
|
||||
default: return "---";
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -19,6 +19,8 @@ final class StylePalette {
|
||||
case CARTOON: return R.style.ThemeOverlay_EV_Style_Cartoon;
|
||||
case NIXIE: return R.style.ThemeOverlay_EV_Style_Nixie;
|
||||
case GRID: return R.style.ThemeOverlay_EV_Style_Grid;
|
||||
case TIME_CIRCUITS: return R.style.ThemeOverlay_EV_Style_TimeCircuits;
|
||||
case QUEST: return R.style.ThemeOverlay_EV_Style_Quest;
|
||||
default: return 0;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,404 @@
|
||||
package com.evsuite.abrp;
|
||||
|
||||
import android.content.Context;
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.LinearGradient;
|
||||
import android.graphics.Paint;
|
||||
import android.graphics.RectF;
|
||||
import android.graphics.Shader;
|
||||
import android.graphics.Typeface;
|
||||
import android.os.SystemClock;
|
||||
|
||||
import java.util.Calendar;
|
||||
import java.util.Locale;
|
||||
|
||||
/**
|
||||
* The Time circuits Overview style, after the time machine in Back to the Future: three rows of
|
||||
* LED read-outs in a metal panel, labelled with embossed tape, a red speed display and a flux
|
||||
* capacitor.
|
||||
*
|
||||
* The rows keep the film's look, each in its colour: red "DESTINATION" — the gear as a word
|
||||
* (DRV, REV, NTL, PRK), the charge, the range (where you can still get to), power or
|
||||
* regeneration on the lamps, and the usage over 5 minutes; green "PRESENT TIME" — today's date
|
||||
* and time; amber "LAST TIME DEPARTED" — when the car last left P, dashes until it has been seen
|
||||
* to. Right, the speed in large red digits and the flux capacitor, whose light runs to its
|
||||
* centre faster with more power and outwards while regenerating; at 88 km/h it flashes. Along
|
||||
* the bottom, trip, usage since start, climate and odometer. Unread values show dashes and light
|
||||
* nothing. It animates (about 30 frames a second) only while there is power to show or at 88,
|
||||
* and only on screen.
|
||||
*/
|
||||
final class TimeCircuitsView extends ClusterCanvasView {
|
||||
|
||||
private static final int RED = 0xFFFF3A26, GREEN = 0xFF3DFF5A, AMBER = 0xFFFFA726, WHITE_HOT = 0xFFFFF4D6;
|
||||
|
||||
private final Paint line = new Paint(Paint.ANTI_ALIAS_FLAG);
|
||||
private final Paint fill = new Paint(Paint.ANTI_ALIAS_FLAG);
|
||||
private final Paint text = new Paint(Paint.ANTI_ALIAS_FLAG);
|
||||
private final Paint metal = new Paint(Paint.ANTI_ALIAS_FLAG);
|
||||
private final RectF r = new RectF();
|
||||
private final Typeface tape = Typeface.create("sans-serif-condensed", Typeface.BOLD);
|
||||
private int metalFor = -1;
|
||||
|
||||
/** When the car last left P, wall clock; 0 until that has been seen. */
|
||||
private long departedAt;
|
||||
private Integer lastGear;
|
||||
private float fluxPhase;
|
||||
private long lastFrame;
|
||||
|
||||
TimeCircuitsView(Context context) {
|
||||
super(context);
|
||||
line.setStyle(Paint.Style.STROKE);
|
||||
line.setStrokeCap(Paint.Cap.ROUND);
|
||||
}
|
||||
|
||||
@Override
|
||||
void setValues(ClusterValues values) {
|
||||
// Readings arrive whether or not the style is shown, so the departure is never missed.
|
||||
Integer gear = values == null ? null : values.gear;
|
||||
if (gear != null && lastGear != null && lastGear == 1 && gear != 1) {
|
||||
departedAt = System.currentTimeMillis();
|
||||
}
|
||||
if (gear != null) lastGear = gear;
|
||||
super.setValues(values);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDraw(Canvas c) {
|
||||
super.onDraw(c);
|
||||
float w = getWidth(), h = getHeight();
|
||||
if (w <= 0 || h <= 0) return;
|
||||
float u = Math.min(h / 640f, w / 1100f);
|
||||
advance();
|
||||
c.drawColor(0xFF050505);
|
||||
|
||||
float split = w * 0.64f;
|
||||
drawPanel(c, 12f * u, 10f * u, split - 6f * u, h - 10f * u, u);
|
||||
float rowLeft = 34f * u, rowRight = split - 24f * u;
|
||||
drawCarRow(c, rowLeft, rowRight, 22f * u, u);
|
||||
drawTimeRow(c, rowLeft, rowRight, 170f * u, u, GREEN, "PRESENT TIME", System.currentTimeMillis());
|
||||
drawTimeRow(c, rowLeft, rowRight, 318f * u, u, AMBER, "LAST TIME DEPARTED", departedAt);
|
||||
drawBottom(c, rowLeft, rowRight, 470f * u, u);
|
||||
|
||||
drawPanel(c, split + 6f * u, 10f * u, w - 12f * u, h - 10f * u, u);
|
||||
drawSpeed(c, split + 28f * u, w - 28f * u, 26f * u, u);
|
||||
drawFlux(c, split + 28f * u, w - 28f * u, 262f * u, h - 26f * u, u);
|
||||
|
||||
boolean moving = (v.powerKw != null && Math.abs(v.powerKw) > 0.5f) || at88();
|
||||
keepAnimating(moving, 33);
|
||||
}
|
||||
|
||||
private boolean at88() {
|
||||
return v.speedKmh != null && v.speedKmh >= 88f;
|
||||
}
|
||||
|
||||
private void advance() {
|
||||
long now = SystemClock.uptimeMillis();
|
||||
float dt = lastFrame == 0 ? 0f : Math.min(0.2f, (now - lastFrame) / 1000f);
|
||||
lastFrame = now;
|
||||
if (v.powerKw != null) fluxPhase = (fluxPhase + v.powerKw * 0.025f * dt + 1000f) % 1f;
|
||||
}
|
||||
|
||||
// ---------- Panels and tape ----------
|
||||
|
||||
private void drawPanel(Canvas c, float l, float t, float rr, float b, float u) {
|
||||
if (metalFor != (int) (b - t)) {
|
||||
metal.setShader(new LinearGradient(0, t, 0, b, 0xFF2C2D31, 0xFF18191B, Shader.TileMode.CLAMP));
|
||||
metalFor = (int) (b - t);
|
||||
}
|
||||
r.set(l, t, rr, b);
|
||||
c.drawRoundRect(r, 10f * u, 10f * u, metal);
|
||||
line.setColor(0xFF45464B);
|
||||
line.setStrokeWidth(2f * u);
|
||||
c.drawRoundRect(r, 10f * u, 10f * u, line);
|
||||
fill.setColor(0xFF5A5B60);
|
||||
for (float x : new float[] { l + 14f * u, rr - 14f * u }) {
|
||||
for (float y : new float[] { t + 14f * u, b - 14f * u }) c.drawCircle(x, y, 4f * u, fill);
|
||||
}
|
||||
}
|
||||
|
||||
/** Embossed label tape: black, raised white capitals, centred on x. */
|
||||
private void tape(Canvas c, String s, float x, float y, float u, int tint) {
|
||||
text.setTypeface(tape);
|
||||
text.setTextSize(18f * u);
|
||||
text.setLetterSpacing(0.18f);
|
||||
text.setTextAlign(Paint.Align.CENTER);
|
||||
float tw = text.measureText(s);
|
||||
r.set(x - tw / 2f - 14f * u, y - 18f * u, x + tw / 2f + 14f * u, y + 7f * u);
|
||||
fill.setColor(tint);
|
||||
c.drawRoundRect(r, 4f * u, 4f * u, fill);
|
||||
fill.setColor(0x22FFFFFF);
|
||||
c.drawRect(r.left + 4f * u, r.top + 3f * u, r.right - 4f * u, r.top + 5f * u, fill);
|
||||
text.setColor(0xFFF2F2F2);
|
||||
c.drawText(s, x, y, text);
|
||||
text.setLetterSpacing(0f);
|
||||
}
|
||||
|
||||
/** A small caption above a field. */
|
||||
private void caption(Canvas c, String s, float x, float y, float u) {
|
||||
text.setTypeface(tape);
|
||||
text.setTextSize(13f * u);
|
||||
text.setLetterSpacing(0.12f);
|
||||
text.setTextAlign(Paint.Align.CENTER);
|
||||
text.setColor(0xFFBFC1C6);
|
||||
c.drawText(s, x, y, text);
|
||||
text.setLetterSpacing(0f);
|
||||
}
|
||||
|
||||
// ---------- Rows ----------
|
||||
|
||||
/** The cells of one field: 'a' alphanumeric (fourteen segments), 'n' digits, 'l' lamps, ':' colon. */
|
||||
private static final class Field {
|
||||
final char kind;
|
||||
final int cells;
|
||||
final String caption;
|
||||
|
||||
Field(char kind, int cells, String caption) {
|
||||
this.kind = kind;
|
||||
this.cells = cells;
|
||||
this.caption = caption;
|
||||
}
|
||||
}
|
||||
|
||||
private static final Field[] CAR = {
|
||||
new Field('a', 3, "GEAR"), new Field('n', 3, "CHARGE %"), new Field('n', 4, "RANGE KM"),
|
||||
new Field('l', 0, ""), new Field('n', 3, "USAGE"),
|
||||
};
|
||||
private static final Field[] TIME = {
|
||||
new Field('a', 3, "MONTH"), new Field('n', 2, "DAY"), new Field('n', 4, "YEAR"),
|
||||
new Field('l', 0, ""), new Field('n', 2, "HOUR"), new Field(':', 0, ""), new Field('n', 2, "MIN"),
|
||||
};
|
||||
|
||||
private void drawCarRow(Canvas c, float left, float right, float top, float u) {
|
||||
String charge = v.socPercent == null ? "---" : String.format(Locale.US, "%3.0f", v.socPercent).replace(' ', '0');
|
||||
String range = v.rangeKm == null ? "----" : String.format(Locale.US, "%04.0f", Math.min(9999f, v.rangeKm));
|
||||
String usage = v.usage5Min == null ? "---" : String.format(Locale.US, "%.1f", Math.min(99.9, Math.abs(v.usage5Min)));
|
||||
boolean regen = v.powerKw != null && v.powerKw < -0.5f, power = v.powerKw != null && v.powerKw > 0.5f;
|
||||
drawRow(c, left, right, top, u, RED, "DESTINATION", CAR,
|
||||
new String[] { Segments14.gearWord(v.gear), charge, range, null, usage },
|
||||
new String[] { "POWER", "REGEN" }, new boolean[] { power, regen });
|
||||
}
|
||||
|
||||
private void drawTimeRow(Canvas c, float left, float right, float top, float u, int colour, String title, long when) {
|
||||
String[] values;
|
||||
boolean[] lamps = { false, false };
|
||||
if (when <= 0) {
|
||||
values = new String[] { "---", "--", "----", null, "--", null, "--" };
|
||||
} else {
|
||||
Calendar cal = Calendar.getInstance();
|
||||
cal.setTimeInMillis(when);
|
||||
int hour = cal.get(Calendar.HOUR_OF_DAY);
|
||||
values = new String[] { Segments14.month(cal.get(Calendar.MONTH)),
|
||||
String.format(Locale.US, "%02d", cal.get(Calendar.DAY_OF_MONTH)),
|
||||
String.format(Locale.US, "%04d", cal.get(Calendar.YEAR)), null,
|
||||
String.format(Locale.US, "%02d", hour), null,
|
||||
String.format(Locale.US, "%02d", cal.get(Calendar.MINUTE)) };
|
||||
lamps = new boolean[] { hour < 12, hour >= 12 };
|
||||
}
|
||||
drawRow(c, left, right, top, u, colour, title, TIME, values, new String[] { "AM", "PM" }, lamps);
|
||||
}
|
||||
|
||||
private void drawRow(Canvas c, float left, float right, float top, float u, int colour, String title,
|
||||
Field[] fields, String[] values, String[] lampNames, boolean[] lampsOn) {
|
||||
float boxTop = top + 22f * u, boxH = 78f * u, gap = 14f * u, pad = 8f * u;
|
||||
float lampW = 74f * u, colonW = 16f * u;
|
||||
int cells = 0;
|
||||
float fixed = 0;
|
||||
for (Field f : fields) {
|
||||
cells += f.cells;
|
||||
if (f.kind == 'l') fixed += lampW;
|
||||
else if (f.kind == ':') fixed += colonW;
|
||||
else fixed += 2 * pad;
|
||||
}
|
||||
fixed += gap * (fields.length - 1);
|
||||
float cw = Math.min(boxH * 0.58f, (right - left - fixed) / cells);
|
||||
float total = cells * cw + fixed, x = left + (right - left - total) / 2f;
|
||||
for (int i = 0; i < fields.length; i++) {
|
||||
Field f = fields[i];
|
||||
if (f.kind == 'l') {
|
||||
drawLamps(c, x, boxTop, lampW, boxH, u, colour, lampNames, lampsOn);
|
||||
x += lampW + gap;
|
||||
continue;
|
||||
}
|
||||
if (f.kind == ':') {
|
||||
fill.setColor(colour);
|
||||
c.drawCircle(x + colonW / 2f, boxTop + boxH * 0.36f, 3.5f * u, fill);
|
||||
c.drawCircle(x + colonW / 2f, boxTop + boxH * 0.64f, 3.5f * u, fill);
|
||||
x += colonW + gap;
|
||||
continue;
|
||||
}
|
||||
float bw = f.cells * cw + 2 * pad;
|
||||
r.set(x, boxTop, x + bw, boxTop + boxH);
|
||||
fill.setColor(0xFF020202);
|
||||
c.drawRoundRect(r, 4f * u, 4f * u, fill);
|
||||
line.setColor(0xFF5C5D62);
|
||||
line.setStrokeWidth(2f * u);
|
||||
c.drawRoundRect(r, 4f * u, 4f * u, line);
|
||||
caption(c, f.caption, x + bw / 2f, top + 14f * u, u);
|
||||
drawText(c, values[i], f.kind == 'a', x + pad, boxTop + 8f * u, cw, boxH - 16f * u, f.cells, colour, u);
|
||||
x += bw + gap;
|
||||
}
|
||||
tape(c, title, (left + right) / 2f, boxTop + boxH + 34f * u, u, 0xFF0E0E0F);
|
||||
}
|
||||
|
||||
private void drawLamps(Canvas c, float x, float top, float w, float h, float u, int colour,
|
||||
String[] names, boolean[] on) {
|
||||
for (int k = 0; k < 2; k++) {
|
||||
float cy = top + h * (k == 0 ? 0.3f : 0.72f), cx = x + w - 16f * u;
|
||||
caption(c, names[k], x + 22f * u, cy + 5f * u, u);
|
||||
fill.setColor(on[k] ? AccentColor.withAlpha(colour, 0x55) : 0x00000000);
|
||||
c.drawCircle(cx, cy, 11f * u, fill);
|
||||
fill.setColor(on[k] ? colour : AccentColor.withAlpha(colour, 0x30));
|
||||
c.drawCircle(cx, cy, 6f * u, fill);
|
||||
}
|
||||
}
|
||||
|
||||
// ---------- Segments ----------
|
||||
|
||||
/** Right-aligned text in cells; a '.' joins the cell before it. */
|
||||
private void drawText(Canvas c, String s, boolean alpha, float x, float y, float cw, float ch,
|
||||
int cells, int colour, float u) {
|
||||
char[] chars = SevenSegment.cells(s == null ? "" : s, cells);
|
||||
boolean[] dots = SevenSegment.dots(s == null ? "" : s, cells);
|
||||
for (int i = 0; i < cells; i++) {
|
||||
int mask = alpha ? Segments14.mask(chars[i]) : from7(SevenSegment.mask(chars[i]));
|
||||
drawCell(c, x + i * cw, y, cw, ch, mask, alpha, colour, u);
|
||||
if (dots[i]) {
|
||||
fill.setColor(colour);
|
||||
c.drawCircle(x + (i + 1) * cw - cw * 0.06f, y + ch - cw * 0.1f, cw * 0.07f, fill);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Seven-segment bits as fourteen-segment ones (the middle bar is both halves). */
|
||||
private static int from7(int m) {
|
||||
int out = m & 63; // a–f share their bits
|
||||
if ((m & SevenSegment.G) != 0) out |= Segments14.G1 | Segments14.G2;
|
||||
return out;
|
||||
}
|
||||
|
||||
private void drawCell(Canvas c, float x, float y, float cw, float ch, int mask, boolean alpha, int colour, float u) {
|
||||
float m = cw * 0.16f, t = cw * 0.12f;
|
||||
float x0 = x + m, x1 = x + cw - m, xc = (x0 + x1) / 2f, y0 = y + t, y1 = y + ch - t, ym = (y0 + y1) / 2f;
|
||||
float[][] segs = {
|
||||
{ x0, y0, x1, y0, Segments14.A }, { x1, y0, x1, ym, Segments14.B }, { x1, ym, x1, y1, Segments14.C },
|
||||
{ x0, y1, x1, y1, Segments14.D }, { x0, ym, x0, y1, Segments14.E }, { x0, y0, x0, ym, Segments14.F },
|
||||
{ x0, ym, xc, ym, Segments14.G1 }, { xc, ym, x1, ym, Segments14.G2 },
|
||||
{ x0, y0, xc, ym, Segments14.H }, { xc, y0, xc, ym, Segments14.I }, { x1, y0, xc, ym, Segments14.J },
|
||||
{ x0, y1, xc, ym, Segments14.K }, { xc, ym, xc, y1, Segments14.L }, { x1, y1, xc, ym, Segments14.M },
|
||||
};
|
||||
for (float[] s : segs) {
|
||||
int bit = (int) s[4];
|
||||
if (!alpha && bit >= Segments14.H) continue; // digits have no inner segments
|
||||
boolean on = (mask & bit) != 0;
|
||||
float dx = s[2] - s[0], dy = s[3] - s[1], len = (float) Math.hypot(dx, dy);
|
||||
float g = Math.min(t * 0.8f, len * 0.3f), ux = dx / len * g, uy = dy / len * g;
|
||||
float ax = s[0] + ux, ay = s[1] + uy, bx = s[2] - ux, by = s[3] - uy;
|
||||
if (on) {
|
||||
line.setColor(AccentColor.withAlpha(colour, 0x38));
|
||||
line.setStrokeWidth(t * 2.4f);
|
||||
c.drawLine(ax, ay, bx, by, line);
|
||||
}
|
||||
line.setColor(on ? colour : AccentColor.withAlpha(colour, 0x1C));
|
||||
line.setStrokeWidth(bit >= Segments14.H ? t * 0.8f : t);
|
||||
c.drawLine(ax, ay, bx, by, line);
|
||||
}
|
||||
}
|
||||
|
||||
// ---------- Bottom read-outs ----------
|
||||
|
||||
private void drawBottom(Canvas c, float left, float right, float top, float u) {
|
||||
String[][] cells = {
|
||||
{ "TRIP KM", fmt(v.tripKm, "%.1f", "---"), "5" },
|
||||
{ "SINCE START", fmt(v.usageTrip, "%.1f", "---"), "4" },
|
||||
{ "CLIMATE °C", fmt(v.climateCelsius, "%.0f", "--"), "3" },
|
||||
{ "ODOMETER KM", fmt(v.odometerKm, "%.0f", "------"), "6" },
|
||||
};
|
||||
float gap = 18f * u, boxH = 62f * u, boxTop = top + 22f * u, pad = 8f * u;
|
||||
int total = 0;
|
||||
for (String[] cell : cells) total += Integer.parseInt(cell[2]);
|
||||
float cw = Math.min(boxH * 0.55f, (right - left - gap * (cells.length - 1) - cells.length * 2 * pad) / total);
|
||||
float width = total * cw + gap * (cells.length - 1) + cells.length * 2 * pad;
|
||||
float x = left + (right - left - width) / 2f;
|
||||
for (String[] cell : cells) {
|
||||
int n = Integer.parseInt(cell[2]);
|
||||
float bw = n * cw + 2 * pad;
|
||||
r.set(x, boxTop, x + bw, boxTop + boxH);
|
||||
fill.setColor(0xFF020202);
|
||||
c.drawRoundRect(r, 4f * u, 4f * u, fill);
|
||||
line.setColor(0xFF5C5D62);
|
||||
line.setStrokeWidth(2f * u);
|
||||
c.drawRoundRect(r, 4f * u, 4f * u, line);
|
||||
caption(c, cell[0], x + bw / 2f, top + 14f * u, u);
|
||||
drawText(c, cell[1], false, x + pad, boxTop + 7f * u, cw, boxH - 14f * u, n, AMBER, u);
|
||||
x += bw + gap;
|
||||
}
|
||||
}
|
||||
|
||||
// ---------- Speed and flux capacitor ----------
|
||||
|
||||
private void drawSpeed(Canvas c, float left, float right, float top, float u) {
|
||||
float boxH = 168f * u, cells = 3;
|
||||
float cw = Math.min(boxH * 0.56f, (right - left - 40f * u) / cells);
|
||||
float bw = cells * cw + 40f * u, x = (left + right - bw) / 2f;
|
||||
r.set(x, top, x + bw, top + boxH);
|
||||
fill.setColor(0xFF020202);
|
||||
c.drawRoundRect(r, 6f * u, 6f * u, fill);
|
||||
line.setColor(0xFF5C5D62);
|
||||
line.setStrokeWidth(2.5f * u);
|
||||
c.drawRoundRect(r, 6f * u, 6f * u, line);
|
||||
String speed = v.speedKmh == null ? "---" : String.format(Locale.US, "%.0f", Math.min(999f, v.speedKmh));
|
||||
// At 88 the digits burn white-hot, and flicker.
|
||||
boolean hot = at88() && (SystemClock.uptimeMillis() / 90) % 3 != 0;
|
||||
drawText(c, speed, false, x + 20f * u, top + 14f * u, cw, boxH - 28f * u, 3, hot ? WHITE_HOT : RED, u);
|
||||
tape(c, at88() ? "88 KM/H — HOLD ON" : "KM/H", (left + right) / 2f, top + boxH + 34f * u, u, 0xFF0E0E0F);
|
||||
}
|
||||
|
||||
/** Three glass tubes in a Y, light running to the centre with the power (outwards in regen). */
|
||||
private void drawFlux(Canvas c, float left, float right, float top, float bottom, float u) {
|
||||
float labelSpace = 70f * u, boxBottom = bottom - labelSpace;
|
||||
float size = Math.min(right - left, boxBottom - top);
|
||||
float cx = (left + right) / 2f, bx = cx - size / 2f;
|
||||
r.set(bx, top, bx + size, boxBottom);
|
||||
fill.setColor(0xFF0B0C0E);
|
||||
c.drawRoundRect(r, 8f * u, 8f * u, fill);
|
||||
line.setColor(0xFF6A6B70);
|
||||
line.setStrokeWidth(3f * u);
|
||||
c.drawRoundRect(r, 8f * u, 8f * u, line);
|
||||
float cy = top + (boxBottom - top) * 0.52f, inset = size * 0.14f;
|
||||
float[][] ends = { { bx + inset, top + inset }, { bx + size - inset, top + inset }, { cx, boxBottom - inset } };
|
||||
boolean flash = at88();
|
||||
boolean live = v.powerKw != null;
|
||||
float strength = live ? Math.min(1f, Math.abs(v.powerKw) / 60f) : 0f;
|
||||
for (float[] e : ends) {
|
||||
line.setColor(0xFF2A2C30);
|
||||
line.setStrokeWidth(16f * u);
|
||||
c.drawLine(e[0], e[1], cx, cy, line);
|
||||
line.setColor(flash ? WHITE_HOT : AccentColor.withAlpha(WHITE_HOT, live ? 0x40 + (int) (0x60 * strength) : 0x18));
|
||||
line.setStrokeWidth(5f * u);
|
||||
c.drawLine(e[0], e[1], cx, cy, line);
|
||||
if (live && Math.abs(v.powerKw) > 0.5f) {
|
||||
// Pulses along the tube: phase 0 at the end, 1 at the centre.
|
||||
for (int k = 0; k < 4; k++) {
|
||||
float p = (fluxPhase + k / 4f) % 1f;
|
||||
float px = e[0] + (cx - e[0]) * p, py = e[1] + (cy - e[1]) * p;
|
||||
fill.setColor(AccentColor.withAlpha(WHITE_HOT, 0x50));
|
||||
c.drawCircle(px, py, 12f * u, fill);
|
||||
fill.setColor(WHITE_HOT);
|
||||
c.drawCircle(px, py, 5f * u, fill);
|
||||
}
|
||||
}
|
||||
fill.setColor(0xFF6A6B70);
|
||||
c.drawCircle(e[0], e[1], 12f * u, fill);
|
||||
}
|
||||
fill.setColor(AccentColor.withAlpha(WHITE_HOT, flash ? 0xA0 : live ? 0x30 + (int) (0x70 * strength) : 0x14));
|
||||
c.drawCircle(cx, cy, 34f * u, fill);
|
||||
fill.setColor(flash || (live && strength > 0.05f) ? WHITE_HOT : 0xFF3A3B40);
|
||||
c.drawCircle(cx, cy, 13f * u, fill);
|
||||
|
||||
tape(c, "FLUX CAPACITOR", cx, boxBottom + 30f * u, u, 0xFF0E0E0F);
|
||||
String power = !live ? "— KILOWATTS"
|
||||
: v.powerKw < -0.5f ? String.format(Locale.US, "REGEN %.1f KILOWATTS", -v.powerKw)
|
||||
: String.format(Locale.US, "%.1f KILOWATTS", v.powerKw);
|
||||
tape(c, power, cx, boxBottom + 62f * u, u, 0xFF5A1410);
|
||||
}
|
||||
}
|
||||
@@ -136,6 +136,8 @@
|
||||
<string name="overview_style_terminal">Terminal</string>
|
||||
<string name="accent_original">Original</string>
|
||||
<string name="overview_style_grid">Grid</string>
|
||||
<string name="overview_style_time_circuits">Time circuits</string>
|
||||
<string name="overview_style_quest">Quest</string>
|
||||
<string name="overview_style_nixie">Nixie tubes</string>
|
||||
<string name="overview_style_chronograph">Chronograph</string>
|
||||
<string name="cockpit_battery">BATTERY</string>
|
||||
|
||||
@@ -64,4 +64,24 @@
|
||||
<item name="evAccentStroke">#80267A86</item>
|
||||
<item name="evAccentTrack">#1E6672</item>
|
||||
</style>
|
||||
<!-- Time circuits: the red of the destination row. -->
|
||||
<style name="ThemeOverlay.EV.Style.TimeCircuits" parent="">
|
||||
<item name="evAccent">#FF3A26</item>
|
||||
<item name="evOnAccent">#1F0603</item>
|
||||
<item name="evAccentLight">#FF9A8C</item>
|
||||
<item name="evAccentDeep">#3D0E08</item>
|
||||
<item name="evAccentSurface">#2A110D</item>
|
||||
<item name="evAccentStroke">#80773A30</item>
|
||||
<item name="evAccentTrack">#6A2C24</item>
|
||||
</style>
|
||||
<!-- Quest: the gold of the frames. -->
|
||||
<style name="ThemeOverlay.EV.Style.Quest" parent="">
|
||||
<item name="evAccent">#C9A24A</item>
|
||||
<item name="evOnAccent">#1A1408</item>
|
||||
<item name="evAccentLight">#FFD873</item>
|
||||
<item name="evAccentDeep">#3A2C10</item>
|
||||
<item name="evAccentSurface">#241C0E</item>
|
||||
<item name="evAccentStroke">#806E5628</item>
|
||||
<item name="evAccentTrack">#6A5426</item>
|
||||
</style>
|
||||
</resources>
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
package com.evsuite.abrp;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertNotEquals;
|
||||
|
||||
import org.junit.Test;
|
||||
|
||||
/** The time circuits' fourteen-segment letters — pure JVM. */
|
||||
public class Segments14Test {
|
||||
|
||||
@Test
|
||||
public void everyMonthAndGearWordLightsEveryLetter() {
|
||||
for (int m = 0; m < 12; m++) {
|
||||
for (char ch : Segments14.month(m).toCharArray()) assertNotEquals(ch + "", 0, Segments14.mask(ch));
|
||||
}
|
||||
for (int g = 1; g <= 4; g++) {
|
||||
for (char ch : Segments14.gearWord(g).toCharArray()) assertNotEquals(ch + "", 0, Segments14.mask(ch));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void unreadIsDashes() {
|
||||
assertEquals("---", Segments14.gearWord(null));
|
||||
assertEquals("---", Segments14.gearWord(9));
|
||||
assertEquals("---", Segments14.month(12));
|
||||
assertEquals(Segments14.G1 | Segments14.G2, Segments14.mask('-'));
|
||||
assertEquals(0, Segments14.mask(' '));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void wordsAsTheCircuitsShowThem() {
|
||||
assertEquals("OCT", Segments14.month(9));
|
||||
assertEquals("DRV", Segments14.gearWord(4));
|
||||
assertEquals("PRK", Segments14.gearWord(1));
|
||||
// The letters that need the diagonals do use them.
|
||||
assertNotEquals(0, Segments14.mask('N') & Segments14.H);
|
||||
assertNotEquals(0, Segments14.mask('V') & Segments14.K);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user