Compare commits
6
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ee7cf85255 | ||
|
|
1686b791bc | ||
|
|
12752c41e2 | ||
|
|
293464e297 | ||
|
|
adc2c86303 | ||
|
|
1f49cdc410 |
@@ -77,7 +77,7 @@ Repository: `ssh://git@git.stinnesbeck.com:2222/nils/mg4.git` (remote `origin`),
|
||||
|
||||
| Tab | Contents |
|
||||
|---|---|
|
||||
| Overview | value tiles (battery, range, range/usage/energy since start, 5-min averages of range, usage, power, speed), vehicle info (odometer, lease allowance and the odometer against it), tyre pressures; read every 0.2 s on a background thread. Styles (Settings → Overview style): Tiles, Gauges (`GaugeView` needles), Cockpit (`RingView` rings, `TraceView` power trace), Retro (`RetroView`, seven-segment VFD), Futuristic (`FlowView`, animated energy flow), Sci-fi (`SciFiView`, starship console), Cartoon (`CartoonView`), MG (`OctagonView`, octagonal dials after the classic MG dashboards), and on `ClusterCanvasView`: Minimal, Terminal, Nixie tubes, Chronograph, Grid (`GridView`, neon lines over a moving grid floor, power as a light-cycle trail from `ClusterValues.powerTrail`), Grid with map (the same `GridView` over the roads round the car, top-down: GPS while it is shown, tiles from the server's `/v1/map/roads` through `MapRoads`, cached in `files/map`; `RoadTile`, `MapGeometry`; the map is `MapLayerView`, a hardware layer drawn once and moved and turned by the GPU every display frame), Time circuits (`TimeCircuitsView`, the time machine's LED rows with `Segments14` letters and a flux capacitor), Quest (`QuestView`, a fantasy game interface), Visor (`VisorView`, a helmet display with a motion tracker of the power trail), Empire (`EmpireView`, a strategy game with an isometric world and ages by odometer, `EmpireAge`); the drawn styles share `ClusterValues`. A style shown in its own palette (Sci-fi, Cartoon, Nixie, Time circuits, Quest, Empire; Retro, Terminal, Grid and Visor with "Original") gives the whole app its colours (`StylePalette`, `res/values/style_palettes.xml`) instead of the accent colour. A button in the navigation's header hides the navigation for a full-width Overview (`PREF_OVERVIEW_FULLSCREEN`); a swipe in from the left edge (`EdgeSwipe`) brings it back; without the app's own full screen the car's sidebar takes that edge, so a tab at the left edge (`edge_tab`) is tapped or swiped instead. Pages change only by the navigation (the pager's own swiping is off) |
|
||||
| Overview | value tiles (battery, range, range/usage/energy since start, 5-min averages of range, usage, power, speed), vehicle info (odometer, lease allowance and the odometer against it), tyre pressures; read every 0.2 s on a background thread. Styles (Settings → Overview style): Tiles, Gauges (`GaugeView` needles), Cockpit (`RingView` rings, `TraceView` power trace), Retro (`RetroView`, seven-segment VFD), Futuristic (`FlowView`, animated energy flow), Sci-fi (`SciFiView`, starship console), Cartoon (`CartoonView`), MG (`OctagonView`, octagonal dials after the classic MG dashboards), and on `ClusterCanvasView`: Minimal, Terminal, Nixie tubes, Chronograph, Grid (`GridView`, neon lines over a moving grid floor, power as a light-cycle trail from `ClusterValues.powerTrail`), Grid with map (the same `GridView` over the roads round the car, top-down: GPS while it is shown, tiles from the server's `/v1/map/roads` through `MapRoads`, cached in `files/map`; `RoadTile`, `MapGeometry`; the map is `MapLayerView`, a hardware layer drawn once and moved and turned by the GPU every display frame), Time circuits (`TimeCircuitsView`, the time machine's LED rows with `Segments14` letters and a flux capacitor), Quest (`QuestView`, a fantasy game interface), Visor (`VisorView`, a helmet display with a motion tracker of the power trail), Empire (`EmpireView`, a strategy game with an isometric world and ages by odometer, `EmpireAge`), Día de Muertos (`MuertosView`, an ofrenda: papel picado, candles, a calavera in a ring of marigolds), Halloween (`HalloweenView`, a jack-o'-lantern, a potion flask, a tombstone on a graveyard hill), both counting with `Tally`; the drawn styles share `ClusterValues`. A style shown in its own palette (Sci-fi, Cartoon, Nixie, Time circuits, Quest, Empire, Día de Muertos, Halloween; Retro, Terminal, Grid and Visor with "Original") gives the whole app its colours (`StylePalette`, `res/values/style_palettes.xml`) instead of the accent colour. A button in the navigation's header hides the navigation for a full-width Overview (`PREF_OVERVIEW_FULLSCREEN`); a swipe in from the left edge (`EdgeSwipe`) brings it back; without the app's own full screen the car's sidebar takes that edge, so a tab at the left edge (`edge_tab`) is tapped or swiped instead. Pages change only by the navigation (the pager's own swiping is off) |
|
||||
| Consumption / Charging | period totals, daily log, charging sessions and curve |
|
||||
| Live data | on/off, ABRP API key and user token, upload frequency |
|
||||
| Server | URL, pairing by QR, ingest token, installation ID (read-only), config import |
|
||||
|
||||
@@ -39,7 +39,7 @@ APIs.
|
||||
|
||||
## Screenshots
|
||||
|
||||
**Overview styles:** the Overview comes in eighteen looks, from an 80s fluorescent display to a
|
||||
**Overview styles:** the Overview comes in twenty looks, from an 80s fluorescent display to a
|
||||
starship console, a watch face, a neon grid (also over a map of the roads round the car), a time machine, a fantasy game, a helmet display and a strategy game. [THEMES.md](THEMES.md) shows each one, with and without the
|
||||
navigation, and what inspired it.
|
||||
|
||||
|
||||
@@ -140,6 +140,26 @@ tyre pressures, GPS (lat, lon, elevation, heading) and the is_* flags. It does *
|
||||
confirmed car facts, app structure, UI conventions, build/test/ship, documents. It replaces
|
||||
upstream's EVABRPUploader text. The test emulator is now in the flake: `nix run .#car-emulator`
|
||||
(Android 9 AOSP at 1920×720, 160 dpi; the car build installs as the system user — checked).
|
||||
- 2026-10-31 — App 2.43.2: Halloween's teeth are triangles hanging from the grin's upper lip, each in the colour of the rib behind it (the right one was the darker orange on the lighter rib; owner: "the color on the right side is off"). Two screenshots redone. Tested: unit tests, lint, emulator with sample values (drawing in panel and full screen, regenerating in full screen) and with no data. Not tried on the car.
|
||||
- 2026-10-31 — App 2.43.1 published (`car-2.43.1`).
|
||||
- 2026-10-31 — App 2.43.1: Halloween's bats can be seen. They were near-black and small on the darkest part of the sky, and the owner did not spot them; now a moonlit purple-grey with a light edge, a third larger. Two screenshots redone. Tested: unit tests, lint, emulator with sample values (panel and full screen) and with no data (no bats). Not tried on the car.
|
||||
- 2026-10-31 — App 2.43.0 published (`car-2.43.0`).
|
||||
- 2026-10-31 — App 2.43.0: Overview styles **Día de Muertos** (`MuertosView`) and **Halloween**
|
||||
(`HalloweenView`; owner: "let's design a 'dia de muertos' and a 'halloween' style"), each with
|
||||
its own palette (the papel picado's pink, the pumpkin's orange). Día de Muertos, an ofrenda and
|
||||
kept a celebration: papel picado = battery (one flag per 10 %), calavera with the speed on its
|
||||
forehead, a ring of marigolds = power (turquoise and anticlockwise regenerating), candles =
|
||||
range (one per 50 km), a path of petals = the last two minutes of power. Halloween: a
|
||||
jack-o'-lantern with the speed in its grin, burning yellow or, regenerating, green; an arch of
|
||||
embers = power; a potion flask = battery; a tombstone and bats = range; the hill = the last two
|
||||
minutes of power. What a value lights is `Tally` (pure, unit-tested: an unread value lights
|
||||
nothing). The static scene is drawn once per size into a bitmap (about 4.5 MB while the view is
|
||||
attached), so a frame only draws the readings; flags, flames, bats, bubbles and the ghost move
|
||||
only while the car moves. Tests and lint pass. Emulator with sample values, panel and full
|
||||
screen: normal, 12 % with regeneration, 40 % (the flask's words over the potion's surface); real
|
||||
build with no data: "—" and nothing lit. No crash of the app. Not tried on the car, so the frame
|
||||
rate on the head unit is unmeasured.
|
||||
- 2026-10-28 — App 2.42.1 published (`car-2.42.1`).
|
||||
- 2026-10-28 — App 2.42.1: **the music-sources check no longer connects to other apps' media
|
||||
services.** In the car, 2.42.0's check (14:26, Bluetooth music playing) made the head unit ask
|
||||
whether to allow access to media (the owner agreed) and **stopped the Bluetooth music**: it
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Overview styles
|
||||
|
||||
The Overview can look eighteen ways: pick one under **Settings → Overview style**. Every style shows
|
||||
The Overview can look twenty ways: pick one under **Settings → Overview style**. Every style shows
|
||||
the same readings (battery, range, power, speed, gear, usage, trip, climate, odometer, time) and
|
||||
shows "—" for anything the car did not answer, never a made-up zero.
|
||||
|
||||
@@ -11,7 +11,7 @@ emulator has no car; on the car the values are live.
|
||||
|
||||
**Colours.** Most styles are drawn in the accent colour you pick from the MG4 paints (Settings →
|
||||
Accent colour; shown here in Dynamic Red). Retro, Terminal, Grid (with or without the map) and Visor can also show their original
|
||||
colours, and Sci-fi, Cartoon, Nixie tubes, Time circuits, Quest and Empire always do; while one of those is shown, the whole app
|
||||
colours, and Sci-fi, Cartoon, Nixie tubes, Time circuits, Quest, Empire, Día de Muertos and Halloween always do; while one of those is shown, the whole app
|
||||
(navigation, buttons, switches) takes on its colours too.
|
||||
|
||||
| Style | Look | Colours |
|
||||
@@ -34,6 +34,8 @@ colours, and Sci-fi, Cartoon, Nixie tubes, Time circuits, Quest and Empire alway
|
||||
| [Quest](#quest) | a fantasy game's interface | own |
|
||||
| [Visor](#visor) | a soldier's helmet display | original or accent |
|
||||
| [Empire](#empire) | a strategy game's interface | own |
|
||||
| [Día de Muertos](#día-de-muertos) | an ofrenda with papel picado and a calavera | own |
|
||||
| [Halloween](#halloween) | a jack-o'-lantern on a graveyard hill | own |
|
||||
|
||||
## Tiles
|
||||
|
||||
@@ -233,3 +235,34 @@ stats, and a diamond minimap with the odometer and the way to the next age. Belo
|
||||
|
||||

|
||||

|
||||
|
||||
## Día de Muertos
|
||||
|
||||
After the ofrenda, the altar families in Mexico build to welcome their dead back for a visit: a
|
||||
celebration, so bright and warm rather than frightening. The papel picado across the top is the
|
||||
battery, one cut-paper flag per 10 %, those not lit hanging dark, turning red with "BATTERY LOW"
|
||||
below 20 %. The calavera in the middle, a decorated sugar skull with marigolds (cempasúchil) for
|
||||
eyes, has the speed on its forehead; the ring of marigolds round it is the power, clockwise while
|
||||
drawing, turquoise and anticlockwise while regenerating. The altar's candles are the range, one
|
||||
per 50 km. The path of marigold petals along the bottom, which in the tradition leads the visitors
|
||||
home, is the last two minutes of power. On the right the clock in a tin frame, P R N D as small
|
||||
marigolds, and trip, usage, climate and odometer on the altar cloth. While you drive the flags
|
||||
sway and the flames flicker. The app turns the papel picado's pink with it.
|
||||
|
||||

|
||||

|
||||
|
||||
## Halloween
|
||||
|
||||
A graveyard hill under a full moon. The jack-o'-lantern has the speed cut into its grin, and its
|
||||
carving burns candle yellow while drawing and witch-fire green while regenerating; the arch of
|
||||
embers over it is the power, from the left while drawing and from the right while regenerating.
|
||||
The potion flask is the battery, red with "BATTERY LOW" blinking below 20 %. The tombstone reads
|
||||
the range ("here lies … km"), and the bats are the range too, one per 50 km; P R N D are small
|
||||
gravestones, the one engaged lit. The moon holds the clock, a sign hanging from a cobweb has trip,
|
||||
usage, climate and odometer, and the hill along the bottom is the last two minutes of power. While
|
||||
you drive the bats fly, the potion bubbles and the ghost floats (where there is room for it, as on
|
||||
this screen). The app turns the pumpkin's orange with it.
|
||||
|
||||

|
||||

|
||||
|
||||
+2
-2
@@ -34,8 +34,8 @@ android {
|
||||
applicationId "com.stinnesbeck.nilsmg4"
|
||||
minSdk 27
|
||||
targetSdk 36
|
||||
versionCode 152
|
||||
versionName "2.42.1"
|
||||
versionCode 155
|
||||
versionName "2.43.2"
|
||||
|
||||
// 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.
|
||||
|
||||
@@ -0,0 +1,549 @@
|
||||
package com.evsuite.abrp;
|
||||
|
||||
import android.content.Context;
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.LinearGradient;
|
||||
import android.graphics.Paint;
|
||||
import android.graphics.Path;
|
||||
import android.graphics.RadialGradient;
|
||||
import android.graphics.RectF;
|
||||
import android.graphics.Shader;
|
||||
import android.graphics.Typeface;
|
||||
import android.os.SystemClock;
|
||||
|
||||
import java.util.Locale;
|
||||
|
||||
/**
|
||||
* The Halloween Overview style: a graveyard hill under a full moon.
|
||||
*
|
||||
* In the middle a jack-o'-lantern with the speed cut into its grin; its carving burns candle yellow
|
||||
* while drawing and witch-fire green while regenerating, and the arch of embers over it is the
|
||||
* power, left to right while drawing, green and right to left while regenerating. On the left a
|
||||
* potion flask: the battery, red with "BATTERY LOW" blinking below 20 %, under a sign hanging from
|
||||
* a cobweb with trip, usage, climate and odometer. On the right the moon holds the clock, and a
|
||||
* tombstone reads the range ("here lies … km"); P R N D are four small gravestones, the one engaged
|
||||
* lit. The bats are the range too, one per 50 km. The hill along the bottom is the last two minutes
|
||||
* of power, rising with it, in orange or, where it regenerated, green. Unread values show "—" and
|
||||
* light nothing.
|
||||
*
|
||||
* The night sky is drawn once into a bitmap per size; only the rest goes over it. Bats, bubbles and
|
||||
* the ghost move (about 30 frames a second) only while the car moves and the view is on screen.
|
||||
*/
|
||||
final class HalloweenView extends ClusterCanvasView {
|
||||
|
||||
private static final int EMBERS = 25, BATS = 8;
|
||||
private static final int SKY_TOP = 0xFF0B0620, SKY_LOW = 0xFF2A1140, HILL = 0xFF0A0612;
|
||||
private static final int PUMPKIN = 0xFFF2760F, PUMPKIN_DARK = 0xFFB84A06, PUMPKIN_LIT = 0xFFFF9A2E;
|
||||
private static final int CANDLE = 0xFFFFD84A, FIRE = 0xFF6BFF5A, RED = 0xFFFF3B30, STONE = 0xFF6C6A78;
|
||||
private static final int STONE_DARK = 0xFF3E3C4A, MOON = 0xFFF4E9B8, MOON_SHADE = 0xFFD8C98C;
|
||||
private static final int POTION = 0xFF7BF03A, GLASS = 0xFFB9D8D0, BONE = 0xFFF2EAD8, ORANGE = 0xFFFF8C1A;
|
||||
private static final int STEM = 0xFF4E7A2B, WOOD = 0xFF2E1B10, INK = 0xFF140A08;
|
||||
private static final int BAT = 0xFF6F6390, BAT_EDGE = 0xFFD6CCEA;
|
||||
|
||||
private final Paint fill = new Paint(Paint.ANTI_ALIAS_FLAG);
|
||||
private final Paint line = new Paint(Paint.ANTI_ALIAS_FLAG);
|
||||
private final Paint text = new Paint(Paint.ANTI_ALIAS_FLAG);
|
||||
private final Paint image = new Paint(Paint.FILTER_BITMAP_FLAG);
|
||||
private final Paint glow = new Paint(Paint.ANTI_ALIAS_FLAG);
|
||||
private final Path path = new Path();
|
||||
private final RectF r = new RectF();
|
||||
private final Typeface serif = Typeface.create("serif", Typeface.BOLD);
|
||||
private final Typeface face = Typeface.create("sans-serif-condensed", Typeface.BOLD);
|
||||
private final Typeface thin = Typeface.create("sans-serif-condensed", Typeface.NORMAL);
|
||||
|
||||
private Bitmap sky;
|
||||
private int skyW, skyH;
|
||||
private float time;
|
||||
private long lastFrame;
|
||||
|
||||
HalloweenView(Context context) {
|
||||
super(context);
|
||||
line.setStyle(Paint.Style.STROKE);
|
||||
line.setStrokeCap(Paint.Cap.ROUND);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDetachedFromWindow() {
|
||||
super.onDetachedFromWindow();
|
||||
sky = null;
|
||||
skyW = 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDraw(Canvas c) {
|
||||
super.onDraw(c);
|
||||
int w = getWidth(), h = getHeight();
|
||||
if (w <= 0 || h <= 0) return;
|
||||
float u = Math.min(h / 640f, w / 1100f);
|
||||
boolean moving = v.speedKmh != null && v.speedKmh > 0.5f;
|
||||
if (moving) {
|
||||
long now = SystemClock.uptimeMillis();
|
||||
time += lastFrame == 0 ? 0f : Math.min(0.2f, (now - lastFrame) / 1000f);
|
||||
lastFrame = now;
|
||||
} else {
|
||||
lastFrame = 0;
|
||||
}
|
||||
if (sky == null || skyW != w || skyH != h) buildSky(w, h, u);
|
||||
c.drawBitmap(sky, 0, 0, image);
|
||||
drawBats(c, w, u);
|
||||
drawMoon(c, w, u);
|
||||
drawGhost(c, w, u);
|
||||
drawHill(c, w, h, u);
|
||||
drawEmbers(c, w, u);
|
||||
drawPumpkin(c, w, u);
|
||||
drawSign(c, u);
|
||||
drawFlask(c, u);
|
||||
drawTombstone(c, w, u);
|
||||
drawGears(c, w, u);
|
||||
keepAnimating(moving, 33);
|
||||
}
|
||||
|
||||
private void label(Canvas c, String s, float x, float y, float size, int colour, Typeface tf, Paint.Align a) {
|
||||
text.setTypeface(tf);
|
||||
text.setTextSize(size);
|
||||
text.setTextAlign(a);
|
||||
text.setColor(colour);
|
||||
c.drawText(s, x, y, text);
|
||||
}
|
||||
|
||||
private boolean regen() {
|
||||
return v.powerKw != null && v.powerKw < -0.05f;
|
||||
}
|
||||
|
||||
// ---------- Sky ----------
|
||||
|
||||
private void buildSky(int w, int h, float u) {
|
||||
sky = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888);
|
||||
skyW = w;
|
||||
skyH = h;
|
||||
Canvas c = new Canvas(sky);
|
||||
Paint p = new Paint();
|
||||
p.setShader(new LinearGradient(0, 0, 0, h, new int[] { SKY_TOP, SKY_LOW, 0xFF4A1A3A },
|
||||
new float[] { 0f, 0.7f, 1f }, Shader.TileMode.CLAMP));
|
||||
c.drawRect(0, 0, w, h, p);
|
||||
Paint star = new Paint(Paint.ANTI_ALIAS_FLAG);
|
||||
star.setColor(0x80FFFFFF);
|
||||
for (int i = 0; i < 90; i++) {
|
||||
float x = ((i * 191 + 17) % 1009) / 1009f * w, y = ((i * 83 + 5) % 421) * u;
|
||||
c.drawCircle(x, y, (i % 4 == 0 ? 1.7f : 0.9f) * u, star);
|
||||
}
|
||||
// Bare trees in the distance.
|
||||
tree(c, w * 0.235f, 590f * u, 150f * u, u);
|
||||
tree(c, w * 0.745f, 590f * u, 118f * u, u);
|
||||
}
|
||||
|
||||
/** A bare tree: a trunk and branches that fork twice. */
|
||||
private void tree(Canvas c, float x, float y, float tall, float u) {
|
||||
Paint p = new Paint(Paint.ANTI_ALIAS_FLAG);
|
||||
p.setStyle(Paint.Style.STROKE);
|
||||
p.setStrokeCap(Paint.Cap.ROUND);
|
||||
p.setColor(0xFF160B20);
|
||||
branch(c, p, x, y, -90f, tall * 0.5f, 7f * u, 4);
|
||||
}
|
||||
|
||||
private void branch(Canvas c, Paint p, float x, float y, float deg, float len, float width, int depth) {
|
||||
double a = Math.toRadians(deg);
|
||||
float nx = x + (float) Math.cos(a) * len, ny = y + (float) Math.sin(a) * len;
|
||||
p.setStrokeWidth(width);
|
||||
c.drawLine(x, y, nx, ny, p);
|
||||
if (depth == 0) return;
|
||||
branch(c, p, nx, ny, deg - 24f - depth * 3f, len * 0.68f, width * 0.65f, depth - 1);
|
||||
branch(c, p, nx, ny, deg + 28f - depth * 2f, len * 0.62f, width * 0.65f, depth - 1);
|
||||
}
|
||||
|
||||
// ---------- Moon and clock ----------
|
||||
|
||||
private void drawMoon(Canvas c, int w, float u) {
|
||||
float rad = 74f * u, cx = w - 100f * u - rad * 0.2f, cy = 96f * u;
|
||||
glow.setShader(new RadialGradient(cx, cy, rad * 2.2f, new int[] { 0x55F4E9B8, 0x00F4E9B8 }, null,
|
||||
Shader.TileMode.CLAMP));
|
||||
c.drawCircle(cx, cy, rad * 2.2f, glow);
|
||||
fill.setColor(MOON);
|
||||
c.drawCircle(cx, cy, rad, fill);
|
||||
fill.setColor(MOON_SHADE);
|
||||
c.drawCircle(cx - 42f * u, cy - 34f * u, 12f * u, fill);
|
||||
c.drawCircle(cx + 40f * u, cy + 38f * u, 16f * u, fill);
|
||||
c.drawCircle(cx + 46f * u, cy - 26f * u, 7f * u, fill);
|
||||
c.drawCircle(cx - 30f * u, cy + 44f * u, 8f * u, fill);
|
||||
label(c, v.clock.isEmpty() ? "—" : v.clock, cx, cy + 17f * u, 50f * u, INK, serif, Paint.Align.CENTER);
|
||||
}
|
||||
|
||||
// ---------- Bats: the range ----------
|
||||
|
||||
private void drawBats(Canvas c, int w, float u) {
|
||||
int lit = Tally.lit(v.rangeKm, 50f, BATS);
|
||||
float cx = w / 2f;
|
||||
for (int i = 0; i < lit; i++) {
|
||||
// Each its own lane high in the sky, clear of the pumpkin's middle.
|
||||
float lane = (40f + (i * 37) % 70) * u;
|
||||
float span = w * 0.92f, speed = (26f + i * 5f) * u;
|
||||
float x = ((i * 0.13f * span + time * speed) % span) + w * 0.04f;
|
||||
float y = lane + (float) Math.sin(time * 2f + i) * 8f * u;
|
||||
if (Math.abs(x - cx) < 60f * u) y -= 10f * u;
|
||||
bat(c, x, y, (i % 2 == 0 ? 20f : 16f) * u, (float) Math.sin(time * 14f + i * 2f), u);
|
||||
}
|
||||
}
|
||||
|
||||
private void bat(Canvas c, float x, float y, float s, float flap, float u) {
|
||||
float tip = -s * 0.5f * flap;
|
||||
path.reset();
|
||||
path.moveTo(x, y - s * 0.3f);
|
||||
path.quadTo(x + s * 0.6f, y - s * 0.7f + tip, x + s * 1.5f, y - s * 0.3f + tip);
|
||||
path.quadTo(x + s * 1.1f, y + s * 0.05f, x + s * 0.9f, y + s * 0.35f);
|
||||
path.quadTo(x + s * 0.6f, y + s * 0.05f, x + s * 0.35f, y + s * 0.4f);
|
||||
path.quadTo(x + s * 0.15f, y + s * 0.15f, x, y + s * 0.5f);
|
||||
path.quadTo(x - s * 0.15f, y + s * 0.15f, x - s * 0.35f, y + s * 0.4f);
|
||||
path.quadTo(x - s * 0.6f, y + s * 0.05f, x - s * 0.9f, y + s * 0.35f);
|
||||
path.quadTo(x - s * 1.1f, y + s * 0.05f, x - s * 1.5f, y - s * 0.3f + tip);
|
||||
path.quadTo(x - s * 0.6f, y - s * 0.7f + tip, x, y - s * 0.3f);
|
||||
path.close();
|
||||
// Lit by the moon: black would vanish in the night sky.
|
||||
fill.setColor(BAT);
|
||||
c.drawPath(path, fill);
|
||||
line.setColor(BAT_EDGE);
|
||||
line.setStrokeWidth(1.5f * u);
|
||||
c.drawPath(path, line);
|
||||
}
|
||||
|
||||
// ---------- Ghost: only where there is room ----------
|
||||
|
||||
private void drawGhost(Canvas c, int w, float u) {
|
||||
if (w / u < 1400f) return;
|
||||
float x = (w / 2f + 272f * u + w - 290f * u) / 2f, y = 330f * u + (float) Math.sin(time * 1.4f) * 12f * u, s = 34f * u;
|
||||
path.reset();
|
||||
path.moveTo(x - s, y + s * 1.5f);
|
||||
path.lineTo(x - s, y);
|
||||
path.cubicTo(x - s, y - s * 1.4f, x + s, y - s * 1.4f, x + s, y);
|
||||
path.lineTo(x + s, y + s * 1.5f);
|
||||
float wob = (float) Math.sin(time * 3f) * s * 0.12f;
|
||||
path.quadTo(x + s * 0.66f, y + s * 1.1f + wob, x + s * 0.33f, y + s * 1.5f);
|
||||
path.quadTo(x, y + s * 1.1f - wob, x - s * 0.33f, y + s * 1.5f);
|
||||
path.quadTo(x - s * 0.66f, y + s * 1.1f + wob, x - s, y + s * 1.5f);
|
||||
path.close();
|
||||
fill.setColor(0xB8F0F4FF);
|
||||
c.drawPath(path, fill);
|
||||
fill.setColor(0xFF1A1030);
|
||||
c.drawCircle(x - s * 0.34f, y - s * 0.2f, s * 0.14f, fill);
|
||||
c.drawCircle(x + s * 0.34f, y - s * 0.2f, s * 0.14f, fill);
|
||||
r.set(x - s * 0.14f, y + s * 0.08f, x + s * 0.14f, y + s * 0.44f);
|
||||
c.drawOval(r, fill);
|
||||
}
|
||||
|
||||
// ---------- The hill: the last two minutes of power ----------
|
||||
|
||||
private void drawHill(Canvas c, int w, int h, float u) {
|
||||
float base = h - 44f * u, amp = 36f * u;
|
||||
float[] trail = v.powerTrail;
|
||||
path.reset();
|
||||
path.moveTo(0, h);
|
||||
int n = trail == null ? 0 : trail.length;
|
||||
if (n == 0) {
|
||||
path.lineTo(0, base);
|
||||
path.lineTo(w, base);
|
||||
} else {
|
||||
for (int i = 0; i < n; i++) {
|
||||
float p = trail[i];
|
||||
float height = Float.isNaN(p) ? 0f : Math.min(1f, Math.abs(p) / (p < 0 ? 40f : 60f));
|
||||
path.lineTo(i / (n - 1f) * w, base - height * amp);
|
||||
}
|
||||
}
|
||||
path.lineTo(w, h);
|
||||
path.close();
|
||||
fill.setColor(HILL);
|
||||
c.drawPath(path, fill);
|
||||
// The ridge line, each stretch in the colour of what the car was doing then.
|
||||
if (n > 1) {
|
||||
line.setStrokeWidth(3f * u);
|
||||
for (int i = 1; i < n; i++) {
|
||||
float a = trail[i - 1], b = trail[i];
|
||||
if (Float.isNaN(a) || Float.isNaN(b)) continue;
|
||||
float ha = Math.min(1f, Math.abs(a) / (a < 0 ? 40f : 60f));
|
||||
float hb = Math.min(1f, Math.abs(b) / (b < 0 ? 40f : 60f));
|
||||
line.setColor(b < 0 ? FIRE : ORANGE);
|
||||
c.drawLine((i - 1f) / (n - 1f) * w, base - ha * amp, i / (n - 1f) * w, base - hb * amp, line);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ---------- The arch of embers: the power ----------
|
||||
|
||||
private void drawEmbers(Canvas c, int w, float u) {
|
||||
float cx = w / 2f, cy = 348f * u, radX = 272f * u, radY = 244f * u;
|
||||
int lit = Tally.ofPower(v.powerKw, 150f, 50f, EMBERS);
|
||||
boolean regen = v.powerKw != null && v.powerKw < 0f;
|
||||
for (int i = 0; i < EMBERS; i++) {
|
||||
// Left to right over the top; regeneration comes back from the right.
|
||||
int k = regen ? EMBERS - 1 - i : i;
|
||||
double a = Math.PI + Math.PI * k / (EMBERS - 1);
|
||||
float x = cx + (float) Math.cos(a) * radX, y = cy + (float) Math.sin(a) * radY;
|
||||
boolean on = i < lit;
|
||||
if (on) {
|
||||
fill.setColor(AccentColor.withAlpha(regen ? FIRE : ORANGE, 0x44));
|
||||
c.drawCircle(x, y, 15f * u, fill);
|
||||
}
|
||||
fill.setColor(on ? (regen ? FIRE : CANDLE) : 0x38FFFFFF);
|
||||
c.drawCircle(x, y, 7.5f * u, fill);
|
||||
}
|
||||
}
|
||||
|
||||
// ---------- The jack-o'-lantern: the speed ----------
|
||||
|
||||
private void drawPumpkin(Canvas c, int w, float u) {
|
||||
float cx = w / 2f, cy = 348f * u, rx = 200f * u, ry = 158f * u;
|
||||
boolean regen = regen();
|
||||
int burn = regen ? FIRE : CANDLE;
|
||||
// The light it throws, stronger the more power.
|
||||
float share = v.powerKw == null ? 0f : Math.min(1f, Math.abs(v.powerKw) / 80f);
|
||||
glow.setShader(new RadialGradient(cx, cy, rx * 1.6f,
|
||||
new int[] { AccentColor.withAlpha(regen ? FIRE : ORANGE, 0x30 + (int) (share * 0x50)),
|
||||
AccentColor.withAlpha(regen ? FIRE : ORANGE, 0) }, null, Shader.TileMode.CLAMP));
|
||||
c.drawCircle(cx, cy, rx * 1.6f, glow);
|
||||
// The stem.
|
||||
path.reset();
|
||||
path.moveTo(cx - 22f * u, cy - ry + 12f * u);
|
||||
path.quadTo(cx - 14f * u, cy - ry - 26f * u, cx + 14f * u, cy - ry - 44f * u);
|
||||
path.lineTo(cx + 34f * u, cy - ry - 30f * u);
|
||||
path.quadTo(cx + 14f * u, cy - ry - 12f * u, cx + 22f * u, cy - ry + 12f * u);
|
||||
path.close();
|
||||
fill.setColor(STEM);
|
||||
c.drawPath(path, fill);
|
||||
// The ribs, outermost first, overlapping.
|
||||
float[][] ribs = { { -0.70f, 0.42f }, { 0.70f, 0.42f }, { -0.40f, 0.48f }, { 0.40f, 0.48f }, { 0f, 0.50f } };
|
||||
for (int i = 0; i < ribs.length; i++) {
|
||||
float rcx = cx + ribs[i][0] * rx, half = ribs[i][1] * rx;
|
||||
r.set(rcx - half, cy - ry * (i < 2 ? 0.86f : 0.98f), rcx + half, cy + ry * (i < 2 ? 0.86f : 0.98f));
|
||||
fill.setColor(i % 2 == 0 ? PUMPKIN : PUMPKIN_LIT);
|
||||
if (i == ribs.length - 1) fill.setColor(PUMPKIN);
|
||||
c.drawOval(r, fill);
|
||||
line.setColor(PUMPKIN_DARK);
|
||||
line.setStrokeWidth(2.5f * u);
|
||||
c.drawOval(r, line);
|
||||
}
|
||||
// Eyes and nose: triangles, burning.
|
||||
triangle(c, cx - 88f * u, cy - 92f * u, cx - 34f * u, cy - 28f * u, cx - 128f * u, cy - 28f * u, burn, u);
|
||||
triangle(c, cx + 88f * u, cy - 92f * u, cx + 128f * u, cy - 28f * u, cx + 34f * u, cy - 28f * u, burn, u);
|
||||
triangle(c, cx, cy - 28f * u, cx + 19f * u, cy + 4f * u, cx - 19f * u, cy + 4f * u, burn, u);
|
||||
// The grin, with the speed cut dark into its light.
|
||||
float gt = cy + 8f * u, gb = cy + 122f * u, gw = 156f * u;
|
||||
path.reset();
|
||||
path.moveTo(cx - gw, gt);
|
||||
path.quadTo(cx, gt + 26f * u, cx + gw, gt);
|
||||
path.quadTo(cx + gw * 0.82f, gb, cx, gb);
|
||||
path.quadTo(cx - gw * 0.82f, gb, cx - gw, gt);
|
||||
path.close();
|
||||
fill.setColor(0xFF4A1F04);
|
||||
c.drawPath(path, fill);
|
||||
c.save();
|
||||
c.scale(0.965f, 0.93f, cx, (gt + gb) / 2f);
|
||||
fill.setColor(burn);
|
||||
c.drawPath(path, fill);
|
||||
c.restore();
|
||||
// Two teeth at the grin's corners, clear of the digits, each the colour of the rib it hangs from.
|
||||
tooth(c, cx, -120f * u, gw, gt, 26f * u, 17f * u, gt + 40f * u, PUMPKIN, u);
|
||||
tooth(c, cx, 120f * u, gw, gt, 26f * u, 17f * u, gt + 40f * u, PUMPKIN_LIT, u);
|
||||
label(c, fmt(v.speedKmh, "%.0f", "—"), cx, gb - 18f * u, 92f * u, INK, serif, Paint.Align.CENTER);
|
||||
label(c, "KM/H", cx + 80f * u, cy + 64f * u, 16f * u, INK, face, Paint.Align.LEFT);
|
||||
// The power under the pumpkin, above the ridge.
|
||||
String power = v.powerKw == null ? "— kW" : String.format(Locale.US, regen ? "+%.1f kW" : "%.1f kW",
|
||||
Math.abs(v.powerKw));
|
||||
label(c, power, cx, cy + ry + 44f * u, 34f * u, regen ? FIRE : BONE, face, Paint.Align.CENTER);
|
||||
}
|
||||
|
||||
private void triangle(Canvas c, float x1, float y1, float x2, float y2, float x3, float y3, int burn, float u) {
|
||||
path.reset();
|
||||
path.moveTo(x1, y1);
|
||||
path.lineTo(x2, y2);
|
||||
path.lineTo(x3, y3);
|
||||
path.close();
|
||||
fill.setColor(burn);
|
||||
c.drawPath(path, fill);
|
||||
line.setColor(0xFF4A1F04);
|
||||
line.setStrokeWidth(4f * u);
|
||||
c.drawPath(path, line);
|
||||
}
|
||||
|
||||
/** A triangle hanging from the grin's upper lip (a curve sagging by {@code sag}) to {@code tipY}. */
|
||||
private void tooth(Canvas c, float cx, float dx, float gw, float gt, float sag, float halfW, float tipY, int colour, float u) {
|
||||
float left = lip(dx - halfW, gw, gt, sag), right = lip(dx + halfW, gw, gt, sag);
|
||||
path.reset();
|
||||
// A little above the lip, so none of its dark edge shows across the tooth's root.
|
||||
path.moveTo(cx + dx - halfW, left - 1.5f * u);
|
||||
path.lineTo(cx + dx + halfW, right - 1.5f * u);
|
||||
path.lineTo(cx + dx, tipY);
|
||||
path.close();
|
||||
fill.setColor(colour);
|
||||
c.drawPath(path, fill);
|
||||
line.setColor(0xFF4A1F04);
|
||||
line.setStrokeWidth(3f * u);
|
||||
c.drawLine(cx + dx - halfW, left + 2f * u, cx + dx, tipY, line);
|
||||
c.drawLine(cx + dx + halfW, right + 2f * u, cx + dx, tipY, line);
|
||||
}
|
||||
|
||||
/** The height of the grin's upper lip {@code dx} from the middle. */
|
||||
private static float lip(float dx, float gw, float gt, float sag) {
|
||||
float t = (gw - Math.abs(dx)) / (2f * gw);
|
||||
return gt + 2f * t * (1f - t) * sag;
|
||||
}
|
||||
|
||||
// ---------- Left: the sign under the cobweb ----------
|
||||
|
||||
private void drawSign(Canvas c, float u) {
|
||||
// The cobweb in the corner.
|
||||
float cs = 150f * u;
|
||||
line.setColor(0x70E8E8F4);
|
||||
line.setStrokeWidth(1.2f * u);
|
||||
for (int i = 0; i <= 5; i++) {
|
||||
double a = Math.PI / 2 * i / 5;
|
||||
c.drawLine(0, 0, (float) Math.cos(a) * cs, (float) Math.sin(a) * cs, line);
|
||||
}
|
||||
for (int ring = 1; ring <= 4; ring++) {
|
||||
float d = cs * ring / 4f;
|
||||
path.reset();
|
||||
for (int i = 0; i <= 5; i++) {
|
||||
double a = Math.PI / 2 * i / 5;
|
||||
float x = (float) Math.cos(a) * d, y = (float) Math.sin(a) * d;
|
||||
if (i == 0) {
|
||||
path.moveTo(x, y);
|
||||
} else {
|
||||
double mid = Math.PI / 2 * (i - 0.5) / 5;
|
||||
path.quadTo((float) Math.cos(mid) * d * 0.86f, (float) Math.sin(mid) * d * 0.86f, x, y);
|
||||
}
|
||||
}
|
||||
c.drawPath(path, line);
|
||||
}
|
||||
// The sign, on two threads.
|
||||
float l = 28f * u, rr = l + 300f * u, t = 50f * u, b = 244f * u;
|
||||
line.setColor(0xA0E8E8F4);
|
||||
c.drawLine(l + 60f * u, 0, l + 60f * u, t, line);
|
||||
c.drawLine(rr - 60f * u, 0, rr - 60f * u, t, line);
|
||||
r.set(l, t, rr, b);
|
||||
fill.setColor(WOOD);
|
||||
c.drawRoundRect(r, 8f * u, 8f * u, fill);
|
||||
line.setColor(0xFF6B4226);
|
||||
line.setStrokeWidth(2.5f * u);
|
||||
c.drawRoundRect(r, 8f * u, 8f * u, line);
|
||||
String[][] rows = {
|
||||
{ "TRIP", fmt(v.tripKm, "%.1f km", "—") },
|
||||
{ "5 MIN", fmt(v.usage5Min, "%.1f kWh/100", "—") },
|
||||
{ "SINCE START", fmt(v.usageTrip, "%.1f kWh/100", "—") },
|
||||
{ "CLIMATE", fmt(v.climateCelsius, "%.0f °C", "—") },
|
||||
{ "ODOMETER", fmt(v.odometerKm, "%,.0f km", "—") },
|
||||
};
|
||||
for (int i = 0; i < rows.length; i++) {
|
||||
float y = t + (36f + i * 34f) * u;
|
||||
label(c, rows[i][0], l + 16f * u, y, 19f * u, ORANGE, thin, Paint.Align.LEFT);
|
||||
label(c, rows[i][1], rr - 16f * u, y, 23f * u, BONE, face, Paint.Align.RIGHT);
|
||||
}
|
||||
}
|
||||
|
||||
// ---------- The potion flask: the battery ----------
|
||||
|
||||
private void drawFlask(Canvas c, float u) {
|
||||
float rad = 104f * u, cx = 28f * u + 150f * u, cy = 452f * u;
|
||||
boolean low = v.socPercent != null && v.socPercent < 20f;
|
||||
boolean blinkOn = (SystemClock.uptimeMillis() / 500) % 2 == 0;
|
||||
int colour = low ? RED : POTION;
|
||||
float neckW = 34f * u, neckTop = cy - rad - 42f * u;
|
||||
// The glass shape: a round belly and a neck.
|
||||
path.reset();
|
||||
path.addCircle(cx, cy, rad, Path.Direction.CW);
|
||||
r.set(cx - neckW, neckTop, cx + neckW, cy - rad + 16f * u);
|
||||
path.addRect(r, Path.Direction.CW);
|
||||
fill.setColor(0x38B9D8D0);
|
||||
c.drawPath(path, fill);
|
||||
// The potion: as high as the battery is full, across the belly only.
|
||||
if (v.socPercent != null) {
|
||||
float share = Math.max(0f, Math.min(1f, v.socPercent / 100f));
|
||||
float level = cy + rad - share * 2f * rad;
|
||||
c.save();
|
||||
c.clipRect(cx - rad, level, cx + rad, cy + rad + 1f);
|
||||
fill.setColor(colour);
|
||||
c.drawCircle(cx, cy, rad - 5f * u, fill);
|
||||
c.restore();
|
||||
// The surface, a brighter line.
|
||||
float half = (float) Math.sqrt(Math.max(0f, (rad - 5f * u) * (rad - 5f * u) - (level - cy) * (level - cy)));
|
||||
line.setColor(0xCCFFFFFF);
|
||||
line.setStrokeWidth(2.5f * u);
|
||||
if (half > 0f) c.drawLine(cx - half, level, cx + half, level, line);
|
||||
// Bubbles rise only while driving.
|
||||
fill.setColor(0x99FFFFFF);
|
||||
for (int i = 0; i < 6; i++) {
|
||||
float span = Math.max(1f, cy + rad - level - 12f * u);
|
||||
float y = cy + rad - 12f * u - ((time * (24f + i * 7f) * u + i * 31f * u) % span);
|
||||
float x = cx + (float) Math.sin(i * 2.1f + time * 1.5f) * rad * 0.5f;
|
||||
c.drawCircle(x, y, (3f + i % 3) * u, fill);
|
||||
}
|
||||
}
|
||||
// The glass, and its cork.
|
||||
line.setColor(GLASS);
|
||||
line.setStrokeWidth(4f * u);
|
||||
c.drawCircle(cx, cy, rad, line);
|
||||
c.drawLine(cx - neckW, neckTop + 12f * u, cx - neckW, cy - rad + 14f * u, line);
|
||||
c.drawLine(cx + neckW, neckTop + 12f * u, cx + neckW, cy - rad + 14f * u, line);
|
||||
r.set(cx - neckW - 4f * u, neckTop - 8f * u, cx + neckW + 4f * u, neckTop + 16f * u);
|
||||
fill.setColor(0xFF9B6B3A);
|
||||
c.drawRoundRect(r, 5f * u, 5f * u, fill);
|
||||
// Dark where the potion is behind the words; outlined where its surface cuts through them.
|
||||
float level = v.socPercent == null ? cy + rad
|
||||
: cy + rad - Math.max(0f, Math.min(1f, v.socPercent / 100f)) * 2f * rad;
|
||||
boolean numberOn = level <= cy - 18f * u, wordOn = level <= cy + 38f * u;
|
||||
String number = fmt(v.socPercent, "%.0f %%", "—");
|
||||
if (!numberOn && level < cy + 30f * u) {
|
||||
text.setStyle(Paint.Style.STROKE);
|
||||
text.setStrokeWidth(7f * u);
|
||||
label(c, number, cx, cy + 22f * u, 62f * u, INK, serif, Paint.Align.CENTER);
|
||||
text.setStyle(Paint.Style.FILL);
|
||||
}
|
||||
label(c, number, cx, cy + 22f * u, 62f * u, numberOn ? INK : low ? RED : BONE, serif, Paint.Align.CENTER);
|
||||
if (low && blinkOn) {
|
||||
label(c, "BATTERY LOW", cx, cy + 56f * u, 21f * u, BONE, face, Paint.Align.CENTER);
|
||||
} else if (!low) {
|
||||
label(c, "BATTERY", cx, cy + 56f * u, 19f * u, wordOn ? INK : POTION, face, Paint.Align.CENTER);
|
||||
}
|
||||
}
|
||||
|
||||
// ---------- The tombstone: the range; small gravestones: the gears ----------
|
||||
|
||||
private void stone(Canvas c, float l, float t, float rr, float b, int colour, int edge, float u) {
|
||||
float mid = (l + rr) / 2f, half = (rr - l) / 2f;
|
||||
path.reset();
|
||||
path.moveTo(l, b);
|
||||
path.lineTo(l, t + half);
|
||||
path.cubicTo(l, t + half * 0.1f, mid - half * 0.5f, t, mid, t);
|
||||
path.cubicTo(mid + half * 0.5f, t, rr, t + half * 0.1f, rr, t + half);
|
||||
path.lineTo(rr, b);
|
||||
path.close();
|
||||
fill.setColor(colour);
|
||||
c.drawPath(path, fill);
|
||||
line.setColor(edge);
|
||||
line.setStrokeWidth(3f * u);
|
||||
c.drawPath(path, line);
|
||||
}
|
||||
|
||||
private void drawTombstone(Canvas c, int w, float u) {
|
||||
float rr = w - 40f * u, l = rr - 250f * u, t = 196f * u, b = 484f * u, mid = (l + rr) / 2f;
|
||||
stone(c, l, t, rr, b, STONE, STONE_DARK, u);
|
||||
label(c, "R.I.P.", mid, t + 70f * u, 36f * u, STONE_DARK, serif, Paint.Align.CENTER);
|
||||
label(c, "HERE LIES", mid, t + 106f * u, 19f * u, 0xFF242230, face, Paint.Align.CENTER);
|
||||
label(c, fmt(v.rangeKm, "%.0f", "—"), mid, t + 186f * u, 86f * u, BONE, serif, Paint.Align.CENTER);
|
||||
label(c, "KM OF RANGE", mid, t + 224f * u, 21f * u, 0xFF242230, face, Paint.Align.CENTER);
|
||||
}
|
||||
|
||||
private void drawGears(Canvas c, int w, float u) {
|
||||
float rr = w - 40f * u, l = rr - 250f * u, t = 496f * u, b = 566f * u;
|
||||
String[] gears = { "P", "R", "N", "D" };
|
||||
String now = Gear.label(v.gear);
|
||||
float step = (rr - l) / 4f, half = 24f * u;
|
||||
for (int i = 0; i < gears.length; i++) {
|
||||
float mid = l + (i + 0.5f) * step;
|
||||
boolean on = gears[i].equals(now);
|
||||
if (on) {
|
||||
fill.setColor(0x55FF8C1A);
|
||||
c.drawCircle(mid, (t + b) / 2f, 42f * u, fill);
|
||||
}
|
||||
stone(c, mid - half, t, mid + half, b, on ? ORANGE : STONE_DARK, on ? CANDLE : 0xFF242230, u);
|
||||
label(c, gears[i], mid, b - 18f * u, 32f * u, on ? INK : 0xB0FFFFFF, serif, Paint.Align.CENTER);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -355,7 +355,7 @@ public class MainActivity extends AppCompatActivity {
|
||||
visorView = new VisorView(this);
|
||||
java.util.Collections.addAll(canvasStyles, new MinimalView(this), terminalView,
|
||||
new NixieView(this), new ChronographView(this), gridView, gridMapView, new TimeCircuitsView(this),
|
||||
new QuestView(this), visorView, new EmpireView(this));
|
||||
new QuestView(this), visorView, new EmpireView(this), new MuertosView(this), new HalloweenView(this));
|
||||
View[] canvasPanes = new View[canvasStyles.size()];
|
||||
for (int i = 0; i < canvasPanes.length; i++) canvasPanes[i] = drawnPane(canvasStyles.get(i));
|
||||
// Grid with map: its map layer goes under it, in the same pane.
|
||||
@@ -1823,7 +1823,8 @@ public class MainActivity extends AppCompatActivity {
|
||||
getString(R.string.overview_style_chronograph), getString(R.string.overview_style_grid),
|
||||
getString(R.string.overview_style_grid_map),
|
||||
getString(R.string.overview_style_time_circuits), getString(R.string.overview_style_quest),
|
||||
getString(R.string.overview_style_visor), getString(R.string.overview_style_empire) };
|
||||
getString(R.string.overview_style_visor), getString(R.string.overview_style_empire),
|
||||
getString(R.string.overview_style_muertos), getString(R.string.overview_style_halloween) };
|
||||
}
|
||||
|
||||
/** The style drop-down; every style is fed on every reading, only the chosen one is shown. */
|
||||
|
||||
@@ -0,0 +1,501 @@
|
||||
package com.evsuite.abrp;
|
||||
|
||||
import android.content.Context;
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.Paint;
|
||||
import android.graphics.Path;
|
||||
import android.graphics.RadialGradient;
|
||||
import android.graphics.RectF;
|
||||
import android.graphics.Shader;
|
||||
import android.graphics.Typeface;
|
||||
import android.os.SystemClock;
|
||||
|
||||
import java.util.Locale;
|
||||
|
||||
/**
|
||||
* The Día de Muertos Overview style: an ofrenda at night, the altar families build to welcome
|
||||
* their dead back for a visit. A celebration, so bright and warm, not frightening.
|
||||
*
|
||||
* Across the top the papel picado, cut-paper flags on a string: the battery, one flag per 10 %,
|
||||
* those not lit hanging dark. In the middle a calavera, a decorated sugar skull, with the speed
|
||||
* on its forehead and marigolds (cempasúchil) for eyes; round it a ring of marigolds is the power,
|
||||
* clockwise while drawing, turquoise and anticlockwise while regenerating. On the left the candles
|
||||
* of the altar: the range, one candle per 50 km. Along the bottom the path of marigold petals that
|
||||
* leads the visitors home: the last two minutes of power, higher the more power. On the right the
|
||||
* clock in a tin frame, P R N D as four small marigolds, and trip, usage, climate and odometer on
|
||||
* the altar cloth. Below 20 % the battery turns red and says so. Unread values show "—" and light
|
||||
* nothing.
|
||||
*
|
||||
* The altar is drawn once into a bitmap per size; only the readings go over it. The flags sway and
|
||||
* the flames flicker (about 30 frames a second) only while the car moves and the view is on screen.
|
||||
*/
|
||||
final class MuertosView extends ClusterCanvasView {
|
||||
|
||||
private static final int FLAGS = 10, PETALS = 24, CANDLES = 8;
|
||||
private static final int NIGHT = 0xFF160A26, PLUM = 0xFF2C1242, CLOTH = 0xFF3A0F3E, BONE = 0xFFF8EEDC;
|
||||
private static final int INK = 0xFF2A0E36, MARIGOLD = 0xFFFF9A1F, GOLD = 0xFFFFC94A, TURQUOISE = 0xFF2EE6D0;
|
||||
private static final int PINK = 0xFFFF2E93, RED = 0xFFFF3B47, FLAME = 0xFFFFD36B, WAX = 0xFFF4E6C8;
|
||||
private static final int[] PAPER = { 0xFFFF2E93, 0xFFFF9A1F, 0xFF2EE6D0, 0xFFFFD23F, 0xFF9B5CFF };
|
||||
|
||||
private final Paint fill = new Paint(Paint.ANTI_ALIAS_FLAG);
|
||||
private final Paint line = new Paint(Paint.ANTI_ALIAS_FLAG);
|
||||
private final Paint text = new Paint(Paint.ANTI_ALIAS_FLAG);
|
||||
private final Paint image = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.FILTER_BITMAP_FLAG);
|
||||
private final Path path = new Path();
|
||||
private final Path flag = new Path();
|
||||
private final RectF r = new RectF();
|
||||
private final Typeface serif = Typeface.create("serif", Typeface.BOLD);
|
||||
private final Typeface face = Typeface.create("sans-serif-condensed", Typeface.BOLD);
|
||||
private final Typeface thin = Typeface.create("sans-serif-condensed", Typeface.NORMAL);
|
||||
|
||||
private Bitmap scene, marigold, turquoise;
|
||||
private int sceneW, sceneH;
|
||||
private float time;
|
||||
private long lastFrame;
|
||||
|
||||
MuertosView(Context context) {
|
||||
super(context);
|
||||
line.setStyle(Paint.Style.STROKE);
|
||||
line.setStrokeCap(Paint.Cap.ROUND);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDetachedFromWindow() {
|
||||
super.onDetachedFromWindow();
|
||||
scene = null;
|
||||
marigold = null;
|
||||
turquoise = null;
|
||||
sceneW = 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDraw(Canvas c) {
|
||||
super.onDraw(c);
|
||||
int w = getWidth(), h = getHeight();
|
||||
if (w <= 0 || h <= 0) return;
|
||||
float u = Math.min(h / 640f, w / 1100f);
|
||||
boolean moving = v.speedKmh != null && v.speedKmh > 0.5f;
|
||||
if (moving) {
|
||||
long now = SystemClock.uptimeMillis();
|
||||
time += lastFrame == 0 ? 0f : Math.min(0.2f, (now - lastFrame) / 1000f);
|
||||
lastFrame = now;
|
||||
} else {
|
||||
lastFrame = 0;
|
||||
}
|
||||
if (scene == null || sceneW != w || sceneH != h) buildScene(w, h, u);
|
||||
c.drawBitmap(scene, 0, 0, image);
|
||||
float cx = w / 2f, cy = 345f * u;
|
||||
drawFlags(c, w, u);
|
||||
drawRing(c, cx, cy, u);
|
||||
drawSpeed(c, cx, cy, u);
|
||||
drawBattery(c, u);
|
||||
drawCandles(c, u, moving);
|
||||
drawPetals(c, w, h, u);
|
||||
drawClock(c, w, u);
|
||||
drawGears(c, w, u);
|
||||
drawRows(c, w, h, u);
|
||||
keepAnimating(moving, 33);
|
||||
}
|
||||
|
||||
private void label(Canvas c, String s, float x, float y, float size, int colour, Typeface tf, Paint.Align a) {
|
||||
text.setTypeface(tf);
|
||||
text.setTextSize(size);
|
||||
text.setTextAlign(a);
|
||||
text.setColor(colour);
|
||||
c.drawText(s, x, y, text);
|
||||
}
|
||||
|
||||
// ---------- The altar, once per size ----------
|
||||
|
||||
private void buildScene(int w, int h, float u) {
|
||||
scene = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888);
|
||||
sceneW = w;
|
||||
sceneH = h;
|
||||
marigold = flower(44f * u, MARIGOLD, GOLD, 0xFFE06A00);
|
||||
turquoise = flower(44f * u, TURQUOISE, 0xFFA8FFF4, 0xFF0E9C90);
|
||||
buildFlag(u);
|
||||
Canvas c = new Canvas(scene);
|
||||
c.drawColor(NIGHT);
|
||||
Paint glow = new Paint();
|
||||
glow.setShader(new RadialGradient(w / 2f, 345f * u, Math.max(w, h) * 0.6f,
|
||||
new int[] { 0xFF4A1C5E, PLUM, NIGHT }, new float[] { 0f, 0.45f, 1f }, Shader.TileMode.CLAMP));
|
||||
c.drawRect(0, 0, w, h, glow);
|
||||
// A scatter of stars, always the same.
|
||||
fill.setColor(0x66FFFFFF);
|
||||
for (int i = 0; i < 70; i++) {
|
||||
float x = ((i * 137 + 31) % 997) / 997f * w, y = (110f + (i * 71 % 113) * 3f) * u;
|
||||
c.drawCircle(x, y, (i % 3 == 0 ? 1.8f : 1f) * u, fill);
|
||||
}
|
||||
// The string the flags hang on.
|
||||
line.setColor(0x99FFFFFF);
|
||||
line.setStrokeWidth(1.5f * u);
|
||||
c.drawLine(0, 12f * u, w, 12f * u, line);
|
||||
drawSkull(c, w / 2f, 345f * u, u);
|
||||
drawAltarCloth(c, w, u);
|
||||
drawCandleStands(c, u);
|
||||
}
|
||||
|
||||
/** One flower on a small bitmap: outer petals, inner petals, heart. */
|
||||
private Bitmap flower(float size, int outer, int inner, int shade) {
|
||||
int s = (int) Math.ceil(size);
|
||||
Bitmap b = Bitmap.createBitmap(s, s, Bitmap.Config.ARGB_8888);
|
||||
Canvas c = new Canvas(b);
|
||||
drawFlower(c, s / 2f, s / 2f, s / 2f, outer, inner, shade);
|
||||
return b;
|
||||
}
|
||||
|
||||
private void drawFlower(Canvas c, float x, float y, float rad, int outer, int inner, int shade) {
|
||||
Paint p = new Paint(Paint.ANTI_ALIAS_FLAG);
|
||||
for (int ring = 0; ring < 3; ring++) {
|
||||
int n = 12 - ring * 3;
|
||||
float d = rad * (0.68f - ring * 0.24f), pr = rad * (0.32f - ring * 0.05f);
|
||||
for (int i = 0; i < n; i++) {
|
||||
double a = Math.PI * 2 * i / n + ring * 0.3;
|
||||
p.setColor(ring == 0 ? (i % 2 == 0 ? outer : shade) : ring == 1 ? (i % 2 == 0 ? inner : outer) : inner);
|
||||
c.drawCircle(x + (float) Math.cos(a) * d, y + (float) Math.sin(a) * d, pr, p);
|
||||
}
|
||||
}
|
||||
p.setColor(shade);
|
||||
c.drawCircle(x, y, rad * 0.14f, p);
|
||||
}
|
||||
|
||||
/** A paper flag with its holes cut out: scalloped below, a flower and diamonds in it. */
|
||||
private void buildFlag(float u) {
|
||||
float w = 100f * u, h = 84f * u;
|
||||
flag.reset();
|
||||
flag.setFillType(Path.FillType.EVEN_ODD);
|
||||
flag.moveTo(0, 0);
|
||||
flag.lineTo(w, 0);
|
||||
flag.lineTo(w, h - 8f * u);
|
||||
int scallops = 6;
|
||||
float sw = w / scallops;
|
||||
for (int i = scallops; i > 0; i--) {
|
||||
float x = i * sw;
|
||||
flag.quadTo(x - sw / 2f, h + 8f * u, x - sw, h - 8f * u);
|
||||
}
|
||||
flag.close();
|
||||
// The middle: a flower of eight petals round a round hole.
|
||||
float cx = w / 2f, cy = h * 0.46f;
|
||||
flag.addCircle(cx, cy, 6f * u, Path.Direction.CW);
|
||||
for (int i = 0; i < 8; i++) {
|
||||
double a = Math.PI / 4 * i;
|
||||
float px = cx + (float) Math.cos(a) * 17f * u, py = cy + (float) Math.sin(a) * 17f * u;
|
||||
diamond(flag, px, py, 7f * u, 5f * u, a);
|
||||
}
|
||||
// Corners and borders.
|
||||
for (int i = 0; i < 6; i++) {
|
||||
float x = (i + 0.5f) * w / 6f;
|
||||
diamond(flag, x, 8f * u, 5f * u, 3f * u, 0);
|
||||
if (i == 0 || i == 5) {
|
||||
diamond(flag, x, h * 0.3f, 4f * u, 6f * u, 0);
|
||||
diamond(flag, x, h * 0.62f, 4f * u, 6f * u, 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void diamond(Path p, float x, float y, float a, float b, double turn) {
|
||||
float ca = (float) Math.cos(turn), sa = (float) Math.sin(turn);
|
||||
p.moveTo(x + a * ca, y + a * sa);
|
||||
p.lineTo(x - b * sa, y + b * ca);
|
||||
p.lineTo(x - a * ca, y - a * sa);
|
||||
p.lineTo(x + b * sa, y - b * ca);
|
||||
p.close();
|
||||
}
|
||||
|
||||
/** The calavera: the bone, the decoration, the teeth. Its speed and ring go over it. */
|
||||
private void drawSkull(Canvas c, float cx, float cy, float u) {
|
||||
float top = cy - 150f * u, jaw = cy + 150f * u;
|
||||
// Bone.
|
||||
path.reset();
|
||||
path.moveTo(cx, top);
|
||||
path.cubicTo(cx + 110f * u, top, cx + 148f * u, cy - 70f * u, cx + 142f * u, cy + 8f * u);
|
||||
path.cubicTo(cx + 140f * u, cy + 48f * u, cx + 112f * u, cy + 62f * u, cx + 84f * u, cy + 74f * u);
|
||||
path.lineTo(cx + 78f * u, jaw - 8f * u);
|
||||
path.quadTo(cx, jaw + 6f * u, cx - 78f * u, jaw - 8f * u);
|
||||
path.lineTo(cx - 84f * u, cy + 74f * u);
|
||||
path.cubicTo(cx - 112f * u, cy + 62f * u, cx - 140f * u, cy + 48f * u, cx - 142f * u, cy + 8f * u);
|
||||
path.cubicTo(cx - 148f * u, cy - 70f * u, cx - 110f * u, top, cx, top);
|
||||
path.close();
|
||||
fill.setColor(BONE);
|
||||
c.drawPath(path, fill);
|
||||
line.setColor(PINK);
|
||||
line.setStrokeWidth(3f * u);
|
||||
c.drawPath(path, line);
|
||||
// A band of scallops under the forehead, where the speed stands above.
|
||||
line.setColor(PINK);
|
||||
line.setStrokeWidth(2.5f * u);
|
||||
for (int i = -4; i < 4; i++) {
|
||||
float x = cx + i * 26f * u;
|
||||
r.set(x, cy - 54f * u, x + 26f * u, cy - 28f * u);
|
||||
c.drawArc(r, 0, 180, false, line);
|
||||
}
|
||||
fill.setColor(TURQUOISE);
|
||||
for (int i = -4; i <= 4; i++) c.drawCircle(cx + i * 26f * u, cy - 41f * u, 3f * u, fill);
|
||||
// Eyes: dark sockets with a marigold each, a ring of dots round them.
|
||||
for (int side = -1; side <= 1; side += 2) {
|
||||
float ex = cx + side * 62f * u, ey = cy + 4f * u;
|
||||
fill.setColor(INK);
|
||||
c.drawCircle(ex, ey, 36f * u, fill);
|
||||
drawFlower(c, ex, ey, 31f * u, MARIGOLD, GOLD, 0xFFE06A00);
|
||||
fill.setColor(PINK);
|
||||
for (int i = 0; i < 14; i++) {
|
||||
double a = Math.PI * 2 * i / 14;
|
||||
c.drawCircle(ex + (float) Math.cos(a) * 43f * u, ey + (float) Math.sin(a) * 43f * u, 2.5f * u, fill);
|
||||
}
|
||||
// A small flower on each cheek.
|
||||
drawFlower(c, cx + side * 92f * u, cy + 56f * u, 10f * u, 0xFF9B5CFF, 0xFFD2B8FF, 0xFF6A32C8);
|
||||
}
|
||||
// Nose: an upside-down heart.
|
||||
path.reset();
|
||||
path.moveTo(cx, cy + 30f * u);
|
||||
path.cubicTo(cx + 18f * u, cy + 46f * u, cx + 20f * u, cy + 66f * u, cx + 8f * u, cy + 66f * u);
|
||||
path.quadTo(cx + 2f * u, cy + 66f * u, cx, cy + 58f * u);
|
||||
path.quadTo(cx - 2f * u, cy + 66f * u, cx - 8f * u, cy + 66f * u);
|
||||
path.cubicTo(cx - 20f * u, cy + 66f * u, cx - 18f * u, cy + 46f * u, cx, cy + 30f * u);
|
||||
path.close();
|
||||
fill.setColor(INK);
|
||||
c.drawPath(path, fill);
|
||||
// Teeth: a smile stitched across.
|
||||
line.setColor(INK);
|
||||
line.setStrokeWidth(3f * u);
|
||||
float my = cy + 106f * u;
|
||||
path.reset();
|
||||
path.moveTo(cx - 70f * u, my - 6f * u);
|
||||
path.quadTo(cx, my + 12f * u, cx + 70f * u, my - 6f * u);
|
||||
c.drawPath(path, line);
|
||||
line.setStrokeWidth(2.5f * u);
|
||||
for (int i = -5; i <= 5; i++) {
|
||||
float x = cx + i * 12f * u;
|
||||
float y = my + 3f * u * (1f - (i * i) / 25f);
|
||||
c.drawLine(x, y - 15f * u, x, y + 15f * u, line);
|
||||
}
|
||||
// The chin's little flower.
|
||||
drawFlower(c, cx, jaw - 13f * u, 10f * u, PINK, 0xFFFFA8D4, 0xFFC4186E);
|
||||
}
|
||||
|
||||
private void drawAltarCloth(Canvas c, int w, float u) {
|
||||
float rr = w - 28f * u, l = rr - 300f * u, t = 340f * u, b = 548f * u;
|
||||
r.set(l, t, rr, b);
|
||||
fill.setColor(CLOTH);
|
||||
c.drawRoundRect(r, 10f * u, 10f * u, fill);
|
||||
// A fringe of paper colours on top.
|
||||
float seg = (rr - l) / 15f;
|
||||
for (int i = 0; i < 15; i++) {
|
||||
fill.setColor(PAPER[i % PAPER.length]);
|
||||
path.reset();
|
||||
path.moveTo(l + i * seg, t);
|
||||
path.lineTo(l + (i + 1) * seg, t);
|
||||
path.lineTo(l + (i + 0.5f) * seg, t + 13f * u);
|
||||
path.close();
|
||||
c.drawPath(path, fill);
|
||||
}
|
||||
}
|
||||
|
||||
/** The tier the candles stand on. */
|
||||
private void drawCandleStands(Canvas c, float u) {
|
||||
float l = 28f * u, rr = l + 300f * u, b = 548f * u;
|
||||
r.set(l, b - 26f * u, rr, b);
|
||||
fill.setColor(CLOTH);
|
||||
c.drawRoundRect(r, 6f * u, 6f * u, fill);
|
||||
float seg = (rr - l) / 15f;
|
||||
for (int i = 0; i < 15; i++) {
|
||||
fill.setColor(PAPER[i % PAPER.length]);
|
||||
path.reset();
|
||||
path.moveTo(l + i * seg, b - 26f * u);
|
||||
path.lineTo(l + (i + 1) * seg, b - 26f * u);
|
||||
path.lineTo(l + (i + 0.5f) * seg, b - 15f * u);
|
||||
path.close();
|
||||
c.drawPath(path, fill);
|
||||
}
|
||||
}
|
||||
|
||||
// ---------- Papel picado: the battery ----------
|
||||
|
||||
private void drawFlags(Canvas c, int w, float u) {
|
||||
boolean low = v.socPercent != null && v.socPercent < 20f;
|
||||
int lit = Tally.lit(v.socPercent, 10f, FLAGS);
|
||||
float fw = 100f * u, gap = (w - FLAGS * fw) / (FLAGS + 1f);
|
||||
fill.setStyle(Paint.Style.FILL);
|
||||
for (int i = 0; i < FLAGS; i++) {
|
||||
float x = gap + i * (fw + gap);
|
||||
c.save();
|
||||
c.translate(x, 12f * u);
|
||||
// The breeze of driving; still when parked.
|
||||
c.skew((float) Math.sin(time * 2.2f + i * 0.8f) * 0.05f, 0);
|
||||
boolean on = i < lit;
|
||||
fill.setColor(!on ? 0x38FFFFFF : low ? RED : PAPER[i % PAPER.length]);
|
||||
c.drawPath(flag, fill);
|
||||
c.restore();
|
||||
}
|
||||
}
|
||||
|
||||
private void drawBattery(Canvas c, float u) {
|
||||
boolean low = v.socPercent != null && v.socPercent < 20f;
|
||||
float x = 36f * u;
|
||||
label(c, low ? "BATTERY LOW" : "BATTERY", x, 134f * u, 19f * u, low ? RED : GOLD, face, Paint.Align.LEFT);
|
||||
label(c, fmt(v.socPercent, "%.0f %%", "—"), x, 200f * u, 66f * u, low ? RED : BONE, serif, Paint.Align.LEFT);
|
||||
}
|
||||
|
||||
// ---------- The marigold ring: the power ----------
|
||||
|
||||
private void drawRing(Canvas c, float cx, float cy, float u) {
|
||||
float rad = 188f * u, half = marigold.getWidth() / 2f;
|
||||
int lit = Tally.ofPower(v.powerKw, 150f, 50f, PETALS);
|
||||
boolean regen = v.powerKw != null && v.powerKw < 0f;
|
||||
for (int i = 0; i < PETALS; i++) {
|
||||
// Clockwise from the top; regeneration runs the other way.
|
||||
int k = regen ? PETALS - i : i;
|
||||
double a = Math.PI * 2 * k / PETALS - Math.PI / 2;
|
||||
float x = cx + (float) Math.cos(a) * rad, y = cy + (float) Math.sin(a) * rad;
|
||||
boolean on = i < lit;
|
||||
image.setAlpha(on ? 255 : 46);
|
||||
c.drawBitmap(on && regen ? turquoise : marigold, x - half, y - half, image);
|
||||
}
|
||||
image.setAlpha(255);
|
||||
// The power's number on a plate at the foot of the ring.
|
||||
String power = v.powerKw == null ? "— kW" : String.format(Locale.US, regen ? "+%.1f kW" : "%.1f kW",
|
||||
Math.abs(v.powerKw));
|
||||
float py = cy + rad + 2f * u;
|
||||
r.set(cx - 78f * u, py - 22f * u, cx + 78f * u, py + 20f * u);
|
||||
fill.setColor(0xF02C1242);
|
||||
c.drawRoundRect(r, 20f * u, 20f * u, fill);
|
||||
line.setColor(regen ? TURQUOISE : MARIGOLD);
|
||||
line.setStrokeWidth(2f * u);
|
||||
c.drawRoundRect(r, 20f * u, 20f * u, line);
|
||||
label(c, power, cx, py + 10f * u, 28f * u, regen ? TURQUOISE : GOLD, face, Paint.Align.CENTER);
|
||||
}
|
||||
|
||||
private void drawSpeed(Canvas c, float cx, float cy, float u) {
|
||||
label(c, fmt(v.speedKmh, "%.0f", "—"), cx, cy - 66f * u, 74f * u, INK, serif, Paint.Align.CENTER);
|
||||
label(c, "KM/H", cx, cy - 128f * u, 14f * u, 0xFFC4186E, face, Paint.Align.CENTER);
|
||||
}
|
||||
|
||||
// ---------- Candles: the range ----------
|
||||
|
||||
private void drawCandles(Canvas c, float u, boolean moving) {
|
||||
float x0 = 36f * u;
|
||||
label(c, "RANGE", x0, 246f * u, 19f * u, GOLD, face, Paint.Align.LEFT);
|
||||
label(c, fmt(v.rangeKm, "%.0f km", "—"), x0, 298f * u, 48f * u, BONE, serif, Paint.Align.LEFT);
|
||||
int lit = Tally.lit(v.rangeKm, 50f, CANDLES);
|
||||
float base = 522f * u, step = 36f * u;
|
||||
for (int i = 0; i < CANDLES; i++) {
|
||||
float x = x0 + 14f * u + i * step;
|
||||
float tall = (130f - (i % 4) * 22f - (i % 3) * 8f) * u;
|
||||
float top = base - tall;
|
||||
r.set(x - 11f * u, top, x + 11f * u, base);
|
||||
boolean on = i < lit;
|
||||
fill.setColor(on ? WAX : 0x40F4E6C8);
|
||||
c.drawRoundRect(r, 3f * u, 3f * u, fill);
|
||||
// A band of colour.
|
||||
r.set(x - 11f * u, top + tall * 0.55f, x + 11f * u, top + tall * 0.55f + 10f * u);
|
||||
fill.setColor(AccentColor.withAlpha(PAPER[i % PAPER.length], on ? 0xFF : 0x40));
|
||||
c.drawRect(r, fill);
|
||||
// The wick.
|
||||
line.setColor(on ? INK : 0x60FFFFFF);
|
||||
line.setStrokeWidth(2f * u);
|
||||
c.drawLine(x, top, x, top - 6f * u, line);
|
||||
if (!on) continue;
|
||||
// The flame, flickering only while driving, with its halo.
|
||||
float flick = moving ? (float) Math.sin(time * 9f + i * 1.7f) * 2.5f * u : 0f;
|
||||
fill.setColor(0x33FFB84A);
|
||||
c.drawCircle(x, top - 18f * u, 19f * u, fill);
|
||||
path.reset();
|
||||
path.moveTo(x, top - 32f * u - Math.abs(flick));
|
||||
path.quadTo(x + 10f * u + flick, top - 12f * u, x, top - 4f * u);
|
||||
path.quadTo(x - 10f * u + flick, top - 12f * u, x, top - 32f * u - Math.abs(flick));
|
||||
path.close();
|
||||
fill.setColor(FLAME);
|
||||
c.drawPath(path, fill);
|
||||
}
|
||||
}
|
||||
|
||||
// ---------- The petal path: the last two minutes of power ----------
|
||||
|
||||
private void drawPetals(Canvas c, int w, int h, float u) {
|
||||
float base = h - 16f * u, amp = 58f * u;
|
||||
float[] trail = v.powerTrail;
|
||||
// The path itself, faint, whatever the readings.
|
||||
fill.setColor(0x30FF9A1F);
|
||||
r.set(0, base - 4f * u, w, base + 4f * u);
|
||||
c.drawRect(r, fill);
|
||||
if (trail == null || trail.length == 0) return;
|
||||
int n = trail.length;
|
||||
for (int i = 0; i < n; i++) {
|
||||
float p = trail[i];
|
||||
if (Float.isNaN(p)) continue;
|
||||
float x = (i + 0.5f) / n * w;
|
||||
float share = Math.min(1f, Math.abs(p) / (p < 0 ? 40f : 60f));
|
||||
// Each stack of petals rises with the power, each petal tilted its own way.
|
||||
int stack = 1 + Math.round(share * 4f);
|
||||
for (int s = 0; s < stack; s++) {
|
||||
float y = base - s * amp / 4f;
|
||||
c.save();
|
||||
c.rotate(((i * 47 + s * 83) % 180) - 90f, x, y);
|
||||
r.set(x - 8f * u, y - 3.5f * u, x + 8f * u, y + 3.5f * u);
|
||||
fill.setColor(p < 0 ? TURQUOISE : s % 2 == 0 ? MARIGOLD : GOLD);
|
||||
c.drawOval(r, fill);
|
||||
c.restore();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ---------- The right side ----------
|
||||
|
||||
/** The clock in a tin frame. */
|
||||
private void drawClock(Canvas c, int w, float u) {
|
||||
float rr = w - 28f * u, l = rr - 300f * u, t = 118f * u, b = 202f * u;
|
||||
r.set(l, t, rr, b);
|
||||
fill.setColor(0xCC160A26);
|
||||
c.drawRoundRect(r, 12f * u, 12f * u, fill);
|
||||
line.setColor(GOLD);
|
||||
line.setStrokeWidth(2.5f * u);
|
||||
c.drawRoundRect(r, 12f * u, 12f * u, line);
|
||||
fill.setColor(GOLD);
|
||||
for (int i = 0; i <= 12; i++) {
|
||||
float x = l + 12f * u + i * (rr - l - 24f * u) / 12f;
|
||||
c.drawCircle(x, t + 8f * u, 2f * u, fill);
|
||||
c.drawCircle(x, b - 8f * u, 2f * u, fill);
|
||||
}
|
||||
label(c, v.clock.isEmpty() ? "—" : v.clock, (l + rr) / 2f, b - 22f * u, 50f * u, BONE, serif, Paint.Align.CENTER);
|
||||
}
|
||||
|
||||
/** P R N D as four small marigolds: the one engaged in bloom. */
|
||||
private void drawGears(Canvas c, int w, float u) {
|
||||
float rr = w - 28f * u, l = rr - 300f * u, y = 270f * u;
|
||||
String[] gears = { "P", "R", "N", "D" };
|
||||
String now = Gear.label(v.gear);
|
||||
float step = (rr - l) / 4f, half = marigold.getWidth() / 2f, scale = 58f * u / marigold.getWidth();
|
||||
for (int i = 0; i < gears.length; i++) {
|
||||
float x = l + (i + 0.5f) * step;
|
||||
boolean on = gears[i].equals(now);
|
||||
if (on) {
|
||||
c.save();
|
||||
c.scale(scale, scale, x, y);
|
||||
c.drawBitmap(marigold, x - half, y - half, image);
|
||||
c.restore();
|
||||
} else {
|
||||
fill.setColor(0x40FFFFFF);
|
||||
c.drawCircle(x, y, 22f * u, fill);
|
||||
}
|
||||
label(c, gears[i], x, y + 11f * u, 30f * u, on ? INK : 0xB0FFFFFF, serif, Paint.Align.CENTER);
|
||||
}
|
||||
}
|
||||
|
||||
/** Trip, usage, climate, odometer on the altar cloth. */
|
||||
private void drawRows(Canvas c, int w, int h, float u) {
|
||||
float rr = w - 28f * u, l = rr - 300f * u, top = 378f * u;
|
||||
String[][] rows = {
|
||||
{ "TRIP", fmt(v.tripKm, "%.1f km", "—") },
|
||||
{ "5 MIN", fmt(v.usage5Min, "%.1f kWh/100", "—") },
|
||||
{ "SINCE START", fmt(v.usageTrip, "%.1f kWh/100", "—") },
|
||||
{ "CLIMATE", fmt(v.climateCelsius, "%.0f °C", "—") },
|
||||
{ "ODOMETER", fmt(v.odometerKm, "%,.0f km", "—") },
|
||||
};
|
||||
for (int i = 0; i < rows.length; i++) {
|
||||
float y = top + i * 35f * u;
|
||||
label(c, rows[i][0], l + 18f * u, y, 19f * u, GOLD, thin, Paint.Align.LEFT);
|
||||
label(c, rows[i][1], rr - 18f * u, y, 23f * u, BONE, face, Paint.Align.RIGHT);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -31,7 +31,9 @@ enum OverviewStyle {
|
||||
TIME_CIRCUITS(false, false),
|
||||
QUEST(false, false),
|
||||
VISOR(true, true),
|
||||
EMPIRE(false, false);
|
||||
EMPIRE(false, false),
|
||||
MUERTOS(false, false),
|
||||
HALLOWEEN(false, false);
|
||||
|
||||
static final String PREF_KEY = "overview_style";
|
||||
|
||||
|
||||
@@ -24,6 +24,8 @@ final class StylePalette {
|
||||
case QUEST: return R.style.ThemeOverlay_EV_Style_Quest;
|
||||
case VISOR: return R.style.ThemeOverlay_EV_Style_Visor;
|
||||
case EMPIRE: return R.style.ThemeOverlay_EV_Style_Empire;
|
||||
case MUERTOS: return R.style.ThemeOverlay_EV_Style_Muertos;
|
||||
case HALLOWEEN: return R.style.ThemeOverlay_EV_Style_Halloween;
|
||||
default: return 0;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
package com.evsuite.abrp;
|
||||
|
||||
/**
|
||||
* How many of a row of things a value lights: flags, candles, petals, embers, bats. An unread
|
||||
* value lights none, never a made-up amount. Android-free and unit-tested.
|
||||
*/
|
||||
final class Tally {
|
||||
|
||||
private Tally() {}
|
||||
|
||||
/** One thing per {@code step} of the value, rounded, from 0 to {@code max}; null or NaN is 0. */
|
||||
static int lit(Float value, float step, int max) {
|
||||
if (value == null || value.isNaN() || step <= 0f || value <= 0f) return 0;
|
||||
return Math.min(max, Math.round(value / step));
|
||||
}
|
||||
|
||||
/**
|
||||
* Of {@code count} things round a gauge, how many the power lights: all of them at
|
||||
* {@code fullDraw} kW drawn, or at {@code fullRegen} kW regenerated (negative power).
|
||||
* Any real power lights at least one, so the gauge never looks idle while it is not.
|
||||
*/
|
||||
static int ofPower(Float kw, float fullDraw, float fullRegen, int count) {
|
||||
if (kw == null || kw.isNaN() || Math.abs(kw) < 0.1f) return 0;
|
||||
float share = Math.abs(kw) / (kw < 0f ? fullRegen : fullDraw);
|
||||
return Math.max(1, Math.min(count, Math.round(share * count)));
|
||||
}
|
||||
}
|
||||
@@ -141,6 +141,8 @@
|
||||
<string name="overview_style_quest">Quest</string>
|
||||
<string name="overview_style_visor">Visor</string>
|
||||
<string name="overview_style_empire">Empire</string>
|
||||
<string name="overview_style_muertos">Día de Muertos</string>
|
||||
<string name="overview_style_halloween">Halloween</string>
|
||||
<string name="overview_style_nixie">Nixie tubes</string>
|
||||
<string name="overview_style_chronograph">Chronograph</string>
|
||||
<string name="cockpit_battery">BATTERY</string>
|
||||
|
||||
@@ -104,4 +104,24 @@
|
||||
<item name="evAccentStroke">#80344A99</item>
|
||||
<item name="evAccentTrack">#2A3F82</item>
|
||||
</style>
|
||||
<!-- Día de Muertos: the pink of the papel picado. -->
|
||||
<style name="ThemeOverlay.EV.Style.Muertos" parent="">
|
||||
<item name="evAccent">#FF2E93</item>
|
||||
<item name="evOnAccent">#22041A</item>
|
||||
<item name="evAccentLight">#FFA8D4</item>
|
||||
<item name="evAccentDeep">#4A0E32</item>
|
||||
<item name="evAccentSurface">#2C1242</item>
|
||||
<item name="evAccentStroke">#80993366</item>
|
||||
<item name="evAccentTrack">#7A2558</item>
|
||||
</style>
|
||||
<!-- Halloween: the pumpkin's orange. -->
|
||||
<style name="ThemeOverlay.EV.Style.Halloween" parent="">
|
||||
<item name="evAccent">#FF8C1A</item>
|
||||
<item name="evOnAccent">#1F0E02</item>
|
||||
<item name="evAccentLight">#FFC27A</item>
|
||||
<item name="evAccentDeep">#3D2008</item>
|
||||
<item name="evAccentSurface">#22102E</item>
|
||||
<item name="evAccentStroke">#80804A1E</item>
|
||||
<item name="evAccentTrack">#6A3F18</item>
|
||||
</style>
|
||||
</resources>
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
package com.evsuite.abrp;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
|
||||
import org.junit.Test;
|
||||
|
||||
public class TallyTest {
|
||||
|
||||
@Test
|
||||
public void anUnreadValueLightsNothing() {
|
||||
assertEquals(0, Tally.lit(null, 10f, 10));
|
||||
assertEquals(0, Tally.lit(Float.NaN, 10f, 10));
|
||||
assertEquals(0, Tally.ofPower(null, 150f, 50f, 24));
|
||||
assertEquals(0, Tally.ofPower(Float.NaN, 150f, 50f, 24));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void oneThingPerStepUpToTheRow() {
|
||||
assertEquals(8, Tally.lit(76f, 10f, 10));
|
||||
assertEquals(10, Tally.lit(100f, 10f, 10));
|
||||
assertEquals(8, Tally.lit(900f, 50f, 8));
|
||||
assertEquals(6, Tally.lit(312f, 50f, 8));
|
||||
assertEquals(0, Tally.lit(0f, 10f, 10));
|
||||
assertEquals(0, Tally.lit(-5f, 10f, 10));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void powerFillsTheGaugeEachWay() {
|
||||
assertEquals(0, Tally.ofPower(0f, 150f, 50f, 24));
|
||||
assertEquals(24, Tally.ofPower(150f, 150f, 50f, 24));
|
||||
assertEquals(24, Tally.ofPower(400f, 150f, 50f, 24));
|
||||
assertEquals(12, Tally.ofPower(-25f, 150f, 50f, 24));
|
||||
assertEquals(24, Tally.ofPower(-50f, 150f, 50f, 24));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void realPowerAlwaysShows() {
|
||||
assertEquals(1, Tally.ofPower(0.5f, 150f, 50f, 24));
|
||||
assertEquals(1, Tally.ofPower(-0.2f, 150f, 50f, 24));
|
||||
}
|
||||
}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 334 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 304 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 466 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 441 KiB |
Reference in New Issue
Block a user