App 2.48.0: the Christmas style
A tree with a bauble for the speed, fairy lights for the power, a mug of cocoa for the battery, a wreath clock and candy canes for the range. Its lights flare on the beat with the music light, now "Light follows the music" for Halloween and Christmas. A sleigh or an angel tows the song. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LhzAVaUyqcw5PkGjz4mWW6
This commit is contained in:
@@ -77,7 +77,7 @@ Repository: `ssh://git@git.stinnesbeck.com:2222/nils/mg4.git` (remote `origin`),
|
|||||||
|
|
||||||
| Tab | Contents |
|
| Tab | Contents |
|
||||||
|---|---|
|
|---|---|
|
||||||
| Overview | value tiles (battery, range, range/usage/energy since start, 5-min averages of range, usage, power, speed, and under the battery what plays: `NowPlaying` picks from the media sessions `NowPlayingWatcher` follows — listened to only, never connected to, nothing logged or sent; Tiles, Terminal, Sci-fi, Quest, Empire, Día de Muertos and Halloween, Settings → "Show what's playing", `PREF_NOW_PLAYING`, on by default), 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 pumpkin's light flaring on the kick drum while on screen, when "Pumpkin light follows the music" is on (off by default, `PREF_MUSIC_LIGHT`; `MusicTap`, `MusicPulse`: Android's audio analyser, 40–120 Hz, jumps not levels; Bluetooth and CarPlay, not the radio); 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) |
|
| Overview | value tiles (battery, range, range/usage/energy since start, 5-min averages of range, usage, power, speed, and under the battery what plays: `NowPlaying` picks from the media sessions `NowPlayingWatcher` follows — listened to only, never connected to, nothing logged or sent; Tiles, Terminal, Sci-fi, Quest, Empire, Día de Muertos, Halloween and Christmas, Settings → "Show what's playing", `PREF_NOW_PLAYING`, on by default), 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), Christmas (`ChristmasView`, a tree with a bauble, fairy lights, a mug of cocoa, a wreath clock and candy canes in the snow), all counting with `Tally`, the pumpkin's light and Christmas's tree lights flaring on the kick drum while on screen, when "Light follows the music" is on (off by default, `PREF_MUSIC_LIGHT`; `MusicTap`, `MusicPulse`: Android's audio analyser, 40–120 Hz, jumps not levels; Bluetooth and CarPlay, not the radio); 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, Christmas; 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 |
|
| Consumption / Charging | period totals, daily log, charging sessions and curve |
|
||||||
| Live data | on/off, ABRP API key and user token, upload frequency |
|
| 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 |
|
| Server | URL, pairing by QR, ingest token, installation ID (read-only), config import |
|
||||||
|
|||||||
@@ -140,6 +140,7 @@ 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
|
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`
|
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).
|
(Android 9 AOSP at 1920×720, 160 dpi; the car build installs as the system user — checked).
|
||||||
|
- 2026-11-07 — App 2.48.0: **the Christmas style** (owner: "let's also do a christmas based theme", with music and something that pulsates; design agreed first). `ChristmasView`: a tree with a big red bauble (speed), fairy lights arching over it (power: gold left to right while drawing, ice blue and right to left while regenerating), a glass mug of cocoa filling with the charge (cold blue and "BATTERY LOW" below 20 %), gift tags (trip, usage, climate, odometer), a wreath clock, a signboard (range) with a candy cane per 50 km, P R N D as baubles, and a snow drift of the last two minutes of power; snow falls and the lights twinkle while driving. Own palette (red); always in the drop-down; English only. The music light is now **"Light follows the music"** for Halloween and Christmas (same `PREF_MUSIC_LIGHT`, off by default): the tree's lights and the arch flare on the kick drum. What plays: Santa's sleigh with a reindeer and, on the next pass, an angel with a trumpet tow a strip of wrapping paper (colour fixed by the song); both in `SongCarriers`. Two screenshots in THEMES.md. Tested: unit tests, lint, emulator with sample values (panel and full screen, and with no data), with the throwaway fake song (both carriers), and the music-tap thread runs only with the switch on. Not tried on the emulator: the pulse itself with real sound (the code path is Halloween's `MusicTap`). Not tried on the car.
|
||||||
- 2026-11-02 — App 2.47.0 published (`car-2.47.0`).
|
- 2026-11-02 — App 2.47.0 published (`car-2.47.0`).
|
||||||
- 2026-11-02 — App 2.47.0: **now playing in four more styles** (owner: "let's implement Now playing in more styles"; Spanish labels declined). Quest: a red dragon tows a waving scroll across the top of the sky, behind the frames. Empire: an eagle flies a scroll across the top of the world, behind the speed. Terminal: a PLAYING row above the prompt. Sci-fi: the song in black on the top bar. The creatures are in the new `SongCarriers` (dragon, eagle) beside `SongBanner`. Same switch as before (Settings → "Show what's playing"). Not yet in the rest: Gauges, Cockpit, Retro, Futuristic, MG, Minimal, Nixie, Chronograph, Grid, Grid with map, Time circuits, Visor, Cartoon. Tested: unit tests, lint, emulator with the throwaway fake session. Not tried on the car.
|
- 2026-11-02 — App 2.47.0: **now playing in four more styles** (owner: "let's implement Now playing in more styles"; Spanish labels declined). Quest: a red dragon tows a waving scroll across the top of the sky, behind the frames. Empire: an eagle flies a scroll across the top of the world, behind the speed. Terminal: a PLAYING row above the prompt. Sci-fi: the song in black on the top bar. The creatures are in the new `SongCarriers` (dragon, eagle) beside `SongBanner`. Same switch as before (Settings → "Show what's playing"). Not yet in the rest: Gauges, Cockpit, Retro, Futuristic, MG, Minimal, Nixie, Chronograph, Grid, Grid with map, Time circuits, Visor, Cartoon. Tested: unit tests, lint, emulator with the throwaway fake session. Not tried on the car.
|
||||||
- 2026-11-02 — App 2.46.0 published (`car-2.46.0`).
|
- 2026-11-02 — App 2.46.0 published (`car-2.46.0`).
|
||||||
|
|||||||
@@ -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 →
|
**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
|
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, Empire, Día de Muertos and Halloween 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, Halloween and Christmas always do; while one of those is shown, the whole app
|
||||||
(navigation, buttons, switches) takes on its colours too.
|
(navigation, buttons, switches) takes on its colours too.
|
||||||
|
|
||||||
| Style | Look | Colours |
|
| Style | Look | Colours |
|
||||||
@@ -36,6 +36,7 @@ colours, and Sci-fi, Cartoon, Nixie tubes, Time circuits, Quest, Empire, Día de
|
|||||||
| [Empire](#empire) | a strategy game's interface | own |
|
| [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 |
|
| [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 |
|
| [Halloween](#halloween) | a jack-o'-lantern on a graveyard hill | own |
|
||||||
|
| [Christmas](#christmas) | a Christmas tree in a snowy village | own |
|
||||||
|
|
||||||
## Tiles
|
## Tiles
|
||||||
|
|
||||||
@@ -281,3 +282,23 @@ pumpkin's orange with it.
|
|||||||
|
|
||||||

|

|
||||||

|

|
||||||
|
|
||||||
|
## Christmas
|
||||||
|
|
||||||
|
A snowy village at night, warm and festive. In the middle a Christmas tree with a big red bauble
|
||||||
|
in front of it: the speed. Fairy lights arch over the tree: the power, left to right in warm gold
|
||||||
|
while drawing, ice blue and right to left while regenerating. On the left a glass mug of hot cocoa
|
||||||
|
fills with the charge, with marshmallows floating on it and steam rising; it turns cold blue with
|
||||||
|
"BATTERY LOW" blinking below 20 %. Above it, gift tags hang from a garland with trip, usage,
|
||||||
|
climate and odometer. On the right a wreath holds the clock, and a snowy signboard reads the range
|
||||||
|
with a candy cane for every 50 km in the snow below; P R N D are four baubles, the one engaged lit.
|
||||||
|
The snow drift along the bottom is the last two minutes of power, rising with it. While you drive the
|
||||||
|
snow falls and the tree's lights twinkle. With "Light follows the music" on (Settings, shown with
|
||||||
|
this style and Halloween; off by default) and Bluetooth or CarPlay music playing, the lights on the
|
||||||
|
tree and the fairy lights flare on each kick drum; the radio cannot be heard, so they twinkle as
|
||||||
|
usual then. What plays (Bluetooth, CarPlay or the radio) crosses the sky above the tree on a waving
|
||||||
|
strip of wrapping paper towed by Santa's sleigh and, on the next pass, a small angel with a
|
||||||
|
trumpet; the paper's colour is fixed by the song. The app turns red with it.
|
||||||
|
|
||||||
|

|
||||||
|

|
||||||
|
|||||||
+2
-2
@@ -34,8 +34,8 @@ android {
|
|||||||
applicationId "com.stinnesbeck.nilsmg4"
|
applicationId "com.stinnesbeck.nilsmg4"
|
||||||
minSdk 27
|
minSdk 27
|
||||||
targetSdk 36
|
targetSdk 36
|
||||||
versionCode 164
|
versionCode 165
|
||||||
versionName "2.47.0"
|
versionName "2.48.0"
|
||||||
|
|
||||||
// Nothing about the server ships in the APK: its URL and ingest token are entered in
|
// 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.
|
// the app's Server tab (ServerConfig), and the database is only reachable by the server.
|
||||||
|
|||||||
@@ -0,0 +1,616 @@
|
|||||||
|
package com.evsuite.abrp;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import android.graphics.Bitmap;
|
||||||
|
import android.graphics.Canvas;
|
||||||
|
import android.graphics.DashPathEffect;
|
||||||
|
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 Christmas Overview style: a snowy village night, warm and festive.
|
||||||
|
*
|
||||||
|
* In the middle a Christmas tree with a big red bauble in front of it: the speed. Fairy lights
|
||||||
|
* arch over the tree: the power, left to right in warm gold while drawing, ice blue and right to
|
||||||
|
* left while regenerating. On the left a glass mug of hot cocoa filling with the charge, cold blue
|
||||||
|
* with "BATTERY LOW" blinking below 20 %, under gift tags with trip, usage, climate and odometer.
|
||||||
|
* On the right a wreath holds the clock, and a snowy signboard reads the range, with a candy cane
|
||||||
|
* per 50 km; P R N D are four baubles, the one engaged lit. The snow drift along the bottom is the
|
||||||
|
* last two minutes of power, rising with it, in gold or, where it regenerated, ice blue. Unread
|
||||||
|
* values show "—" and light nothing. With music playing, the tree's lights and the fairy lights
|
||||||
|
* flare on the beat ({@link MusicTap}); with no music they twinkle as before. What plays crosses
|
||||||
|
* the sky on a banner of wrapping paper, towed by Santa's sleigh and, on the next pass, an angel
|
||||||
|
* ({@link NowPlaying}).
|
||||||
|
*
|
||||||
|
* The night, the hills and the stars are drawn once into a bitmap per size; only the rest goes over
|
||||||
|
* it. Snow falls and the lights twinkle (about 30 frames a second) only while the car moves and the
|
||||||
|
* view is on screen; the lights' pulse and the song keep it drawing while there are any.
|
||||||
|
*/
|
||||||
|
final class ChristmasView extends ClusterCanvasView {
|
||||||
|
|
||||||
|
private static final int BULBS = 25, FLAKES = 70, CANES = 8;
|
||||||
|
private static final int SKY_TOP = 0xFF06142B, SKY_LOW = 0xFF173A5E, SKY_END = 0xFF2A5A7A;
|
||||||
|
private static final int SNOW = 0xFFE9F2FF, PINE = 0xFF1F7A3C, PINE_DARK = 0xFF0F4A24;
|
||||||
|
private static final int RED = 0xFFE53946, GOLD = 0xFFFFD04A, BONE = 0xFFF6F0E4, INK = 0xFF1B0F12;
|
||||||
|
private static final int COCOA = 0xFF7B3F22, COLD = 0xFF4A8AD0, ICE = 0xFF8FD8FF, WOOD = 0xFF4A2E1A;
|
||||||
|
private static final int[] LIGHTS = { RED, GOLD, ICE, 0xFFFFFFFF, 0xFF5CE08A };
|
||||||
|
private static final int[] PAPER = { 0xFFC62828, 0xFF1E7B3C, 0xFF2B5FA8, 0xFFD9A62E, 0xFFF1F1F1 };
|
||||||
|
|
||||||
|
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 SongBanner banner = new SongBanner();
|
||||||
|
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;
|
||||||
|
private MusicTap music;
|
||||||
|
|
||||||
|
ChristmasView(Context context) {
|
||||||
|
super(context);
|
||||||
|
line.setStyle(Paint.Style.STROKE);
|
||||||
|
line.setStrokeCap(Paint.Cap.ROUND);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Where the lights hear the music; without it, they do not follow any. */
|
||||||
|
void setMusic(MusicTap tap) {
|
||||||
|
music = tap;
|
||||||
|
}
|
||||||
|
|
||||||
|
@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);
|
||||||
|
boolean song = drawSong(c, w, u);
|
||||||
|
drawSnow(c, w, h, u);
|
||||||
|
drawDrift(c, w, h, u);
|
||||||
|
float pulse = music == null ? 0f : music.pulse();
|
||||||
|
drawTree(c, w, u, pulse);
|
||||||
|
drawArch(c, w, u, pulse);
|
||||||
|
drawBauble(c, w, u, pulse);
|
||||||
|
drawWreath(c, w, u);
|
||||||
|
drawTags(c, u);
|
||||||
|
drawMug(c, u);
|
||||||
|
drawBoard(c, w, u);
|
||||||
|
drawGears(c, w, u);
|
||||||
|
keepAnimating(moving || pulse > 0f || song, 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);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Text with a dark outline, to read over whatever is behind it. */
|
||||||
|
private void outlined(Canvas c, String s, float x, float y, float size, int colour, Typeface tf, Paint.Align a, float u) {
|
||||||
|
text.setStyle(Paint.Style.STROKE);
|
||||||
|
text.setStrokeWidth(Math.max(3f * u, size * 0.12f));
|
||||||
|
text.setStrokeJoin(Paint.Join.ROUND);
|
||||||
|
label(c, s, x, y, size, 0xDD0A0608, tf, a);
|
||||||
|
text.setStyle(Paint.Style.FILL);
|
||||||
|
label(c, s, x, y, size, colour, tf, a);
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean regen() {
|
||||||
|
return v.powerKw != null && v.powerKw < -0.05f;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------- Night sky and hills ----------
|
||||||
|
|
||||||
|
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, SKY_END },
|
||||||
|
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(0x99FFFFFF);
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
// Two ranges of snowy hills, with pines on the far one.
|
||||||
|
hills(c, w, h, h - 150f * u, 0xFF28496B, 0.8f, u);
|
||||||
|
Paint pine = new Paint(Paint.ANTI_ALIAS_FLAG);
|
||||||
|
pine.setColor(0xFF0F3A2A);
|
||||||
|
for (int i = 0; i < 14; i++) {
|
||||||
|
float x = ((i * 233 + 41) % 997) / 997f * w, tall = (36f + (i * 17) % 30) * u;
|
||||||
|
float base = h - 150f * u + 8f * u;
|
||||||
|
Path tri = new Path();
|
||||||
|
tri.moveTo(x, base - tall);
|
||||||
|
tri.lineTo(x + tall * 0.32f, base);
|
||||||
|
tri.lineTo(x - tall * 0.32f, base);
|
||||||
|
tri.close();
|
||||||
|
c.drawPath(tri, pine);
|
||||||
|
}
|
||||||
|
hills(c, w, h, h - 100f * u, 0xFF3C6286, 1.5f, u);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void hills(Canvas c, int w, int h, float base, int colour, float wave, float u) {
|
||||||
|
Path hill = new Path();
|
||||||
|
hill.moveTo(0, h);
|
||||||
|
for (int i = 0; i <= 24; i++) {
|
||||||
|
float x = w * i / 24f;
|
||||||
|
hill.lineTo(x, base - (float) (Math.sin(i * wave) * 0.5 + Math.sin(i * wave * 0.37 + 1) * 0.5) * 22f * u);
|
||||||
|
}
|
||||||
|
hill.lineTo(w, h);
|
||||||
|
hill.close();
|
||||||
|
Paint p = new Paint(Paint.ANTI_ALIAS_FLAG);
|
||||||
|
p.setColor(colour);
|
||||||
|
c.drawPath(hill, p);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void drawSnow(Canvas c, int w, int h, float u) {
|
||||||
|
for (int i = 0; i < FLAKES; i++) {
|
||||||
|
float fall = (30f + (i % 5) * 12f) * u;
|
||||||
|
float y = (((i * 53) % 600) * u + time * fall) % h;
|
||||||
|
float x = ((i * 97 + 13) % 1000) / 1000f * w + (float) Math.sin(time * 0.8f + i) * 12f * u;
|
||||||
|
fill.setColor(i % 3 == 0 ? 0xE0FFFFFF : 0x90FFFFFF);
|
||||||
|
c.drawCircle(x, y, (1f + (i % 3) * 0.8f) * u, fill);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------- The drift: the last two minutes of power ----------
|
||||||
|
|
||||||
|
private void drawDrift(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(SNOW);
|
||||||
|
c.drawPath(path, fill);
|
||||||
|
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 ? ICE : GOLD);
|
||||||
|
c.drawLine((i - 1f) / (n - 1f) * w, base - ha * amp, i / (n - 1f) * w, base - hb * amp, line);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------- The tree ----------
|
||||||
|
|
||||||
|
private void drawTree(Canvas c, int w, float u, float pulse) {
|
||||||
|
float cx = w / 2f;
|
||||||
|
// The trunk, then three tiers, each with a darker underside.
|
||||||
|
r.set(cx - 22f * u, 480f * u, cx + 22f * u, 526f * u);
|
||||||
|
fill.setColor(WOOD);
|
||||||
|
c.drawRect(r, fill);
|
||||||
|
tier(c, cx, 118f * u, 238f * u, 96f * u, u);
|
||||||
|
tier(c, cx, 190f * u, 358f * u, 136f * u, u);
|
||||||
|
tier(c, cx, 270f * u, 492f * u, 182f * u, u);
|
||||||
|
// The star on top, flaring with the music.
|
||||||
|
float sy = 104f * u, flare = 1f + pulse * 0.5f;
|
||||||
|
glow.setShader(new RadialGradient(cx, sy, 60f * u * flare,
|
||||||
|
new int[] { AccentColor.withAlpha(GOLD, 0x55 + (int) (pulse * 0x80)), AccentColor.withAlpha(GOLD, 0) }, null,
|
||||||
|
Shader.TileMode.CLAMP));
|
||||||
|
c.drawCircle(cx, sy, 60f * u * flare, glow);
|
||||||
|
path.reset();
|
||||||
|
for (int i = 0; i < 10; i++) {
|
||||||
|
double a = -Math.PI / 2 + Math.PI * i / 5;
|
||||||
|
float rad = (i % 2 == 0 ? 24f : 10f) * u;
|
||||||
|
float x = cx + (float) Math.cos(a) * rad, y = sy + (float) Math.sin(a) * rad;
|
||||||
|
if (i == 0) path.moveTo(x, y);
|
||||||
|
else path.lineTo(x, y);
|
||||||
|
}
|
||||||
|
path.close();
|
||||||
|
fill.setColor(GOLD);
|
||||||
|
c.drawPath(path, fill);
|
||||||
|
// The lights: strung along each tier's lower edge, steady, twinkling a little while driving,
|
||||||
|
// flaring together on the beat.
|
||||||
|
float[][] strings = { { 238f, 96f, 5 }, { 358f, 136f, 7 }, { 492f, 182f, 10 } };
|
||||||
|
int i = 0;
|
||||||
|
for (float[] st : strings) {
|
||||||
|
int n = (int) st[2];
|
||||||
|
for (int k = 0; k < n; k++, i++) {
|
||||||
|
float x = cx - st[1] * 0.85f * u + k * (1.7f * st[1] * u) / (n - 1);
|
||||||
|
float y = (st[0] - 16f + (k % 2) * 9f) * u;
|
||||||
|
int colour = LIGHTS[i % LIGHTS.length];
|
||||||
|
float twinkle = 0.5f + 0.5f * (float) Math.sin(time * 3f + i * 1.7f);
|
||||||
|
int alpha = Math.min(0xFF, 0x58 + (int) (twinkle * 0x38) + (int) (pulse * 0x90));
|
||||||
|
fill.setColor(AccentColor.withAlpha(colour, alpha));
|
||||||
|
c.drawCircle(x, y, (11f + pulse * 9f) * u, fill);
|
||||||
|
fill.setColor(brighter(colour, pulse * 0.4f));
|
||||||
|
c.drawCircle(x, y, 4.8f * u * (1f + pulse * 0.35f), fill);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void tier(Canvas c, float cx, float apex, float base, float half, float u) {
|
||||||
|
for (int pass = 0; pass < 2; pass++) {
|
||||||
|
float dy = pass == 0 ? 7f * u : 0f;
|
||||||
|
path.reset();
|
||||||
|
path.moveTo(cx, apex + dy);
|
||||||
|
path.lineTo(cx + half, base - 6f * u + dy);
|
||||||
|
int scallops = 5;
|
||||||
|
float sw = half * 2f / scallops;
|
||||||
|
for (int i = scallops; i > 0; i--) {
|
||||||
|
float x = cx - half + i * sw;
|
||||||
|
path.quadTo(x - sw / 2f, base + 12f * u + dy, x - sw, base - 6f * u + dy);
|
||||||
|
}
|
||||||
|
path.close();
|
||||||
|
fill.setColor(pass == 0 ? PINE_DARK : PINE);
|
||||||
|
c.drawPath(path, fill);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** The colour moved {@code share} of the way to white. */
|
||||||
|
private static int brighter(int colour, float share) {
|
||||||
|
int r = (colour >> 16) & 0xFF, g = (colour >> 8) & 0xFF, b = colour & 0xFF;
|
||||||
|
return 0xFF000000 | (r + (int) ((255 - r) * share)) << 16 | (g + (int) ((255 - g) * share)) << 8
|
||||||
|
| (b + (int) ((255 - b) * share));
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------- The fairy lights over the tree: the power ----------
|
||||||
|
|
||||||
|
private void drawArch(Canvas c, int w, float u, float pulse) {
|
||||||
|
float cx = w / 2f, cy = 348f * u, radX = 272f * u, radY = 244f * u;
|
||||||
|
int lit = Tally.ofPower(v.powerKw, 150f, 50f, BULBS);
|
||||||
|
boolean regen = regen();
|
||||||
|
// The wire.
|
||||||
|
line.setColor(PINE_DARK);
|
||||||
|
line.setStrokeWidth(3f * u);
|
||||||
|
path.reset();
|
||||||
|
for (int i = 0; i <= 48; i++) {
|
||||||
|
double a = Math.PI + Math.PI * i / 48.0;
|
||||||
|
float x = cx + (float) Math.cos(a) * radX, y = cy + (float) Math.sin(a) * radY;
|
||||||
|
if (i == 0) path.moveTo(x, y);
|
||||||
|
else path.lineTo(x, y);
|
||||||
|
}
|
||||||
|
c.drawPath(path, line);
|
||||||
|
for (int i = 0; i < BULBS; i++) {
|
||||||
|
// Left to right over the top; regeneration comes back from the right.
|
||||||
|
int k = regen ? BULBS - 1 - i : i;
|
||||||
|
double a = Math.PI + Math.PI * k / (BULBS - 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 ? ICE : GOLD, 0x44 + (int) (pulse * 0x50)));
|
||||||
|
c.drawCircle(x, y, (15f + pulse * 6f) * u, fill);
|
||||||
|
}
|
||||||
|
fill.setColor(on ? brighter(regen ? ICE : GOLD, pulse * 0.4f) : 0x38FFFFFF);
|
||||||
|
c.drawCircle(x, y, 7.5f * u, fill);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------- The big bauble: the speed ----------
|
||||||
|
|
||||||
|
private void drawBauble(Canvas c, int w, float u, float pulse) {
|
||||||
|
float cx = w / 2f, cy = 372f * u, rad = 88f * u;
|
||||||
|
glow.setShader(new RadialGradient(cx, cy, rad * (1.6f + pulse * 0.2f),
|
||||||
|
new int[] { AccentColor.withAlpha(RED, 0x40 + (int) (pulse * 0x50)), AccentColor.withAlpha(RED, 0) }, null,
|
||||||
|
Shader.TileMode.CLAMP));
|
||||||
|
c.drawCircle(cx, cy, rad * (1.6f + pulse * 0.2f), glow);
|
||||||
|
// The cap and its loop.
|
||||||
|
line.setColor(GOLD);
|
||||||
|
line.setStrokeWidth(3f * u);
|
||||||
|
r.set(cx - 12f * u, cy - rad - 30f * u, cx + 12f * u, cy - rad - 6f * u);
|
||||||
|
c.drawOval(r, line);
|
||||||
|
glow.setShader(new RadialGradient(cx - rad * 0.3f, cy - rad * 0.35f, rad * 1.3f,
|
||||||
|
new int[] { 0xFFFF7A82, RED, 0xFF8E1623 }, new float[] { 0f, 0.5f, 1f }, Shader.TileMode.CLAMP));
|
||||||
|
c.drawCircle(cx, cy, rad, glow);
|
||||||
|
r.set(cx - 16f * u, cy - rad - 12f * u, cx + 16f * u, cy - rad + 8f * u);
|
||||||
|
fill.setColor(GOLD);
|
||||||
|
c.drawRect(r, fill);
|
||||||
|
// A gold band round the middle's lower half, and a highlight.
|
||||||
|
line.setColor(0x88FFD04A);
|
||||||
|
line.setStrokeWidth(3f * u);
|
||||||
|
r.set(cx - rad + 6f * u, cy + 34f * u, cx + rad - 6f * u, cy + rad - 4f * u);
|
||||||
|
c.drawArc(r, 20f, 140f, false, line);
|
||||||
|
fill.setColor(0x55FFFFFF);
|
||||||
|
r.set(cx - rad * 0.62f, cy - rad * 0.72f, cx - rad * 0.2f, cy - rad * 0.42f);
|
||||||
|
c.drawOval(r, fill);
|
||||||
|
label(c, fmt(v.speedKmh, "%.0f", "—"), cx, cy + 20f * u, 84f * u, BONE, serif, Paint.Align.CENTER);
|
||||||
|
label(c, "KM/H", cx, cy + 50f * u, 16f * u, 0xCCFFE9EA, face, Paint.Align.CENTER);
|
||||||
|
// The power under the tree, above the drift.
|
||||||
|
boolean regen = regen();
|
||||||
|
String power = v.powerKw == null ? "— kW" : String.format(Locale.US, regen ? "+%.1f kW" : "%.1f kW",
|
||||||
|
Math.abs(v.powerKw));
|
||||||
|
outlined(c, power, cx, 556f * u, 34f * u, regen ? ICE : BONE, face, Paint.Align.CENTER, u);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------- The wreath: the clock ----------
|
||||||
|
|
||||||
|
private void drawWreath(Canvas c, int w, float u) {
|
||||||
|
float rad = 74f * u, cx = w - 100f * u - rad * 0.2f, cy = 96f * u;
|
||||||
|
line.setColor(PINE);
|
||||||
|
line.setStrokeWidth(24f * u);
|
||||||
|
c.drawCircle(cx, cy, rad, line);
|
||||||
|
line.setColor(PINE_DARK);
|
||||||
|
line.setStrokeWidth(5f * u);
|
||||||
|
c.drawCircle(cx, cy, rad - 8f * u, line);
|
||||||
|
fill.setColor(0xFF2E9C52);
|
||||||
|
for (int i = 0; i < 16; i++) {
|
||||||
|
double a = Math.PI * 2 * i / 16;
|
||||||
|
c.drawCircle(cx + (float) Math.cos(a) * rad, cy + (float) Math.sin(a) * rad, 8f * u, fill);
|
||||||
|
}
|
||||||
|
fill.setColor(RED);
|
||||||
|
for (int i = 0; i < 9; i++) {
|
||||||
|
double a = Math.PI * 2 * i / 9 + 0.3;
|
||||||
|
c.drawCircle(cx + (float) Math.cos(a) * rad, cy + (float) Math.sin(a) * rad, 4.5f * u, fill);
|
||||||
|
}
|
||||||
|
// The bow at the foot.
|
||||||
|
path.reset();
|
||||||
|
float bx = cx, by = cy + rad;
|
||||||
|
path.moveTo(bx, by);
|
||||||
|
path.lineTo(bx - 28f * u, by - 14f * u);
|
||||||
|
path.lineTo(bx - 28f * u, by + 14f * u);
|
||||||
|
path.close();
|
||||||
|
path.moveTo(bx, by);
|
||||||
|
path.lineTo(bx + 28f * u, by - 14f * u);
|
||||||
|
path.lineTo(bx + 28f * u, by + 14f * u);
|
||||||
|
path.close();
|
||||||
|
fill.setColor(RED);
|
||||||
|
c.drawPath(path, fill);
|
||||||
|
c.drawCircle(bx, by, 7f * u, fill);
|
||||||
|
// The clock face inside.
|
||||||
|
fill.setColor(0xFFFFF6E0);
|
||||||
|
c.drawCircle(cx, cy, rad - 18f * u, fill);
|
||||||
|
line.setColor(GOLD);
|
||||||
|
line.setStrokeWidth(3f * u);
|
||||||
|
c.drawCircle(cx, cy, rad - 18f * u, line);
|
||||||
|
label(c, v.clock.isEmpty() ? "—" : v.clock, cx, cy + 14f * u, 40f * u, INK, serif, Paint.Align.CENTER);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------- Gift tags: trip, usage, climate, odometer ----------
|
||||||
|
|
||||||
|
private void drawTags(Canvas c, float u) {
|
||||||
|
float l = 28f * u, rr = l + 300f * u, t = 44f * u, tag = 34f * u, step = 38f * u;
|
||||||
|
// The garland they hang from.
|
||||||
|
line.setColor(PINE);
|
||||||
|
line.setStrokeWidth(8f * u);
|
||||||
|
c.drawLine(l + 10f * u, 20f * u, rr - 10f * u, 20f * u, line);
|
||||||
|
fill.setColor(RED);
|
||||||
|
for (int i = 0; i < 7; i++) c.drawCircle(l + 24f * u + i * 44f * u, 20f * u, 4f * u, fill);
|
||||||
|
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 top = t + i * step;
|
||||||
|
// A tag with a clipped corner and a hole, hung on a thread from the one above.
|
||||||
|
path.reset();
|
||||||
|
path.moveTo(l + 18f * u, top);
|
||||||
|
path.lineTo(rr, top);
|
||||||
|
path.lineTo(rr, top + tag);
|
||||||
|
path.lineTo(l + 18f * u, top + tag);
|
||||||
|
path.lineTo(l, top + tag / 2f);
|
||||||
|
path.close();
|
||||||
|
fill.setColor(i % 2 == 0 ? 0xFFF6EBD0 : 0xFFEAD9B6);
|
||||||
|
c.drawPath(path, fill);
|
||||||
|
line.setColor(RED);
|
||||||
|
line.setStrokeWidth(2f * u);
|
||||||
|
c.drawPath(path, line);
|
||||||
|
line.setColor(0xCCFFFFFF);
|
||||||
|
line.setStrokeWidth(1.5f * u);
|
||||||
|
c.drawLine(l + 12f * u, top - (i == 0 ? 20f * u - 0f : step - tag), l + 12f * u, top + tag / 2f, line);
|
||||||
|
fill.setColor(0xFF2A1A10);
|
||||||
|
c.drawCircle(l + 12f * u, top + tag / 2f, 3f * u, fill);
|
||||||
|
label(c, rows[i][0], l + 28f * u, top + 23f * u, 17f * u, 0xFF8E1623, thin, Paint.Align.LEFT);
|
||||||
|
label(c, rows[i][1], rr - 12f * u, top + 25f * u, 22f * u, INK, face, Paint.Align.RIGHT);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------- The mug of cocoa: the battery ----------
|
||||||
|
|
||||||
|
private void drawMug(Canvas c, float u) {
|
||||||
|
float cx = 178f * u, l = cx - 92f * u, rr = cx + 92f * u, top = 330f * u, bottom = 560f * u;
|
||||||
|
boolean low = v.socPercent != null && v.socPercent < 20f;
|
||||||
|
boolean blinkOn = (SystemClock.uptimeMillis() / 500) % 2 == 0;
|
||||||
|
float corner = 22f * u, inner = 6f * u;
|
||||||
|
// The handle.
|
||||||
|
line.setColor(0xFFCFE6F0);
|
||||||
|
line.setStrokeWidth(14f * u);
|
||||||
|
r.set(rr - 26f * u, top + 50f * u, rr + 40f * u, top + 170f * u);
|
||||||
|
c.drawArc(r, -90f, 180f, false, line);
|
||||||
|
// The glass.
|
||||||
|
r.set(l, top, rr, bottom);
|
||||||
|
fill.setColor(0x30CFE6F0);
|
||||||
|
c.drawRoundRect(r, corner, corner, fill);
|
||||||
|
if (v.socPercent != null) {
|
||||||
|
float share = Math.max(0f, Math.min(1f, v.socPercent / 100f));
|
||||||
|
float level = bottom - inner - share * (bottom - top - 2f * inner);
|
||||||
|
c.save();
|
||||||
|
r.set(l + inner, top + inner, rr - inner, bottom - inner);
|
||||||
|
path.reset();
|
||||||
|
path.addRoundRect(r, corner - inner, corner - inner, Path.Direction.CW);
|
||||||
|
c.clipPath(path);
|
||||||
|
fill.setColor(low ? COLD : COCOA);
|
||||||
|
c.drawRect(l, level, rr, bottom, fill);
|
||||||
|
fill.setColor(0x33FFFFFF);
|
||||||
|
c.drawRect(l, level, rr, level + 4f * u, fill);
|
||||||
|
c.restore();
|
||||||
|
// Marshmallows float on top, bobbing while driving.
|
||||||
|
for (int i = 0; i < 3; i++) {
|
||||||
|
float mx = cx + (i - 1) * 50f * u, my = level - 8f * u + (float) Math.sin(time * 2f + i * 2f) * 2f * u;
|
||||||
|
r.set(mx - 14f * u, my - 12f * u, mx + 14f * u, my + 8f * u);
|
||||||
|
fill.setColor(BONE);
|
||||||
|
c.drawRoundRect(r, 6f * u, 6f * u, fill);
|
||||||
|
}
|
||||||
|
// Steam.
|
||||||
|
line.setColor(0x66FFFFFF);
|
||||||
|
line.setStrokeWidth(4f * u);
|
||||||
|
for (int i = 0; i < 3; i++) {
|
||||||
|
float sx = cx + (i - 1) * 44f * u;
|
||||||
|
path.reset();
|
||||||
|
path.moveTo(sx, level - 24f * u);
|
||||||
|
for (int k = 1; k <= 6; k++) {
|
||||||
|
path.lineTo(sx + (float) Math.sin(time * 2.5f + i + k * 0.9f) * 9f * u, level - 24f * u - k * 11f * u);
|
||||||
|
}
|
||||||
|
c.drawPath(path, line);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
line.setColor(0xFFCFE6F0);
|
||||||
|
line.setStrokeWidth(4f * u);
|
||||||
|
r.set(l, top, rr, bottom);
|
||||||
|
c.drawRoundRect(r, corner, corner, line);
|
||||||
|
String number = fmt(v.socPercent, "%.0f %%", "—");
|
||||||
|
outlined(c, number, cx, 470f * u, 58f * u, low ? 0xFFFF9AA3 : BONE, serif, Paint.Align.CENTER, u);
|
||||||
|
if (low && blinkOn) outlined(c, "BATTERY LOW", cx, 508f * u, 21f * u, BONE, face, Paint.Align.CENTER, u);
|
||||||
|
else if (!low) outlined(c, "BATTERY", cx, 508f * u, 19f * u, GOLD, face, Paint.Align.CENTER, u);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------- The signboard and candy canes: the range; baubles: the gears ----------
|
||||||
|
|
||||||
|
private void drawBoard(Canvas c, int w, float u) {
|
||||||
|
float rr = w - 40f * u, l = rr - 250f * u, t = 214f * u, b = 394f * u, mid = (l + rr) / 2f;
|
||||||
|
r.set(l, t, rr, b);
|
||||||
|
fill.setColor(WOOD);
|
||||||
|
c.drawRoundRect(r, 10f * u, 10f * u, fill);
|
||||||
|
line.setColor(0xFF8A5A33);
|
||||||
|
line.setStrokeWidth(3f * u);
|
||||||
|
c.drawRoundRect(r, 10f * u, 10f * u, line);
|
||||||
|
// Snow on top.
|
||||||
|
path.reset();
|
||||||
|
path.moveTo(l - 4f * u, t + 4f * u);
|
||||||
|
path.quadTo(l + 40f * u, t - 24f * u, l + 90f * u, t - 4f * u);
|
||||||
|
path.quadTo(l + 140f * u, t - 28f * u, l + 190f * u, t - 6f * u);
|
||||||
|
path.quadTo(l + 230f * u, t - 16f * u, rr + 4f * u, t + 4f * u);
|
||||||
|
path.lineTo(rr + 4f * u, t + 12f * u);
|
||||||
|
path.lineTo(l - 4f * u, t + 12f * u);
|
||||||
|
path.close();
|
||||||
|
fill.setColor(SNOW);
|
||||||
|
c.drawPath(path, fill);
|
||||||
|
label(c, "RANGE", mid, t + 52f * u, 20f * u, GOLD, face, Paint.Align.CENTER);
|
||||||
|
label(c, fmt(v.rangeKm, "%.0f", "—"), mid, t + 128f * u, 80f * u, BONE, serif, Paint.Align.CENTER);
|
||||||
|
label(c, "KM", mid, t + 160f * u, 20f * u, GOLD, face, Paint.Align.CENTER);
|
||||||
|
// A candy cane per 50 km in the snow below.
|
||||||
|
int lit = Tally.lit(v.rangeKm, 50f, CANES);
|
||||||
|
for (int i = 0; i < CANES; i++) {
|
||||||
|
cane(c, l + 18f * u + i * 30f * u, 488f * u, u, i < lit);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void cane(Canvas c, float x, float foot, float u, boolean on) {
|
||||||
|
path.reset();
|
||||||
|
path.moveTo(x, foot);
|
||||||
|
path.lineTo(x, foot - 58f * u);
|
||||||
|
path.cubicTo(x, foot - 78f * u, x + 18f * u, foot - 78f * u, x + 18f * u, foot - 62f * u);
|
||||||
|
line.setStrokeWidth(9f * u);
|
||||||
|
line.setColor(on ? 0xFFFFFFFF : 0x40FFFFFF);
|
||||||
|
c.drawPath(path, line);
|
||||||
|
line.setColor(on ? RED : 0x40E53946);
|
||||||
|
line.setPathEffect(new DashPathEffect(new float[] { 7f * u, 7f * u }, 0f));
|
||||||
|
c.drawPath(path, line);
|
||||||
|
line.setPathEffect(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void drawGears(Canvas c, int w, float u) {
|
||||||
|
float rr = w - 40f * u, l = rr - 250f * u, cy = 540f * u, rad = 25f * u;
|
||||||
|
String[] gears = { "P", "R", "N", "D" };
|
||||||
|
String now = Gear.label(v.gear);
|
||||||
|
float step = (rr - l) / 4f;
|
||||||
|
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(0x55FFD04A);
|
||||||
|
c.drawCircle(mid, cy, 42f * u, fill);
|
||||||
|
}
|
||||||
|
fill.setColor(GOLD);
|
||||||
|
r.set(mid - 7f * u, cy - rad - 9f * u, mid + 7f * u, cy - rad + 3f * u);
|
||||||
|
c.drawRect(r, fill);
|
||||||
|
fill.setColor(on ? GOLD : 0xFF7A1F2A);
|
||||||
|
c.drawCircle(mid, cy, rad, fill);
|
||||||
|
line.setColor(on ? 0xFFFFF0B0 : 0xFFB03A48);
|
||||||
|
line.setStrokeWidth(2f * u);
|
||||||
|
c.drawCircle(mid, cy, rad, line);
|
||||||
|
label(c, gears[i], mid, cy + 11f * u, 30f * u, on ? INK : 0xB0FFFFFF, serif, Paint.Align.CENTER);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------- What plays: a sleigh or an angel tows it on wrapping paper ----------
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Santa's sleigh and an angel take turns towing a waving strip of wrapping paper with the song
|
||||||
|
* across the sky above the tree, behind the wreath and the tags, again and again while
|
||||||
|
* something plays. The paper's colour is fixed by the song. Returns whether it is drawn, so the
|
||||||
|
* view keeps animating for it.
|
||||||
|
*/
|
||||||
|
private boolean drawSong(Canvas c, int w, float u) {
|
||||||
|
if (v.songTitle == null) return false;
|
||||||
|
text.setTypeface(face);
|
||||||
|
text.setTextSize(19f * u);
|
||||||
|
text.setStyle(Paint.Style.FILL);
|
||||||
|
String s = SongBanner.line(text, v.songTitle, v.songArtist, Math.min(560f * u, w * 0.45f));
|
||||||
|
float t = SystemClock.uptimeMillis() / 1000f;
|
||||||
|
float bw = text.measureText(s) + 40f * u, bh = 28f * u, tow = 80f * u;
|
||||||
|
float bx = SongBanner.left(t, w, bw, tow, u);
|
||||||
|
float yc = 66f * u + (float) Math.sin(t * 1.3f) * 3f * u;
|
||||||
|
int paper = PAPER[Math.floorMod((v.songTitle + "\n" + v.songArtist).hashCode(), PAPER.length)];
|
||||||
|
boolean darkInk = paper == PAPER[3] || paper == PAPER[4];
|
||||||
|
banner.draw(c, fill, line, text, s, bx, yc, bw, bh, t, u, 0f, paper, 0xCCFFFFFF, darkInk ? INK : 0xFFFFFFFF);
|
||||||
|
boolean sleigh = SongBanner.pass(t, w, bw, tow, u) % 2 == 0;
|
||||||
|
float right = bx + bw, cx = right + tow, cy = yc - 6f * u, size = (sleigh ? 70f : 56f) * u;
|
||||||
|
float[] tail = sleigh ? SongCarriers.sleighTail : SongCarriers.angelTail;
|
||||||
|
float ax = cx + tail[0] * size, ay = cy + tail[1] * size;
|
||||||
|
line.setColor(0xCCFFFFFF);
|
||||||
|
line.setStrokeWidth(1.5f * u);
|
||||||
|
c.drawLine(right, yc - bh / 2f + 3f * u, ax, ay, line);
|
||||||
|
c.drawLine(right, yc + bh / 2f - 3f * u, ax, ay, line);
|
||||||
|
if (sleigh) SongCarriers.sleigh(c, fill, line, path, r, cx, cy, size, t, u);
|
||||||
|
else SongCarriers.angel(c, fill, line, path, r, cx, cy, size, (float) Math.sin(t * 7f), u);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -217,9 +217,9 @@ public class MainActivity extends AppCompatActivity {
|
|||||||
|
|
||||||
/** Whether the Overview hides the navigation (the button in the navigation's header). */
|
/** Whether the Overview hides the navigation (the button in the navigation's header). */
|
||||||
static final String PREF_OVERVIEW_FULLSCREEN = "overview_fullscreen";
|
static final String PREF_OVERVIEW_FULLSCREEN = "overview_fullscreen";
|
||||||
/** The Overview shows what plays (Tiles, Halloween); on by default. */
|
/** The Overview shows what plays (the styles that have a place for it); on by default. */
|
||||||
static final String PREF_NOW_PLAYING = "now_playing";
|
static final String PREF_NOW_PLAYING = "now_playing";
|
||||||
/** The Halloween pumpkin's light follows the music; off by default, so the analyser stays off. */
|
/** The Halloween pumpkin's and the Christmas tree's light follows the music; off by default, so the analyser stays off. */
|
||||||
static final String PREF_MUSIC_LIGHT = "music_light";
|
static final String PREF_MUSIC_LIGHT = "music_light";
|
||||||
|
|
||||||
/** True while the Overview is full screen: the navigation hidden, the edge swipe armed. */
|
/** True while the Overview is full screen: the navigation hidden, the edge swipe armed. */
|
||||||
@@ -364,10 +364,13 @@ public class MainActivity extends AppCompatActivity {
|
|||||||
HalloweenView halloweenView = new HalloweenView(this);
|
HalloweenView halloweenView = new HalloweenView(this);
|
||||||
musicTap = new MusicTap(this);
|
musicTap = new MusicTap(this);
|
||||||
nowPlaying = new NowPlayingWatcher(this);
|
nowPlaying = new NowPlayingWatcher(this);
|
||||||
|
ChristmasView christmasView = new ChristmasView(this);
|
||||||
halloweenView.setMusic(musicTap);
|
halloweenView.setMusic(musicTap);
|
||||||
|
christmasView.setMusic(musicTap);
|
||||||
java.util.Collections.addAll(canvasStyles, new MinimalView(this), terminalView,
|
java.util.Collections.addAll(canvasStyles, new MinimalView(this), terminalView,
|
||||||
new NixieView(this), new ChronographView(this), gridView, gridMapView, new TimeCircuitsView(this),
|
new NixieView(this), new ChronographView(this), gridView, gridMapView, new TimeCircuitsView(this),
|
||||||
new QuestView(this), visorView, new EmpireView(this), new MuertosView(this), halloweenView);
|
new QuestView(this), visorView, new EmpireView(this), new MuertosView(this), halloweenView,
|
||||||
|
christmasView);
|
||||||
View[] canvasPanes = new View[canvasStyles.size()];
|
View[] canvasPanes = new View[canvasStyles.size()];
|
||||||
for (int i = 0; i < canvasPanes.length; i++) canvasPanes[i] = drawnPane(canvasStyles.get(i));
|
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.
|
// Grid with map: its map layer goes under it, in the same pane.
|
||||||
@@ -1787,7 +1790,8 @@ public class MainActivity extends AppCompatActivity {
|
|||||||
private static boolean showsNowPlaying(OverviewStyle style) {
|
private static boolean showsNowPlaying(OverviewStyle style) {
|
||||||
return style == OverviewStyle.TILES || style == OverviewStyle.HALLOWEEN
|
return style == OverviewStyle.TILES || style == OverviewStyle.HALLOWEEN
|
||||||
|| style == OverviewStyle.MUERTOS || style == OverviewStyle.QUEST || style == OverviewStyle.EMPIRE
|
|| style == OverviewStyle.MUERTOS || style == OverviewStyle.QUEST || style == OverviewStyle.EMPIRE
|
||||||
|| style == OverviewStyle.TERMINAL || style == OverviewStyle.SCIFI;
|
|| style == OverviewStyle.TERMINAL || style == OverviewStyle.SCIFI
|
||||||
|
|| style == OverviewStyle.CHRISTMAS;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** The media sessions are watched only while what plays can be seen, and the switch is on. */
|
/** The media sessions are watched only while what plays can be seen, and the switch is on. */
|
||||||
@@ -1835,13 +1839,18 @@ public class MainActivity extends AppCompatActivity {
|
|||||||
if (!android.text.TextUtils.equals(view.getText(), text)) view.setText(text);
|
if (!android.text.TextUtils.equals(view.getText(), text)) view.setText(text);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** The styles with a light that can follow the music: Halloween's pumpkin and Christmas's tree. */
|
||||||
|
private static boolean hasMusicLight(OverviewStyle style) {
|
||||||
|
return style == OverviewStyle.HALLOWEEN || style == OverviewStyle.CHRISTMAS;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The pumpkin listens only while it can be seen (the app in front, on the Overview, in
|
* The light listens only while it can be seen (the app in front, on the Overview, in
|
||||||
* Halloween) and only when "Pumpkin light follows the music" is on.
|
* Halloween or Christmas) and only when "Light follows the music" is on.
|
||||||
*/
|
*/
|
||||||
private void updateMusicTap() {
|
private void updateMusicTap() {
|
||||||
ViewPager2 pager = findViewById(R.id.content);
|
ViewPager2 pager = findViewById(R.id.content);
|
||||||
musicTap.setRunning(inFront && overviewStyle() == OverviewStyle.HALLOWEEN
|
musicTap.setRunning(inFront && hasMusicLight(overviewStyle())
|
||||||
&& prefs.getBoolean(PREF_MUSIC_LIGHT, false)
|
&& prefs.getBoolean(PREF_MUSIC_LIGHT, false)
|
||||||
&& pager != null && pager.getCurrentItem() == 0);
|
&& pager != null && pager.getCurrentItem() == 0);
|
||||||
}
|
}
|
||||||
@@ -1909,7 +1918,7 @@ public class MainActivity extends AppCompatActivity {
|
|||||||
int playing = showsNowPlaying(style) ? View.VISIBLE : View.GONE;
|
int playing = showsNowPlaying(style) ? View.VISIBLE : View.GONE;
|
||||||
settingsPane.findViewById(R.id.now_playing_switch).setVisibility(playing);
|
settingsPane.findViewById(R.id.now_playing_switch).setVisibility(playing);
|
||||||
settingsPane.findViewById(R.id.now_playing_hint).setVisibility(playing);
|
settingsPane.findViewById(R.id.now_playing_hint).setVisibility(playing);
|
||||||
int music = style == OverviewStyle.HALLOWEEN ? View.VISIBLE : View.GONE;
|
int music = hasMusicLight(style) ? View.VISIBLE : View.GONE;
|
||||||
settingsPane.findViewById(R.id.music_light_switch).setVisibility(music);
|
settingsPane.findViewById(R.id.music_light_switch).setVisibility(music);
|
||||||
settingsPane.findViewById(R.id.music_light_hint).setVisibility(music);
|
settingsPane.findViewById(R.id.music_light_hint).setVisibility(music);
|
||||||
}
|
}
|
||||||
@@ -1927,7 +1936,8 @@ public class MainActivity extends AppCompatActivity {
|
|||||||
getString(R.string.overview_style_grid_map),
|
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_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) };
|
getString(R.string.overview_style_muertos), getString(R.string.overview_style_halloween),
|
||||||
|
getString(R.string.overview_style_christmas) };
|
||||||
}
|
}
|
||||||
|
|
||||||
/** The style drop-down; every style is fed on every reading, only the chosen one is shown. */
|
/** The style drop-down; every style is fed on every reading, only the chosen one is shown. */
|
||||||
|
|||||||
@@ -33,7 +33,8 @@ enum OverviewStyle {
|
|||||||
VISOR(true, true),
|
VISOR(true, true),
|
||||||
EMPIRE(false, false),
|
EMPIRE(false, false),
|
||||||
MUERTOS(false, false),
|
MUERTOS(false, false),
|
||||||
HALLOWEEN(false, false);
|
HALLOWEEN(false, false),
|
||||||
|
CHRISTMAS(false, false);
|
||||||
|
|
||||||
static final String PREF_KEY = "overview_style";
|
static final String PREF_KEY = "overview_style";
|
||||||
|
|
||||||
|
|||||||
@@ -7,14 +7,16 @@ import android.graphics.RectF;
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* The creatures that tow a {@link SongBanner} in the styles with a sky for it, drawn flying right
|
* The creatures that tow a {@link SongBanner} in the styles with a sky for it, drawn flying right
|
||||||
* with their centre at (x, y) and about {@code s} wide: a dragon (Quest) and an eagle (Empire).
|
* with their centre at (x, y) and about {@code s} wide: a dragon (Quest), an eagle (Empire), Santa's
|
||||||
|
* sleigh with a reindeer and an angel (Christmas).
|
||||||
* {@code flap} runs from −1 to 1 with the wing beat, {@code t} is the time in seconds. The caller
|
* {@code flap} runs from −1 to 1 with the wing beat, {@code t} is the time in seconds. The caller
|
||||||
* passes its own paints and scratch objects; the paints' colour and width are set here. Where the
|
* passes its own paints and scratch objects; the paints' colour and width are set here. Where the
|
||||||
* tow lines tie on is {@link #dragonTail} and {@link #eagleTail}, as offsets in units of {@code s}.
|
* tow lines tie on is {@link #dragonTail}, {@link #eagleTail}, {@link #sleighTail} and {@link #angelTail}, as offsets in units of {@code s}.
|
||||||
*/
|
*/
|
||||||
final class SongCarriers {
|
final class SongCarriers {
|
||||||
|
|
||||||
static final float[] dragonTail = { -1.0f, -0.25f }, eagleTail = { -0.55f, 0.05f };
|
static final float[] dragonTail = { -1.0f, -0.25f }, eagleTail = { -0.55f, 0.05f },
|
||||||
|
sleighTail = { -0.62f, 0f }, angelTail = { -0.45f, 0.1f };
|
||||||
|
|
||||||
private SongCarriers() {}
|
private SongCarriers() {}
|
||||||
|
|
||||||
@@ -134,4 +136,132 @@ final class SongCarriers {
|
|||||||
c.drawLine(0.05f * s, 0.13f * s, 0.02f * s, 0.26f * s, line);
|
c.drawLine(0.05f * s, 0.13f * s, 0.02f * s, 0.26f * s, line);
|
||||||
c.restore();
|
c.restore();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** A reindeer with a glowing nose pulling Santa's sleigh, drawn about 1.7 s wide. */
|
||||||
|
static void sleigh(Canvas c, Paint fill, Paint line, Path path, RectF r, float x, float y, float s,
|
||||||
|
float t, float u) {
|
||||||
|
final int brown = 0xFF8A5A33, red = 0xFFC62828, gold = 0xFFFFD04A;
|
||||||
|
c.save();
|
||||||
|
c.translate(x, y);
|
||||||
|
// The reindeer: four legs trotting, a body, a neck, a head with antlers and a red nose.
|
||||||
|
float swing = (float) Math.sin(t * 8f) * 0.18f;
|
||||||
|
line.setColor(brown);
|
||||||
|
line.setStrokeWidth(3f * u);
|
||||||
|
c.drawLine(0.72f * s, 0.05f * s, (0.72f + swing) * s, 0.3f * s, line);
|
||||||
|
c.drawLine(0.66f * s, 0.05f * s, (0.66f - swing) * s, 0.3f * s, line);
|
||||||
|
c.drawLine(0.44f * s, 0.05f * s, (0.44f - swing) * s, 0.3f * s, line);
|
||||||
|
c.drawLine(0.38f * s, 0.05f * s, (0.38f + swing) * s, 0.3f * s, line);
|
||||||
|
r.set(0.32f * s, -0.15f * s, 0.8f * s, 0.1f * s);
|
||||||
|
fill.setColor(brown);
|
||||||
|
c.drawOval(r, fill);
|
||||||
|
line.setStrokeWidth(0.1f * s);
|
||||||
|
c.drawLine(0.76f * s, -0.08f * s, 0.86f * s, -0.2f * s, line);
|
||||||
|
c.drawCircle(0.88f * s, -0.22f * s, 0.075f * s, fill);
|
||||||
|
line.setColor(0xFFD9C08A);
|
||||||
|
line.setStrokeWidth(2f * u);
|
||||||
|
c.drawLine(0.85f * s, -0.28f * s, 0.78f * s, -0.46f * s, line);
|
||||||
|
c.drawLine(0.82f * s, -0.37f * s, 0.9f * s, -0.4f * s, line);
|
||||||
|
c.drawLine(0.9f * s, -0.28f * s, 0.97f * s, -0.46f * s, line);
|
||||||
|
c.drawLine(0.94f * s, -0.37f * s, 0.86f * s, -0.42f * s, line);
|
||||||
|
float glow = 0.75f + 0.25f * (float) Math.sin(t * 6f);
|
||||||
|
fill.setColor(0x55FF3B30);
|
||||||
|
c.drawCircle(0.97f * s, -0.2f * s, 0.1f * s * glow, fill);
|
||||||
|
fill.setColor(0xFFFF3B30);
|
||||||
|
c.drawCircle(0.96f * s, -0.2f * s, 0.04f * s, fill);
|
||||||
|
// The harness, and the sleigh: a red body with a curled front on a gold runner.
|
||||||
|
line.setColor(gold);
|
||||||
|
line.setStrokeWidth(1.5f * u);
|
||||||
|
c.drawLine(0.36f * s, -0.02f * s, 0.12f * s, -0.02f * s, line);
|
||||||
|
path.reset();
|
||||||
|
path.moveTo(-0.55f * s, -0.12f * s);
|
||||||
|
path.lineTo(0.05f * s, -0.12f * s);
|
||||||
|
path.quadTo(0.22f * s, -0.12f * s, 0.2f * s, -0.28f * s);
|
||||||
|
path.lineTo(0.12f * s, 0.1f * s);
|
||||||
|
path.lineTo(-0.5f * s, 0.1f * s);
|
||||||
|
path.quadTo(-0.64f * s, 0.08f * s, -0.6f * s, -0.12f * s);
|
||||||
|
path.close();
|
||||||
|
fill.setColor(red);
|
||||||
|
c.drawPath(path, fill);
|
||||||
|
line.setColor(gold);
|
||||||
|
line.setStrokeWidth(2f * u);
|
||||||
|
c.drawPath(path, line);
|
||||||
|
path.reset();
|
||||||
|
path.moveTo(-0.62f * s, 0.19f * s);
|
||||||
|
path.lineTo(0.18f * s, 0.19f * s);
|
||||||
|
path.quadTo(0.32f * s, 0.19f * s, 0.3f * s, 0.07f * s);
|
||||||
|
line.setStrokeWidth(3f * u);
|
||||||
|
c.drawPath(path, line);
|
||||||
|
c.drawLine(-0.4f * s, 0.1f * s, -0.4f * s, 0.19f * s, line);
|
||||||
|
c.drawLine(0.0f, 0.1f * s, 0.0f, 0.19f * s, line);
|
||||||
|
// Santa with his sack.
|
||||||
|
fill.setColor(0xFF7A4A22);
|
||||||
|
c.drawCircle(-0.5f * s, -0.18f * s, 0.11f * s, fill);
|
||||||
|
fill.setColor(red);
|
||||||
|
c.drawCircle(-0.25f * s, -0.2f * s, 0.12f * s, fill);
|
||||||
|
fill.setColor(0xFFF2CFA0);
|
||||||
|
c.drawCircle(-0.22f * s, -0.36f * s, 0.07f * s, fill);
|
||||||
|
fill.setColor(0xFFFFFFFF);
|
||||||
|
r.set(-0.28f * s, -0.37f * s, -0.15f * s, -0.28f * s);
|
||||||
|
c.drawOval(r, fill);
|
||||||
|
path.reset();
|
||||||
|
path.moveTo(-0.29f * s, -0.4f * s);
|
||||||
|
path.lineTo(-0.14f * s, -0.4f * s);
|
||||||
|
path.lineTo(-0.3f * s, -0.56f * s);
|
||||||
|
path.close();
|
||||||
|
fill.setColor(red);
|
||||||
|
c.drawPath(path, fill);
|
||||||
|
fill.setColor(0xFFFFFFFF);
|
||||||
|
c.drawCircle(-0.3f * s, -0.57f * s, 0.035f * s, fill);
|
||||||
|
c.restore();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** A small angel flying right with a trumpet, wings beating. */
|
||||||
|
static void angel(Canvas c, Paint fill, Paint line, Path path, RectF r, float x, float y, float s,
|
||||||
|
float flap, float u) {
|
||||||
|
final int white = 0xFFF4F8FF, gold = 0xFFFFD04A;
|
||||||
|
c.save();
|
||||||
|
c.translate(x, y);
|
||||||
|
for (int i = 0; i < 4; i++) {
|
||||||
|
c.save();
|
||||||
|
c.translate(-0.05f * s, -0.12f * s);
|
||||||
|
c.rotate(-125f + flap * 28f + i * 14f);
|
||||||
|
r.set(0, -0.06f * s, (0.85f - 0.07f * i) * s, 0.06f * s);
|
||||||
|
fill.setColor(white);
|
||||||
|
c.drawOval(r, fill);
|
||||||
|
line.setColor(0xFFB9C6DC);
|
||||||
|
line.setStrokeWidth(1.5f * u);
|
||||||
|
c.drawOval(r, line);
|
||||||
|
c.restore();
|
||||||
|
}
|
||||||
|
// The robe trails behind, with a gold sash.
|
||||||
|
path.reset();
|
||||||
|
path.moveTo(0.2f * s, -0.14f * s);
|
||||||
|
path.quadTo(0.28f * s, 0.05f * s, 0.05f * s, 0.12f * s);
|
||||||
|
path.lineTo(-0.5f * s, 0.22f * s);
|
||||||
|
path.quadTo(-0.35f * s, -0.02f * s, -0.1f * s, -0.14f * s);
|
||||||
|
path.close();
|
||||||
|
fill.setColor(white);
|
||||||
|
c.drawPath(path, fill);
|
||||||
|
line.setColor(0xFFB9C6DC);
|
||||||
|
line.setStrokeWidth(1.5f * u);
|
||||||
|
c.drawPath(path, line);
|
||||||
|
line.setColor(gold);
|
||||||
|
line.setStrokeWidth(3f * u);
|
||||||
|
c.drawLine(0.02f * s, -0.12f * s, 0.12f * s, 0.06f * s, line);
|
||||||
|
// The head with a halo, and the trumpet held out in front.
|
||||||
|
fill.setColor(0xFFF2CFA0);
|
||||||
|
c.drawCircle(0.3f * s, -0.2f * s, 0.1f * s, fill);
|
||||||
|
fill.setColor(gold);
|
||||||
|
r.set(0.2f * s, -0.4f * s, 0.4f * s, -0.33f * s);
|
||||||
|
c.drawOval(r, line);
|
||||||
|
line.setStrokeWidth(2.5f * u);
|
||||||
|
c.drawLine(0.3f * s, -0.04f * s, 0.7f * s, -0.1f * s, line);
|
||||||
|
path.reset();
|
||||||
|
path.moveTo(0.66f * s, -0.1f * s);
|
||||||
|
path.lineTo(0.9f * s, -0.22f * s);
|
||||||
|
path.lineTo(0.9f * s, 0.02f * s);
|
||||||
|
path.close();
|
||||||
|
c.drawPath(path, fill);
|
||||||
|
c.restore();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ final class StylePalette {
|
|||||||
case EMPIRE: return R.style.ThemeOverlay_EV_Style_Empire;
|
case EMPIRE: return R.style.ThemeOverlay_EV_Style_Empire;
|
||||||
case MUERTOS: return R.style.ThemeOverlay_EV_Style_Muertos;
|
case MUERTOS: return R.style.ThemeOverlay_EV_Style_Muertos;
|
||||||
case HALLOWEEN: return R.style.ThemeOverlay_EV_Style_Halloween;
|
case HALLOWEEN: return R.style.ThemeOverlay_EV_Style_Halloween;
|
||||||
|
case CHRISTMAS: return R.style.ThemeOverlay_EV_Style_Christmas;
|
||||||
default: return 0;
|
default: return 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,10 +26,10 @@
|
|||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
<TextView android:id="@+id/accent_note" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginTop="4dp" android:textAppearance="@style/Text.EV.Secondary" android:visibility="gone" />
|
<TextView android:id="@+id/accent_note" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginTop="4dp" android:textAppearance="@style/Text.EV.Secondary" android:visibility="gone" />
|
||||||
<!-- Only the styles that show what plays (Tiles, Halloween). -->
|
<!-- Only the styles that show what plays (the styles with a place for it). -->
|
||||||
<com.google.android.material.switchmaterial.SwitchMaterial android:id="@+id/now_playing_switch" android:layout_width="match_parent" android:layout_height="wrap_content" android:minHeight="72dp" android:text="@string/label_now_playing" android:textAppearance="@style/Text.EV.Body" android:paddingTop="8dp" android:paddingBottom="8dp" app:useMaterialThemeColors="true" app:thumbTint="@color/switch_thumb" app:trackTint="@color/switch_track" android:textColor="@color/ev_text_primary" android:visibility="gone" />
|
<com.google.android.material.switchmaterial.SwitchMaterial android:id="@+id/now_playing_switch" android:layout_width="match_parent" android:layout_height="wrap_content" android:minHeight="72dp" android:text="@string/label_now_playing" android:textAppearance="@style/Text.EV.Body" android:paddingTop="8dp" android:paddingBottom="8dp" app:useMaterialThemeColors="true" app:thumbTint="@color/switch_thumb" app:trackTint="@color/switch_track" android:textColor="@color/ev_text_primary" android:visibility="gone" />
|
||||||
<TextView android:id="@+id/now_playing_hint" android:layout_width="match_parent" android:layout_height="wrap_content" android:text="@string/hint_now_playing" android:textAppearance="@style/Text.EV.Secondary" android:visibility="gone" />
|
<TextView android:id="@+id/now_playing_hint" android:layout_width="match_parent" android:layout_height="wrap_content" android:text="@string/hint_now_playing" android:textAppearance="@style/Text.EV.Secondary" android:visibility="gone" />
|
||||||
<!-- Only Halloween listens to the music. -->
|
<!-- Only Halloween and Christmas listen to the music. -->
|
||||||
<com.google.android.material.switchmaterial.SwitchMaterial android:id="@+id/music_light_switch" android:layout_width="match_parent" android:layout_height="wrap_content" android:minHeight="72dp" android:text="@string/label_music_light" android:textAppearance="@style/Text.EV.Body" android:paddingTop="8dp" android:paddingBottom="8dp" app:useMaterialThemeColors="true" app:thumbTint="@color/switch_thumb" app:trackTint="@color/switch_track" android:textColor="@color/ev_text_primary" android:visibility="gone" />
|
<com.google.android.material.switchmaterial.SwitchMaterial android:id="@+id/music_light_switch" android:layout_width="match_parent" android:layout_height="wrap_content" android:minHeight="72dp" android:text="@string/label_music_light" android:textAppearance="@style/Text.EV.Body" android:paddingTop="8dp" android:paddingBottom="8dp" app:useMaterialThemeColors="true" app:thumbTint="@color/switch_thumb" app:trackTint="@color/switch_track" android:textColor="@color/ev_text_primary" android:visibility="gone" />
|
||||||
<TextView android:id="@+id/music_light_hint" android:layout_width="match_parent" android:layout_height="wrap_content" android:text="@string/hint_music_light" android:textAppearance="@style/Text.EV.Secondary" android:visibility="gone" />
|
<TextView android:id="@+id/music_light_hint" android:layout_width="match_parent" android:layout_height="wrap_content" android:text="@string/hint_music_light" android:textAppearance="@style/Text.EV.Secondary" android:visibility="gone" />
|
||||||
<LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="horizontal" android:baselineAligned="false">
|
<LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="horizontal" android:baselineAligned="false">
|
||||||
|
|||||||
@@ -17,10 +17,10 @@
|
|||||||
<com.google.android.material.textfield.MaterialAutoCompleteTextView android:id="@+id/accent_choices" android:layout_width="match_parent" android:layout_height="wrap_content" android:minHeight="72dp" android:inputType="none" android:textSize="20sp" app:simpleItemLayout="@layout/dropdown_item" />
|
<com.google.android.material.textfield.MaterialAutoCompleteTextView android:id="@+id/accent_choices" android:layout_width="match_parent" android:layout_height="wrap_content" android:minHeight="72dp" android:inputType="none" android:textSize="20sp" app:simpleItemLayout="@layout/dropdown_item" />
|
||||||
</com.google.android.material.textfield.TextInputLayout>
|
</com.google.android.material.textfield.TextInputLayout>
|
||||||
<TextView android:id="@+id/accent_note" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginTop="4dp" android:textAppearance="@style/Text.EV.Secondary" android:visibility="gone" />
|
<TextView android:id="@+id/accent_note" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginTop="4dp" android:textAppearance="@style/Text.EV.Secondary" android:visibility="gone" />
|
||||||
<!-- Only the styles that show what plays (Tiles, Halloween). -->
|
<!-- Only the styles that show what plays (the styles with a place for it). -->
|
||||||
<com.google.android.material.switchmaterial.SwitchMaterial android:id="@+id/now_playing_switch" android:layout_width="match_parent" android:layout_height="wrap_content" android:minHeight="72dp" android:text="@string/label_now_playing" android:textAppearance="@style/Text.EV.Body" android:paddingTop="8dp" android:paddingBottom="8dp" app:useMaterialThemeColors="true" app:thumbTint="@color/switch_thumb" app:trackTint="@color/switch_track" android:textColor="@color/ev_text_primary" android:visibility="gone" />
|
<com.google.android.material.switchmaterial.SwitchMaterial android:id="@+id/now_playing_switch" android:layout_width="match_parent" android:layout_height="wrap_content" android:minHeight="72dp" android:text="@string/label_now_playing" android:textAppearance="@style/Text.EV.Body" android:paddingTop="8dp" android:paddingBottom="8dp" app:useMaterialThemeColors="true" app:thumbTint="@color/switch_thumb" app:trackTint="@color/switch_track" android:textColor="@color/ev_text_primary" android:visibility="gone" />
|
||||||
<TextView android:id="@+id/now_playing_hint" android:layout_width="match_parent" android:layout_height="wrap_content" android:text="@string/hint_now_playing" android:textAppearance="@style/Text.EV.Secondary" android:visibility="gone" />
|
<TextView android:id="@+id/now_playing_hint" android:layout_width="match_parent" android:layout_height="wrap_content" android:text="@string/hint_now_playing" android:textAppearance="@style/Text.EV.Secondary" android:visibility="gone" />
|
||||||
<!-- Only Halloween listens to the music. -->
|
<!-- Only Halloween and Christmas listen to the music. -->
|
||||||
<com.google.android.material.switchmaterial.SwitchMaterial android:id="@+id/music_light_switch" android:layout_width="match_parent" android:layout_height="wrap_content" android:minHeight="72dp" android:text="@string/label_music_light" android:textAppearance="@style/Text.EV.Body" android:paddingTop="8dp" android:paddingBottom="8dp" app:useMaterialThemeColors="true" app:thumbTint="@color/switch_thumb" app:trackTint="@color/switch_track" android:textColor="@color/ev_text_primary" android:visibility="gone" />
|
<com.google.android.material.switchmaterial.SwitchMaterial android:id="@+id/music_light_switch" android:layout_width="match_parent" android:layout_height="wrap_content" android:minHeight="72dp" android:text="@string/label_music_light" android:textAppearance="@style/Text.EV.Body" android:paddingTop="8dp" android:paddingBottom="8dp" app:useMaterialThemeColors="true" app:thumbTint="@color/switch_thumb" app:trackTint="@color/switch_track" android:textColor="@color/ev_text_primary" android:visibility="gone" />
|
||||||
<TextView android:id="@+id/music_light_hint" android:layout_width="match_parent" android:layout_height="wrap_content" android:text="@string/hint_music_light" android:textAppearance="@style/Text.EV.Secondary" android:visibility="gone" />
|
<TextView android:id="@+id/music_light_hint" android:layout_width="match_parent" android:layout_height="wrap_content" android:text="@string/hint_music_light" android:textAppearance="@style/Text.EV.Secondary" android:visibility="gone" />
|
||||||
<TextView android:layout_width="match_parent" android:layout_height="wrap_content" android:text="@string/label_tire_unit" android:textAppearance="@style/Text.EV.Body" android:layout_marginTop="20dp" />
|
<TextView android:layout_width="match_parent" android:layout_height="wrap_content" android:text="@string/label_tire_unit" android:textAppearance="@style/Text.EV.Body" android:layout_marginTop="20dp" />
|
||||||
|
|||||||
@@ -136,7 +136,7 @@
|
|||||||
<string name="now_playing_radio">Radio</string>
|
<string name="now_playing_radio">Radio</string>
|
||||||
<string name="now_playing_other">Media</string>
|
<string name="now_playing_other">Media</string>
|
||||||
<string name="now_playing_paused">%1$s · paused</string>
|
<string name="now_playing_paused">%1$s · paused</string>
|
||||||
<string name="label_music_light">Pumpkin light follows the music</string>
|
<string name="label_music_light">Light follows the music</string>
|
||||||
<string name="hint_music_light">Flares on the kick drum over Bluetooth or CarPlay, not the radio. Levels only, no sound kept.</string>
|
<string name="hint_music_light">Flares on the kick drum over Bluetooth or CarPlay, not the radio. Levels only, no sound kept.</string>
|
||||||
<string name="accent_not_used">%1$s has its own colours, and the app takes them on. To change the accent colour, pick a style that uses it.</string>
|
<string name="accent_not_used">%1$s has its own colours, and the app takes them on. To change the accent colour, pick a style that uses it.</string>
|
||||||
<string name="overview_fullscreen_enter">Overview full screen: hide the navigation</string>
|
<string name="overview_fullscreen_enter">Overview full screen: hide the navigation</string>
|
||||||
@@ -152,6 +152,7 @@
|
|||||||
<string name="overview_style_empire">Empire</string>
|
<string name="overview_style_empire">Empire</string>
|
||||||
<string name="overview_style_muertos">Día de Muertos</string>
|
<string name="overview_style_muertos">Día de Muertos</string>
|
||||||
<string name="overview_style_halloween">Halloween</string>
|
<string name="overview_style_halloween">Halloween</string>
|
||||||
|
<string name="overview_style_christmas">Christmas</string>
|
||||||
<string name="overview_style_nixie">Nixie tubes</string>
|
<string name="overview_style_nixie">Nixie tubes</string>
|
||||||
<string name="overview_style_chronograph">Chronograph</string>
|
<string name="overview_style_chronograph">Chronograph</string>
|
||||||
<string name="cockpit_battery">BATTERY</string>
|
<string name="cockpit_battery">BATTERY</string>
|
||||||
|
|||||||
@@ -124,4 +124,14 @@
|
|||||||
<item name="evAccentStroke">#80804A1E</item>
|
<item name="evAccentStroke">#80804A1E</item>
|
||||||
<item name="evAccentTrack">#6A3F18</item>
|
<item name="evAccentTrack">#6A3F18</item>
|
||||||
</style>
|
</style>
|
||||||
|
<!-- Christmas: the red of the big bauble. -->
|
||||||
|
<style name="ThemeOverlay.EV.Style.Christmas" parent="">
|
||||||
|
<item name="evAccent">#E53946</item>
|
||||||
|
<item name="evOnAccent">#1F0306</item>
|
||||||
|
<item name="evAccentLight">#FF9AA3</item>
|
||||||
|
<item name="evAccentDeep">#3D0C12</item>
|
||||||
|
<item name="evAccentSurface">#10243A</item>
|
||||||
|
<item name="evAccentStroke">#80B08A3A</item>
|
||||||
|
<item name="evAccentTrack">#7A2630</item>
|
||||||
|
</style>
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
Binary file not shown.
|
After Width: | Height: | Size: 317 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 287 KiB |
Reference in New Issue
Block a user