App 2.29.0: a chibi Mascot, chosen in Settings beside the tyre unit
ChibiFigure draws a small, full-figure co-pilot in a hoodie whose whole body acts the mood (waving, arms up and hopping, fist, hands on cheeks, nose bubble, pointing back, leaning into the wind). Settings → Mascot (Classic / Chibi, MascotCharacter) shows only for the Mascot style; the chibi has lines of her own. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LhzAVaUyqcw5PkGjz4mWW6
This commit is contained in:
@@ -0,0 +1,397 @@
|
||||
package com.evsuite.abrp;
|
||||
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.Color;
|
||||
import android.graphics.Paint;
|
||||
import android.graphics.Path;
|
||||
import android.graphics.RectF;
|
||||
|
||||
/**
|
||||
* The chibi co-pilot for the Mascot Overview style ({@link MascotCharacter#CHIBI}): the small,
|
||||
* full-figure version — a big round head with a coral bob and a star clip, a hoodie in the
|
||||
* accent colour, short legs, white trainers — drawn with dark outlines like a sticker.
|
||||
*
|
||||
* Her whole body acts the mood: waves while content, throws both arms up and hops while
|
||||
* cheering (star eyes), pumps a fist when determined, holds her cheeks with a tear when worried,
|
||||
* sleeps with a nose bubble in P, glances and points back in R, leans into the wind when
|
||||
* fast, and wonders "?" while waiting.
|
||||
*
|
||||
* Draws on the same 100 × 100 grid as {@link MascotView}'s classic figure; it holds no state.
|
||||
*/
|
||||
final class ChibiFigure {
|
||||
|
||||
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 Path p = new Path();
|
||||
private final Path q = new Path();
|
||||
private final RectF r = new RectF();
|
||||
private final int accent, accentLight, regen;
|
||||
private final int ink = 0xFF3A2A33, hair = 0xFFF0855C, hairLight = 0xFFFFB08A, skin = 0xFFFFE6D6,
|
||||
iris = 0xFF6E45B8, irisLight = 0xFFA27BE6, blush = 0x66FF7A8A, trousers = 0xFF2D3142,
|
||||
shoe = 0xFFF6F6F8, mouth = 0xFF8E3A48, tongue = 0xFFE88A95;
|
||||
|
||||
ChibiFigure(int accent, int accentLight, int regen) {
|
||||
this.accent = accent;
|
||||
this.accentLight = accentLight;
|
||||
this.regen = regen;
|
||||
line.setStyle(Paint.Style.STROKE);
|
||||
line.setStrokeCap(Paint.Cap.ROUND);
|
||||
line.setStrokeJoin(Paint.Join.ROUND);
|
||||
line.setColor(ink);
|
||||
text.setTextAlign(Paint.Align.CENTER);
|
||||
text.setFakeBoldText(true);
|
||||
}
|
||||
|
||||
/** Where the speech bubble's tail points, on the grid. */
|
||||
static final float TIP_X = 70f, TIP_Y = 34f;
|
||||
|
||||
void draw(Canvas c, MascotMood mood, long t, boolean blink) {
|
||||
// Smaller and further left than the grid, so her big head stays clear of the speed.
|
||||
c.save();
|
||||
c.translate(-6f, 0f);
|
||||
c.scale(0.92f, 0.92f, 50f, 100f);
|
||||
// A hop while cheering.
|
||||
float hop = mood == MascotMood.CHEERING ? -3.5f * (float) Math.abs(Math.sin(t / 180.0)) : 0f;
|
||||
fill.setColor(0x40000000);
|
||||
float shadow = 18f + hop * 0.8f;
|
||||
c.drawOval(oval(50f - shadow, 96.5f, 50f + shadow, 100f), fill);
|
||||
|
||||
c.save();
|
||||
c.translate(0f, hop);
|
||||
float lean = mood == MascotMood.FAST ? 5f : mood == MascotMood.SLEEPING ? -6f : 0f;
|
||||
c.rotate(lean, 50f, 96f);
|
||||
|
||||
drawLegs(c);
|
||||
drawBody(c);
|
||||
// Hands on her cheeks and the fist at her chest are in front of the head.
|
||||
boolean inFront = mood == MascotMood.WORRIED || mood == MascotMood.DETERMINED;
|
||||
if (!inFront) drawArms(c, mood, t);
|
||||
drawHead(c, mood, t, blink);
|
||||
if (inFront) drawArms(c, mood, t);
|
||||
c.restore();
|
||||
|
||||
switch (mood) {
|
||||
case SLEEPING: zs(c, t); break;
|
||||
case CHEERING: sparkles(c, t); break;
|
||||
case FAST: wind(c, t); break;
|
||||
case WAITING: question(c, t); break;
|
||||
default: break;
|
||||
}
|
||||
c.restore();
|
||||
}
|
||||
|
||||
private void drawLegs(Canvas c) {
|
||||
fill.setColor(trousers);
|
||||
line.setStrokeWidth(1.2f);
|
||||
for (float x : new float[] { 42f, 52f }) {
|
||||
oval(x, 84f, x + 6f, 95f);
|
||||
c.drawRect(r, fill);
|
||||
c.drawRect(r, line);
|
||||
}
|
||||
fill.setColor(shoe);
|
||||
for (float x : new float[] { 39.5f, 50.5f }) {
|
||||
oval(x, 92.5f, x + 10f, 98.5f);
|
||||
c.drawOval(r, fill);
|
||||
c.drawOval(r, line);
|
||||
}
|
||||
}
|
||||
|
||||
private void drawBody(Canvas c) {
|
||||
p.reset();
|
||||
p.moveTo(35.5f, 88f);
|
||||
p.lineTo(38f, 67f);
|
||||
p.quadTo(50f, 60f, 62f, 67f);
|
||||
p.lineTo(64.5f, 88f);
|
||||
p.quadTo(50f, 91f, 35.5f, 88f);
|
||||
p.close();
|
||||
fill.setColor(accent);
|
||||
c.drawPath(p, fill);
|
||||
line.setStrokeWidth(1.2f);
|
||||
c.drawPath(p, line);
|
||||
// The pocket and the drawstrings.
|
||||
line.setStrokeWidth(0.9f);
|
||||
p.reset();
|
||||
p.moveTo(42.5f, 88.5f);
|
||||
p.lineTo(43.5f, 79.5f);
|
||||
p.quadTo(50f, 78f, 56.5f, 79.5f);
|
||||
p.lineTo(57.5f, 88.5f);
|
||||
c.drawPath(p, line);
|
||||
line.setColor(Color.WHITE);
|
||||
line.setStrokeWidth(1f);
|
||||
c.drawLine(46.5f, 66f, 46f, 74f, line);
|
||||
c.drawLine(53.5f, 66f, 54f, 74f, line);
|
||||
line.setColor(ink);
|
||||
}
|
||||
|
||||
/** Two arms from the shoulders to hands placed by the mood. */
|
||||
private void drawArms(Canvas c, MascotMood mood, long t) {
|
||||
float wave = (float) Math.sin(t / 220.0) * 3f;
|
||||
float lx, ly, rx, ry;
|
||||
switch (mood) {
|
||||
case CONTENT: lx = 33f; ly = 82f; rx = 82f + wave * 0.5f; ry = 64f + wave; break;
|
||||
case CHEERING: lx = 16f; ly = 56f; rx = 84f; ry = 56f; break;
|
||||
case DETERMINED: lx = 33f; ly = 82f; rx = 57f; ry = 72f; break;
|
||||
case WORRIED: lx = 34f; ly = 55f; rx = 66f; ry = 55f; break;
|
||||
// Pointing back.
|
||||
case REVERSE: lx = 20f; ly = 66f; rx = 67f; ry = 82f; break;
|
||||
case FAST: lx = 30f; ly = 76f; rx = 70f; ry = 76f; break;
|
||||
default: lx = 33f; ly = 82f; rx = 67f; ry = 82f;
|
||||
}
|
||||
arm(c, 39.5f, 69f, lx, ly);
|
||||
arm(c, 60.5f, 69f, rx, ry);
|
||||
}
|
||||
|
||||
private void arm(Canvas c, float sx, float sy, float hx, float hy) {
|
||||
line.setStrokeWidth(7.4f);
|
||||
c.drawLine(sx, sy, hx, hy, line);
|
||||
line.setColor(accent);
|
||||
line.setStrokeWidth(5f);
|
||||
c.drawLine(sx, sy, hx, hy, line);
|
||||
line.setColor(ink);
|
||||
fill.setColor(skin);
|
||||
line.setStrokeWidth(1.1f);
|
||||
c.drawCircle(hx, hy, 3.2f, fill);
|
||||
c.drawCircle(hx, hy, 3.2f, line);
|
||||
}
|
||||
|
||||
private void drawHead(Canvas c, MascotMood mood, long t, boolean blink) {
|
||||
float tilt = mood == MascotMood.REVERSE ? -10f : mood == MascotMood.SLEEPING ? 10f
|
||||
: mood == MascotMood.WAITING ? 6f : 0f;
|
||||
c.save();
|
||||
c.rotate(tilt, 50f, 62f);
|
||||
// The bob behind the face.
|
||||
fill.setColor(hair);
|
||||
line.setStrokeWidth(1.3f);
|
||||
p.reset();
|
||||
p.addOval(oval(20f, 5f, 80f, 62f), Path.Direction.CW);
|
||||
q.reset();
|
||||
q.addRoundRect(oval(19.5f, 30f, 80.5f, 64f), 7f, 7f, Path.Direction.CW);
|
||||
p.op(q, Path.Op.UNION);
|
||||
c.drawPath(p, fill);
|
||||
c.drawPath(p, line);
|
||||
// The face.
|
||||
fill.setColor(skin);
|
||||
oval(25f, 15f, 75f, 65f);
|
||||
c.drawOval(r, fill);
|
||||
c.drawOval(r, line);
|
||||
// Bangs.
|
||||
p.reset();
|
||||
p.moveTo(22.5f, 40f);
|
||||
p.cubicTo(19f, 7f, 81f, 7f, 77.5f, 40f);
|
||||
p.lineTo(73f, 27f);
|
||||
p.lineTo(66f, 34f);
|
||||
p.lineTo(60f, 23f);
|
||||
p.lineTo(52f, 32f);
|
||||
p.lineTo(45f, 22f);
|
||||
p.lineTo(38f, 32f);
|
||||
p.lineTo(32f, 25f);
|
||||
p.lineTo(27f, 35f);
|
||||
p.close();
|
||||
fill.setColor(hair);
|
||||
c.drawPath(p, fill);
|
||||
c.drawPath(p, line);
|
||||
line.setColor(hairLight);
|
||||
line.setStrokeWidth(1.6f);
|
||||
c.drawArc(oval(30f, 10f, 56f, 30f), 200f, 60f, false, line);
|
||||
line.setColor(ink);
|
||||
// The star clip.
|
||||
fill.setColor(accentLight);
|
||||
star(c, 70f, 19f, 4.5f);
|
||||
line.setStrokeWidth(0.8f);
|
||||
c.drawPath(p, line);
|
||||
|
||||
if (mood == MascotMood.CHEERING || mood == MascotMood.FAST || mood == MascotMood.CONTENT
|
||||
|| mood == MascotMood.SLEEPING || mood == MascotMood.WORRIED) {
|
||||
fill.setColor(blush);
|
||||
c.drawOval(oval(29.5f, 50f, 37f, 54f), fill);
|
||||
c.drawOval(oval(63f, 50f, 70.5f, 54f), fill);
|
||||
}
|
||||
eye(c, 39f, 44f, mood, blink, t);
|
||||
eye(c, 61f, 44f, mood, blink, t);
|
||||
brows(c, mood);
|
||||
mouth(c, mood, t);
|
||||
if (mood == MascotMood.WORRIED) {
|
||||
fill.setColor(0xFF8FD3FF);
|
||||
drop(c, 72f, 30f, 3f);
|
||||
drop(c, 34.5f, 52f, 1.8f);
|
||||
}
|
||||
c.restore();
|
||||
}
|
||||
|
||||
private void eye(Canvas c, float cx, float cy, MascotMood mood, boolean blink, long t) {
|
||||
line.setColor(ink);
|
||||
line.setStrokeWidth(1.5f);
|
||||
if (mood == MascotMood.SLEEPING || (blink && mood != MascotMood.CHEERING && mood != MascotMood.FAST)) {
|
||||
c.drawArc(oval(cx - 4.5f, cy - 3f, cx + 4.5f, cy + 2f), 20f, 140f, false, line);
|
||||
return;
|
||||
}
|
||||
if (mood == MascotMood.FAST) {
|
||||
// ^ ^
|
||||
c.drawArc(oval(cx - 4.5f, cy - 2f, cx + 4.5f, cy + 5f), 200f, 140f, false, line);
|
||||
return;
|
||||
}
|
||||
float look = mood == MascotMood.REVERSE ? -2f : 0f;
|
||||
oval(cx - 4.6f, cy - 5.6f, cx + 4.6f, cy + 5.6f);
|
||||
if (mood == MascotMood.REVERSE) {
|
||||
fill.setColor(Color.WHITE);
|
||||
c.drawOval(r, fill);
|
||||
fill.setColor(iris);
|
||||
c.drawOval(oval(cx - 3f + look, cy - 4f, cx + 3f + look, cy + 4.5f), fill);
|
||||
} else {
|
||||
fill.setColor(iris);
|
||||
c.drawOval(r, fill);
|
||||
fill.setColor(irisLight);
|
||||
c.drawOval(oval(cx - 3.6f, cy + 0.5f, cx + 3.6f, cy + 5f), fill);
|
||||
}
|
||||
if (mood == MascotMood.CHEERING) {
|
||||
fill.setColor(Color.WHITE);
|
||||
star(c, cx, cy - 0.5f, 3.6f);
|
||||
} else {
|
||||
fill.setColor(Color.WHITE);
|
||||
c.drawCircle(cx - 1.6f + look, cy - 2.4f, 2f, fill);
|
||||
c.drawCircle(cx + 1.8f + look, cy + 2.4f, 0.9f, fill);
|
||||
}
|
||||
if (mood == MascotMood.DETERMINED) {
|
||||
// A flat lid over the top: focused.
|
||||
fill.setColor(skin);
|
||||
c.drawRect(cx - 5.2f, cy - 6.4f, cx + 5.2f, cy - 2f, fill);
|
||||
line.setStrokeWidth(1.6f);
|
||||
c.drawLine(cx - 5f, cy - 2f, cx + 5f, cy - 2f, line);
|
||||
return;
|
||||
}
|
||||
line.setStrokeWidth(1.6f);
|
||||
c.drawArc(oval(cx - 5f, cy - 5.9f, cx + 5f, cy + 5.6f), 200f, 140f, false, line);
|
||||
}
|
||||
|
||||
private void brows(Canvas c, MascotMood mood) {
|
||||
line.setColor(ink);
|
||||
line.setStrokeWidth(1.4f);
|
||||
if (mood == MascotMood.DETERMINED) {
|
||||
c.drawLine(34f, 33.5f, 43.5f, 36.5f, line);
|
||||
c.drawLine(56.5f, 36.5f, 66f, 33.5f, line);
|
||||
} else if (mood == MascotMood.WORRIED) {
|
||||
c.drawLine(34f, 36f, 43f, 33f, line);
|
||||
c.drawLine(57f, 33f, 66f, 36f, line);
|
||||
}
|
||||
}
|
||||
|
||||
private void mouth(Canvas c, MascotMood mood, long t) {
|
||||
line.setColor(ink);
|
||||
line.setStrokeWidth(1.2f);
|
||||
switch (mood) {
|
||||
case CHEERING:
|
||||
case FAST:
|
||||
p.reset();
|
||||
p.moveTo(45f, 53.5f);
|
||||
p.quadTo(50f, 61.5f, 55f, 53.5f);
|
||||
p.close();
|
||||
fill.setColor(mouth);
|
||||
c.drawPath(p, fill);
|
||||
fill.setColor(tongue);
|
||||
c.drawOval(oval(47.5f, 56.4f, 52.5f, 58.4f), fill);
|
||||
c.drawPath(p, line);
|
||||
break;
|
||||
case DETERMINED:
|
||||
fill.setColor(Color.WHITE);
|
||||
oval(44.5f, 53f, 55.5f, 57f);
|
||||
c.drawRoundRect(r, 1.5f, 1.5f, fill);
|
||||
c.drawRoundRect(r, 1.5f, 1.5f, line);
|
||||
c.drawLine(44.8f, 55f, 55.2f, 55f, line);
|
||||
break;
|
||||
case WORRIED:
|
||||
p.reset();
|
||||
p.moveTo(45.5f, 56f);
|
||||
p.quadTo(47f, 54.3f, 48.5f, 56f);
|
||||
p.quadTo(50f, 57.7f, 51.5f, 56f);
|
||||
p.quadTo(53f, 54.3f, 54.5f, 56f);
|
||||
c.drawPath(p, line);
|
||||
break;
|
||||
case SLEEPING: {
|
||||
fill.setColor(mouth);
|
||||
c.drawOval(oval(48.6f, 54.5f, 51.4f, 57f), fill);
|
||||
// A nose bubble that swells and pops.
|
||||
float grow = (t % 2400) / 2400f;
|
||||
fill.setColor(0x7090D8FF);
|
||||
c.drawCircle(53.5f, 51f, 1f + grow * 4f, fill);
|
||||
break;
|
||||
}
|
||||
case REVERSE:
|
||||
fill.setColor(mouth);
|
||||
c.drawOval(oval(48.6f, 54f, 51.4f, 57f), fill);
|
||||
break;
|
||||
case WAITING:
|
||||
c.drawLine(47.5f, 55.5f, 52.5f, 55.5f, line);
|
||||
break;
|
||||
default:
|
||||
// A cat smile: w.
|
||||
p.reset();
|
||||
p.moveTo(45.5f, 54f);
|
||||
p.quadTo(47.75f, 57.5f, 50f, 54.5f);
|
||||
p.quadTo(52.25f, 57.5f, 54.5f, 54f);
|
||||
c.drawPath(p, line);
|
||||
}
|
||||
}
|
||||
|
||||
private void zs(Canvas c, long t) {
|
||||
float phase = (t % 3000) / 3000f;
|
||||
float[][] zs = { { 20f, 24f, 6f }, { 13f, 14f, 7.5f }, { 5f, 4f, 9f } };
|
||||
for (float[] z : zs) {
|
||||
text.setTextSize(z[2]);
|
||||
text.setColor(AccentColor.withAlpha(accentLight, Math.round(255 * (1f - phase * 0.5f))));
|
||||
c.drawText("Z", z[0] - phase * 2f, z[1] + z[2] - phase * 3f, text);
|
||||
}
|
||||
}
|
||||
|
||||
private void sparkles(Canvas c, long t) {
|
||||
float[][] at = { { 14f, 30f }, { 86f, 26f }, { 12f, 62f }, { 88f, 60f }, { 50f, 1.5f } };
|
||||
for (int i = 0; i < at.length; i++) {
|
||||
float s = 2.4f + 1.8f * (float) Math.abs(Math.sin(t / 260.0 + i * 1.3));
|
||||
fill.setColor(i % 2 == 0 ? regen : Color.WHITE);
|
||||
star(c, at[i][0], at[i][1], s);
|
||||
}
|
||||
}
|
||||
|
||||
private void wind(Canvas c, long t) {
|
||||
line.setStrokeWidth(1f);
|
||||
line.setColor(0x88FFFFFF);
|
||||
for (int i = 0; i < 5; i++) {
|
||||
float x = 18f - ((t / 6f + i * 37f) % 20f);
|
||||
float y = 20f + i * 14f;
|
||||
c.drawLine(x - 10f, y, x, y, line);
|
||||
}
|
||||
line.setColor(ink);
|
||||
}
|
||||
|
||||
private void question(Canvas c, long t) {
|
||||
text.setTextSize(14f);
|
||||
text.setColor(accentLight);
|
||||
float bob = (float) Math.sin(t / 400.0) * 1.2f;
|
||||
c.drawText("?", 16f, 18f + bob, text);
|
||||
}
|
||||
|
||||
private void drop(Canvas c, float x, float y, float s) {
|
||||
p.reset();
|
||||
p.moveTo(x, y - s * 1.6f);
|
||||
p.quadTo(x + s * 1.2f, y + s * 0.2f, x, y + s);
|
||||
p.quadTo(x - s * 1.2f, y + s * 0.2f, x, y - s * 1.6f);
|
||||
c.drawPath(p, fill);
|
||||
}
|
||||
|
||||
/** A four-pointed sparkle; leaves its outline in {@link #p}. */
|
||||
private void star(Canvas c, float x, float y, float s) {
|
||||
p.reset();
|
||||
p.moveTo(x, y - s);
|
||||
p.quadTo(x, y, x + s, y);
|
||||
p.quadTo(x, y, x, y + s);
|
||||
p.quadTo(x, y, x - s, y);
|
||||
p.quadTo(x, y, x, y - s);
|
||||
p.close();
|
||||
c.drawPath(p, fill);
|
||||
}
|
||||
|
||||
private RectF oval(float left, float top, float right, float bottom) {
|
||||
r.set(left, top, right, bottom);
|
||||
return r;
|
||||
}
|
||||
}
|
||||
@@ -69,6 +69,7 @@ public class MainActivity extends AppCompatActivity {
|
||||
/** The styles built on {@link ClusterCanvasView}, fed together. */
|
||||
private final java.util.List<ClusterCanvasView> canvasStyles = new java.util.ArrayList<>();
|
||||
private TerminalView terminalView;
|
||||
private MascotView mascotView;
|
||||
/** The pane of each Overview style, by its {@link OverviewStyle} ordinal. */
|
||||
private View[] stylePanes;
|
||||
private View consumptionPane;
|
||||
@@ -284,8 +285,10 @@ public class MainActivity extends AppCompatActivity {
|
||||
View sciFiPane = drawnPane(sciFiView), cartoonPane = drawnPane(cartoonView),
|
||||
blueprintPane = drawnPane(blueprintView), mgPane = drawnPane(octagonView);
|
||||
terminalView = new TerminalView(this);
|
||||
mascotView = new MascotView(this);
|
||||
mascotView.setCharacter(MascotCharacter.fromPref(prefs.getString(MascotCharacter.PREF_KEY, null)));
|
||||
java.util.Collections.addAll(canvasStyles, new MinimalView(this), terminalView,
|
||||
new AviationView(this), new NixieView(this), new ChronographView(this), new MascotView(this));
|
||||
new AviationView(this), new NixieView(this), new ChronographView(this), mascotView);
|
||||
View[] canvasPanes = new View[canvasStyles.size()];
|
||||
for (int i = 0; i < canvasPanes.length; i++) canvasPanes[i] = drawnPane(canvasStyles.get(i));
|
||||
stylePanes = new View[] { vehiclePane, gaugesPane, cockpitPane, retroPane,
|
||||
@@ -1120,6 +1123,7 @@ public class MainActivity extends AppCompatActivity {
|
||||
voiceKeySwitch.setOnCheckedChangeListener((btn, checked) ->
|
||||
prefs.edit().putBoolean(UploadSettings.KEY_VOICE_KEY_OPENS, checked).apply());
|
||||
bindTireUnit();
|
||||
bindMascotCharacter();
|
||||
bindOverviewStyle();
|
||||
bindAccentColor();
|
||||
fullScreenSwitch = settingsPane.findViewById(R.id.full_screen_switch);
|
||||
@@ -1260,6 +1264,17 @@ public class MainActivity extends AppCompatActivity {
|
||||
prefs.edit().putString(PressureUnit.PREF_KEY, units[position].name()).apply());
|
||||
}
|
||||
|
||||
/** Who sits in the Mascot style; the drop-down shows only while that style is chosen. */
|
||||
private void bindMascotCharacter() {
|
||||
MascotCharacter[] characters = MascotCharacter.values();
|
||||
String[] labels = { getString(R.string.mascot_character_classic), getString(R.string.mascot_character_chibi) };
|
||||
int current = MascotCharacter.fromPref(prefs.getString(MascotCharacter.PREF_KEY, null)).ordinal();
|
||||
bindChoices(settingsPane.findViewById(R.id.mascot_choices), labels, current, position -> {
|
||||
prefs.edit().putString(MascotCharacter.PREF_KEY, characters[position].name()).apply();
|
||||
mascotView.setCharacter(characters[position]);
|
||||
});
|
||||
}
|
||||
|
||||
/** The service reads these settings on every tick, so a change applies without a restart. */
|
||||
private void bindCadenceControls() {
|
||||
UploadSettings current = UploadSettings.from(prefs);
|
||||
@@ -1548,6 +1563,10 @@ public class MainActivity extends AppCompatActivity {
|
||||
TextView note = settingsPane.findViewById(R.id.accent_note);
|
||||
note.setVisibility(style.usesAccent ? View.GONE : View.VISIBLE);
|
||||
note.setText(getString(R.string.accent_not_used, styleLabels()[style.ordinal()]));
|
||||
// The wide layout keeps the Mascot field's place so the page does not jump.
|
||||
View mascot = settingsPane.findViewById(R.id.mascot_field);
|
||||
boolean wide = getResources().getConfiguration().screenWidthDp >= 1000;
|
||||
mascot.setVisibility(style == OverviewStyle.MASCOT ? View.VISIBLE : wide ? View.INVISIBLE : View.GONE);
|
||||
}
|
||||
|
||||
/** The drop-down's labels, by {@link OverviewStyle} ordinal. */
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
package com.evsuite.abrp;
|
||||
|
||||
/**
|
||||
* Who sits in the Mascot Overview style, in the order of the Settings drop-down: the classic
|
||||
* co-pilot (head and shoulders, headset, racing jacket) or the chibi (small, full figure, big
|
||||
* head, hoodie, bubbly). Each has her own drawing and lines; both follow {@link MascotMood}.
|
||||
* Pure Java, unit-tested.
|
||||
*/
|
||||
enum MascotCharacter {
|
||||
CLASSIC,
|
||||
CHIBI;
|
||||
|
||||
static final String PREF_KEY = "mascot_character";
|
||||
|
||||
/** The stored character; anything unknown is the classic one. */
|
||||
static MascotCharacter fromPref(String stored) {
|
||||
for (MascotCharacter character : values()) {
|
||||
if (character.name().equals(stored)) return character;
|
||||
}
|
||||
return CLASSIC;
|
||||
}
|
||||
}
|
||||
@@ -24,14 +24,20 @@ import android.os.SystemClock;
|
||||
* power, 5-minute usage and the trip; the clock top right, left of the full-screen button.
|
||||
* Unread values show "—".
|
||||
*
|
||||
* Two characters ({@link MascotCharacter}): this classic co-pilot, or the chibi drawn by
|
||||
* {@link ChibiFigure}, each with her own lines.
|
||||
*
|
||||
* Drawn on a 100 × 100 grid scaled to the pane. Nothing animates on its own: blinking, the Zs,
|
||||
* sparkles and wind move because readings redraw the view every 0.2 s.
|
||||
*/
|
||||
final class MascotView extends ClusterCanvasView {
|
||||
|
||||
private static final int[] LINES = {
|
||||
R.array.mascot_waiting, R.array.mascot_reverse, R.array.mascot_worried, R.array.mascot_sleeping,
|
||||
R.array.mascot_cheering, R.array.mascot_determined, R.array.mascot_fast, R.array.mascot_content };
|
||||
/** Her lines per {@link MascotCharacter}, then per {@link MascotMood}, in their orders. */
|
||||
private static final int[][] LINES = {
|
||||
{ R.array.mascot_waiting, R.array.mascot_reverse, R.array.mascot_worried, R.array.mascot_sleeping,
|
||||
R.array.mascot_cheering, R.array.mascot_determined, R.array.mascot_fast, R.array.mascot_content },
|
||||
{ R.array.chibi_waiting, R.array.chibi_reverse, R.array.chibi_worried, R.array.chibi_sleeping,
|
||||
R.array.chibi_cheering, R.array.chibi_determined, R.array.chibi_fast, R.array.chibi_content } };
|
||||
|
||||
private final Paint fill = new Paint(Paint.ANTI_ALIAS_FLAG);
|
||||
private final Paint stroke = new Paint(Paint.ANTI_ALIAS_FLAG);
|
||||
@@ -39,7 +45,9 @@ final class MascotView extends ClusterCanvasView {
|
||||
private final Path p = new Path();
|
||||
private final RectF r = new RectF();
|
||||
private final MascotMood.Holder holder = new MascotMood.Holder();
|
||||
private final String[][] lines = new String[LINES.length][];
|
||||
private final String[][][] lines = new String[LINES.length][][];
|
||||
private final ChibiFigure chibi;
|
||||
private MascotCharacter character = MascotCharacter.CLASSIC;
|
||||
private final int accent, accentDark, accentLight, regen, warn, white, grey, track;
|
||||
private final int hair = 0xFF2E2440, hairLight = 0xFF5A4880, skin = 0xFFFBE2D0, skinShade = 0xFFE9BFA9,
|
||||
iris = 0xFF2F8F9D, pupil = 0xFF123238, lash = 0xFF221A2E, lips = 0xFFC2606A, mouth = 0xFF8E3A48,
|
||||
@@ -56,7 +64,11 @@ final class MascotView extends ClusterCanvasView {
|
||||
white = context.getColor(R.color.ev_text_primary);
|
||||
grey = context.getColor(R.color.ev_text_secondary);
|
||||
track = context.getColor(R.color.ev_outline);
|
||||
for (int i = 0; i < LINES.length; i++) lines[i] = context.getResources().getStringArray(LINES[i]);
|
||||
for (int i = 0; i < LINES.length; i++) {
|
||||
lines[i] = new String[LINES[i].length][];
|
||||
for (int j = 0; j < LINES[i].length; j++) lines[i][j] = context.getResources().getStringArray(LINES[i][j]);
|
||||
}
|
||||
chibi = new ChibiFigure(accent, accentLight, regen);
|
||||
stroke.setStyle(Paint.Style.STROKE);
|
||||
stroke.setStrokeCap(Paint.Cap.ROUND);
|
||||
stroke.setStrokeJoin(Paint.Join.ROUND);
|
||||
@@ -64,6 +76,12 @@ final class MascotView extends ClusterCanvasView {
|
||||
text.setFontFeatureSettings("tnum");
|
||||
}
|
||||
|
||||
/** Who sits beside the values (Settings → Mascot, shown for this style only). */
|
||||
void setCharacter(MascotCharacter character) {
|
||||
this.character = character == null ? MascotCharacter.CLASSIC : character;
|
||||
invalidate();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDraw(Canvas c) {
|
||||
super.onDraw(c);
|
||||
@@ -80,11 +98,14 @@ final class MascotView extends ClusterCanvasView {
|
||||
c.save();
|
||||
c.translate(ox, oy);
|
||||
c.scale(k, k);
|
||||
drawCharacter(c, mood, t);
|
||||
boolean chibiShown = character == MascotCharacter.CHIBI;
|
||||
if (chibiShown) chibi.draw(c, mood, t, t % 4200 < 260);
|
||||
else drawCharacter(c, mood, t);
|
||||
c.restore();
|
||||
|
||||
float x0 = ox + 78f * k;
|
||||
drawBubble(c, mood, t, x0, h, ox + 70f * k, oy + 46f * k, w);
|
||||
float tipX = chibiShown ? ChibiFigure.TIP_X : 70f, tipY = chibiShown ? ChibiFigure.TIP_Y : 46f;
|
||||
drawBubble(c, mood, t, x0, h, ox + tipX * k, oy + tipY * k, w);
|
||||
drawValues(c, x0, ox + 93f * k, w, h);
|
||||
}
|
||||
|
||||
@@ -420,7 +441,7 @@ final class MascotView extends ClusterCanvasView {
|
||||
// ---------- Speech bubble and values, in pixels ----------
|
||||
|
||||
private void drawBubble(Canvas c, MascotMood mood, long t, float x0, float h, float tipX, float tipY, float w) {
|
||||
String[] options = lines[mood.ordinal()];
|
||||
String[] options = lines[character.ordinal()][mood.ordinal()];
|
||||
String line = options.length == 0 ? "" : options[(int) ((t / 30000 + mood.ordinal()) % options.length)];
|
||||
text.setTextAlign(Paint.Align.LEFT);
|
||||
float pad = h * 0.045f, room = w - 140f * dp - x0 - 2f * pad;
|
||||
|
||||
@@ -33,7 +33,13 @@
|
||||
<com.google.android.material.textfield.MaterialAutoCompleteTextView android:id="@+id/tire_unit_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>
|
||||
</LinearLayout>
|
||||
<Space android:layout_width="0dp" android:layout_weight="1" android:layout_height="1dp" />
|
||||
<!-- Only for the Mascot style: who sits in it. -->
|
||||
<LinearLayout android:id="@+id/mascot_field" android:layout_width="0dp" android:layout_weight="1" android:layout_height="wrap_content" android:orientation="vertical" android:visibility="invisible">
|
||||
<TextView android:layout_width="match_parent" android:layout_height="wrap_content" android:text="@string/label_mascot_character" android:textAppearance="@style/Text.EV.Body" android:layout_marginTop="8dp" />
|
||||
<com.google.android.material.textfield.TextInputLayout style="@style/Widget.EV.Dropdown" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginTop="4dp">
|
||||
<com.google.android.material.textfield.MaterialAutoCompleteTextView android:id="@+id/mascot_choices" android:layout_width="match_parent" android:layout_height="wrap_content" android:minHeight="72dp" android:inputType="none" android:maxLines="1" android:ellipsize="end" android:textSize="20sp" app:simpleItemLayout="@layout/dropdown_item" />
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
</LinearLayout>
|
||||
</LinearLayout>
|
||||
</LinearLayout>
|
||||
</LinearLayout>
|
||||
|
||||
@@ -21,6 +21,13 @@
|
||||
<com.google.android.material.textfield.TextInputLayout style="@style/Widget.EV.Dropdown" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginTop="12dp">
|
||||
<com.google.android.material.textfield.MaterialAutoCompleteTextView android:id="@+id/tire_unit_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>
|
||||
<!-- Only for the Mascot style: who sits in it. -->
|
||||
<LinearLayout android:id="@+id/mascot_field" android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="vertical" android:visibility="gone">
|
||||
<TextView android:layout_width="match_parent" android:layout_height="wrap_content" android:text="@string/label_mascot_character" android:textAppearance="@style/Text.EV.Body" android:layout_marginTop="20dp" />
|
||||
<com.google.android.material.textfield.TextInputLayout style="@style/Widget.EV.Dropdown" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginTop="12dp">
|
||||
<com.google.android.material.textfield.MaterialAutoCompleteTextView android:id="@+id/mascot_choices" android:layout_width="match_parent" android:layout_height="wrap_content" android:minHeight="72dp" android:inputType="none" android:maxLines="1" android:ellipsize="end" android:textSize="20sp" app:simpleItemLayout="@layout/dropdown_item" />
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
</LinearLayout>
|
||||
</LinearLayout>
|
||||
<LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="vertical" android:padding="24dp" android:background="@drawable/bg_glass" android:layout_marginTop="16dp">
|
||||
<TextView android:layout_width="match_parent" android:layout_height="wrap_content" android:text="@string/design_battery_settings_title" android:textAppearance="@style/Text.EV.Eyebrow" />
|
||||
|
||||
@@ -140,6 +140,43 @@
|
||||
<string name="overview_style_nixie">Nixie tubes</string>
|
||||
<string name="overview_style_chronograph">Chronograph</string>
|
||||
<string name="overview_style_mascot">Mascot</string>
|
||||
<string name="label_mascot_character">Mascot</string>
|
||||
<string name="mascot_character_classic">Classic</string>
|
||||
<string name="mascot_character_chibi">Chibi</string>
|
||||
<!-- The chibi's lines, per mood like mascot_*. -->
|
||||
<string-array name="chibi_waiting">
|
||||
<item>Hellooo? Car?</item>
|
||||
<item>Is the car awake yet?</item>
|
||||
</string-array>
|
||||
<string-array name="chibi_reverse">
|
||||
<item>Beep beep, backing up!</item>
|
||||
<item>Eyes on the back!</item>
|
||||
</string-array>
|
||||
<string-array name="chibi_worried">
|
||||
<item>The battery\'s hungry!</item>
|
||||
<item>Snack time for the car?</item>
|
||||
</string-array>
|
||||
<string-array name="chibi_sleeping">
|
||||
<item>Zzz… five more minutes…</item>
|
||||
<item>Napping in P…</item>
|
||||
</string-array>
|
||||
<string-array name="chibi_cheering">
|
||||
<item>Free energy! Yay!</item>
|
||||
<item>Regen power!</item>
|
||||
</string-array>
|
||||
<string-array name="chibi_determined">
|
||||
<item>Zoom zoom!</item>
|
||||
<item>Full throttle!</item>
|
||||
</string-array>
|
||||
<string-array name="chibi_fast">
|
||||
<item>Weeee, so fast!</item>
|
||||
<item>Wind in my hair!</item>
|
||||
</string-array>
|
||||
<string-array name="chibi_content">
|
||||
<item>La la la, driving~</item>
|
||||
<item>Comfy cruising!</item>
|
||||
<item>Vroom vroom!</item>
|
||||
</string-array>
|
||||
<!-- The Mascot style's co-pilot: a line per mood (MascotMood), one picked every 30 s. -->
|
||||
<string-array name="mascot_waiting">
|
||||
<item>Waiting for the car…</item>
|
||||
|
||||
Reference in New Issue
Block a user