App 2.28.0: Overview style Mascot, an anime co-pilot who reacts to the drive
MascotView draws her head and shoulders beside speed, gear, battery, power, usage and trip. MascotMood picks her expression from the reading (waiting, reverse, worried, asleep, cheering, determined, fast, content) and holds a new mood for 1.5 s so values at a threshold do not make her flicker. A speech bubble carries a line per mood. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LhzAVaUyqcw5PkGjz4mWW6
This commit is contained in:
@@ -285,12 +285,13 @@ public class MainActivity extends AppCompatActivity {
|
||||
blueprintPane = drawnPane(blueprintView), mgPane = drawnPane(octagonView);
|
||||
terminalView = new TerminalView(this);
|
||||
java.util.Collections.addAll(canvasStyles, new MinimalView(this), terminalView,
|
||||
new AviationView(this), new NixieView(this), new ChronographView(this));
|
||||
new AviationView(this), new NixieView(this), new ChronographView(this), new MascotView(this));
|
||||
View[] canvasPanes = new View[canvasStyles.size()];
|
||||
for (int i = 0; i < canvasPanes.length; i++) canvasPanes[i] = drawnPane(canvasStyles.get(i));
|
||||
stylePanes = new View[] { vehiclePane, gaugesPane, cockpitPane, retroPane,
|
||||
futuristicPane, sciFiPane, cartoonPane, blueprintPane, mgPane,
|
||||
canvasPanes[0], canvasPanes[1], canvasPanes[2], canvasPanes[3], canvasPanes[4] };
|
||||
canvasPanes[0], canvasPanes[1], canvasPanes[2], canvasPanes[3], canvasPanes[4],
|
||||
canvasPanes[5] };
|
||||
// The Overview page holds every style; Settings → Overview style picks the one shown.
|
||||
android.widget.FrameLayout overviewPage = new android.widget.FrameLayout(this);
|
||||
overviewPage.setLayoutParams(new android.view.ViewGroup.LayoutParams(
|
||||
@@ -1558,7 +1559,7 @@ public class MainActivity extends AppCompatActivity {
|
||||
getString(R.string.overview_style_blueprint), getString(R.string.overview_style_mg),
|
||||
getString(R.string.overview_style_minimal), getString(R.string.overview_style_terminal),
|
||||
getString(R.string.overview_style_aviation), getString(R.string.overview_style_nixie),
|
||||
getString(R.string.overview_style_chronograph) };
|
||||
getString(R.string.overview_style_chronograph), getString(R.string.overview_style_mascot) };
|
||||
}
|
||||
|
||||
/** The style drop-down; every style is fed on every reading, only the chosen one is shown. */
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
package com.evsuite.abrp;
|
||||
|
||||
/**
|
||||
* What the Mascot Overview style's co-pilot feels, from one reading ({@link #of}), and a
|
||||
* {@link Holder} that keeps her from flickering between moods when a value hovers at a
|
||||
* threshold. Pure Java, unit-tested.
|
||||
*/
|
||||
enum MascotMood {
|
||||
/** Nothing read yet (the emulator, or the car's services not up). */
|
||||
WAITING,
|
||||
/** Reversing: looks back, careful. */
|
||||
REVERSE,
|
||||
/** Battery under {@link #LOW_SOC} %: worried, a sweat drop. */
|
||||
WORRIED,
|
||||
/** In P: asleep. */
|
||||
SLEEPING,
|
||||
/** Regenerating more than {@link #REGEN_KW} kW: cheering. */
|
||||
CHEERING,
|
||||
/** Drawing more than {@link #BOOST_KW} kW: determined. */
|
||||
DETERMINED,
|
||||
/** {@link #FAST_KMH} km/h and more: enjoying the speed, wind in her hair. */
|
||||
FAST,
|
||||
/** Anything else: content. */
|
||||
CONTENT;
|
||||
|
||||
static final float LOW_SOC = 20f, REGEN_KW = 5f, BOOST_KW = 60f, FAST_KMH = 100f;
|
||||
/** How long a new mood must last before she shows it. */
|
||||
static final long HOLD_MS = 1500;
|
||||
|
||||
/** The mood for one reading; unread values never count as a reason. */
|
||||
static MascotMood of(ClusterValues v) {
|
||||
if (v == null || (v.gear == null && v.speedKmh == null && v.socPercent == null && v.powerKw == null)) {
|
||||
return WAITING;
|
||||
}
|
||||
if (v.gear != null && v.gear == 2) return REVERSE;
|
||||
if (v.socPercent != null && v.socPercent < LOW_SOC) return WORRIED;
|
||||
if (v.gear != null && v.gear == 1) return SLEEPING;
|
||||
if (v.powerKw != null && v.powerKw < -REGEN_KW) return CHEERING;
|
||||
if (v.powerKw != null && v.powerKw > BOOST_KW) return DETERMINED;
|
||||
if (v.speedKmh != null && v.speedKmh >= FAST_KMH) return FAST;
|
||||
return CONTENT;
|
||||
}
|
||||
|
||||
/** Moods that show at once: shifting into R and losing the data are not to be waited out. */
|
||||
boolean immediate() {
|
||||
return this == REVERSE || this == WAITING;
|
||||
}
|
||||
|
||||
/** The shown mood: a new one takes over after it has lasted {@link #HOLD_MS}. */
|
||||
static final class Holder {
|
||||
private MascotMood shown, candidate;
|
||||
private long candidateSince;
|
||||
|
||||
MascotMood update(MascotMood now, long nowMs) {
|
||||
if (shown == null || now == shown || now.immediate()) {
|
||||
shown = now;
|
||||
candidate = null;
|
||||
return shown;
|
||||
}
|
||||
if (now != candidate) {
|
||||
candidate = now;
|
||||
candidateSince = nowMs;
|
||||
} else if (nowMs - candidateSince >= HOLD_MS) {
|
||||
shown = now;
|
||||
candidate = null;
|
||||
}
|
||||
return shown;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,511 @@
|
||||
package com.evsuite.abrp;
|
||||
|
||||
import android.content.Context;
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.Color;
|
||||
import android.graphics.Paint;
|
||||
import android.graphics.Path;
|
||||
import android.graphics.RectF;
|
||||
import android.graphics.Typeface;
|
||||
import android.os.SystemClock;
|
||||
|
||||
/**
|
||||
* The Mascot Overview style: an anime co-pilot beside the values, who reacts to the drive.
|
||||
*
|
||||
* On the left, head and shoulders: a young woman with dark plum hair, a headset with a boom mic
|
||||
* and a racing jacket in the accent colour with an octagon badge. Her face follows
|
||||
* {@link MascotMood} (held for a moment so it does not flicker): content, enjoying the speed
|
||||
* with wind lines, cheering with sparkles while regenerating, determined under hard
|
||||
* acceleration, worried with a sweat drop on a low battery, asleep with drifting Zs in P,
|
||||
* glancing back in R, and patient while nothing is read. She blinks now and then and says a
|
||||
* line for her mood in a speech bubble.
|
||||
*
|
||||
* On the right: speed large, the gear in a ring, battery as a bar with percent and range, then
|
||||
* power, 5-minute usage and the trip; the clock top right, left of the full-screen button.
|
||||
* Unread values show "—".
|
||||
*
|
||||
* 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 };
|
||||
|
||||
private final Paint fill = new Paint(Paint.ANTI_ALIAS_FLAG);
|
||||
private final Paint stroke = new Paint(Paint.ANTI_ALIAS_FLAG);
|
||||
private final Paint text = new Paint(Paint.ANTI_ALIAS_FLAG);
|
||||
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 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,
|
||||
shirt = 0xFFF4F4F6, headset = 0xFF2A2D33, cup = 0xFF3C414A;
|
||||
|
||||
MascotView(Context context) {
|
||||
super(context);
|
||||
accent = AccentColor.resolve(context, R.attr.evAccent);
|
||||
accentLight = AccentColor.resolve(context, R.attr.evAccentLight);
|
||||
accentDark = Color.rgb(Math.round(Color.red(accent) * 0.6f), Math.round(Color.green(accent) * 0.6f),
|
||||
Math.round(Color.blue(accent) * 0.6f));
|
||||
regen = context.getColor(R.color.ev_ok);
|
||||
warn = context.getColor(R.color.ev_error);
|
||||
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]);
|
||||
stroke.setStyle(Paint.Style.STROKE);
|
||||
stroke.setStrokeCap(Paint.Cap.ROUND);
|
||||
stroke.setStrokeJoin(Paint.Join.ROUND);
|
||||
text.setTypeface(Typeface.create("sans-serif-medium", Typeface.NORMAL));
|
||||
text.setFontFeatureSettings("tnum");
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDraw(Canvas c) {
|
||||
super.onDraw(c);
|
||||
float w = getWidth(), h = getHeight();
|
||||
if (w <= 0 || h <= 0) return;
|
||||
long t = SystemClock.uptimeMillis();
|
||||
MascotMood mood = holder.update(MascotMood.of(v), t);
|
||||
|
||||
// A soft halo behind her.
|
||||
float k = h * 0.98f / 100f, ox = w * 0.02f, oy = h - 100f * k;
|
||||
fill.setColor(AccentColor.withAlpha(accent, 0x30));
|
||||
c.drawCircle(ox + 50f * k, oy + 46f * k, 46f * k, fill);
|
||||
|
||||
c.save();
|
||||
c.translate(ox, oy);
|
||||
c.scale(k, k);
|
||||
drawCharacter(c, mood, t);
|
||||
c.restore();
|
||||
|
||||
float x0 = ox + 78f * k;
|
||||
drawBubble(c, mood, t, x0, h, ox + 70f * k, oy + 46f * k, w);
|
||||
drawValues(c, x0, ox + 93f * k, w, h);
|
||||
}
|
||||
|
||||
// ---------- The co-pilot, on the 100 × 100 grid ----------
|
||||
|
||||
private void drawCharacter(Canvas c, MascotMood mood, long t) {
|
||||
float tilt = mood == MascotMood.SLEEPING ? 8f : mood == MascotMood.REVERSE ? -6f
|
||||
: mood == MascotMood.CHEERING ? -4f : 0f;
|
||||
|
||||
// Hair behind the head, falling behind the shoulders.
|
||||
c.save();
|
||||
c.rotate(tilt, 50f, 62f);
|
||||
fill.setColor(hair);
|
||||
c.drawOval(oval(27f, 9f, 73f, 57f), fill);
|
||||
p.reset();
|
||||
p.moveTo(28.5f, 28f);
|
||||
p.cubicTo(26f, 46f, 25f, 64f, 22f, 84f);
|
||||
p.lineTo(78f, 84f);
|
||||
p.cubicTo(75f, 64f, 74f, 46f, 71.5f, 28f);
|
||||
p.close();
|
||||
c.drawPath(p, fill);
|
||||
c.restore();
|
||||
|
||||
// Neck, jacket, shirt collar, badge.
|
||||
fill.setColor(skin);
|
||||
c.drawRect(44.5f, 48f, 55.5f, 70f, fill);
|
||||
fill.setColor(skinShade);
|
||||
c.drawOval(oval(44f, 50f, 56f, 57f), fill);
|
||||
p.reset();
|
||||
p.moveTo(10f, 100f);
|
||||
p.lineTo(14f, 80f);
|
||||
p.cubicTo(16f, 71f, 26f, 68f, 40f, 66f);
|
||||
p.lineTo(60f, 66f);
|
||||
p.cubicTo(74f, 68f, 84f, 71f, 86f, 80f);
|
||||
p.lineTo(90f, 100f);
|
||||
p.close();
|
||||
fill.setColor(accent);
|
||||
c.drawPath(p, fill);
|
||||
triangle(c, 40.5f, 66f, 59.5f, 66f, 50f, 85f, shirt);
|
||||
triangle(c, 45f, 66f, 55f, 66f, 50f, 74f, skin);
|
||||
stroke.setColor(accentDark);
|
||||
stroke.setStrokeWidth(2.2f);
|
||||
c.drawLine(40.5f, 66.5f, 49f, 90f, stroke);
|
||||
c.drawLine(59.5f, 66.5f, 51f, 90f, stroke);
|
||||
stroke.setColor(shirt);
|
||||
stroke.setStrokeWidth(1.6f);
|
||||
c.drawLine(16f, 79f, 38f, 70f, stroke);
|
||||
c.drawLine(84f, 79f, 62f, 70f, stroke);
|
||||
badge(c, 68f, 86f, 4f);
|
||||
|
||||
// The head.
|
||||
c.save();
|
||||
c.rotate(tilt, 50f, 62f);
|
||||
p.reset();
|
||||
p.moveTo(35f, 28f);
|
||||
p.cubicTo(35f, 44f, 42f, 55f, 50f, 57f);
|
||||
p.cubicTo(58f, 55f, 65f, 44f, 65f, 28f);
|
||||
p.cubicTo(65f, 14f, 35f, 14f, 35f, 28f);
|
||||
p.close();
|
||||
fill.setColor(skin);
|
||||
c.drawPath(p, fill);
|
||||
|
||||
boolean blink = t % 4200 < 260;
|
||||
boolean blush = mood == MascotMood.CHEERING || mood == MascotMood.FAST || mood == MascotMood.SLEEPING;
|
||||
if (blush) {
|
||||
fill.setColor(0x55FF7A8A);
|
||||
c.drawOval(oval(37.5f, 43.2f, 43.5f, 46f), fill);
|
||||
c.drawOval(oval(56.5f, 43.2f, 62.5f, 46f), fill);
|
||||
}
|
||||
eye(c, 43f, 38f, mood, blink, true);
|
||||
eye(c, 57f, 38f, mood, blink, false);
|
||||
stroke.setColor(skinShade);
|
||||
stroke.setStrokeWidth(0.8f);
|
||||
c.drawLine(50f, 43.5f, 49.3f, 45.2f, stroke);
|
||||
mouth(c, mood);
|
||||
|
||||
drawFrontHair(c);
|
||||
brows(c, mood);
|
||||
drawHeadset(c);
|
||||
if (mood == MascotMood.WORRIED) sweatDrop(c);
|
||||
c.restore();
|
||||
|
||||
if (mood == MascotMood.SLEEPING) sleepZs(c, t);
|
||||
if (mood == MascotMood.CHEERING) sparkles(c, t);
|
||||
if (mood == MascotMood.FAST) wind(c, t);
|
||||
}
|
||||
|
||||
private void drawFrontHair(Canvas c) {
|
||||
fill.setColor(hair);
|
||||
p.reset();
|
||||
p.moveTo(33f, 36f);
|
||||
p.lineTo(33.5f, 24f);
|
||||
p.cubicTo(36f, 10f, 64f, 10f, 66.5f, 24f);
|
||||
p.lineTo(67f, 36f);
|
||||
p.lineTo(64f, 28f);
|
||||
p.lineTo(61.5f, 33f);
|
||||
p.lineTo(58.5f, 26.5f);
|
||||
p.lineTo(55f, 32f);
|
||||
p.lineTo(51.5f, 25.5f);
|
||||
p.lineTo(48f, 31.5f);
|
||||
p.lineTo(44.5f, 26f);
|
||||
p.lineTo(41.5f, 32.5f);
|
||||
p.lineTo(38.5f, 27f);
|
||||
p.lineTo(36f, 34f);
|
||||
p.close();
|
||||
c.drawPath(p, fill);
|
||||
// Side locks framing the face, the right one mirrored.
|
||||
p.reset();
|
||||
p.moveTo(33.5f, 26f);
|
||||
p.cubicTo(31f, 38f, 31f, 50f, 33f, 61f);
|
||||
p.lineTo(36.2f, 52f);
|
||||
p.cubicTo(35.5f, 44f, 36f, 36f, 37.5f, 30f);
|
||||
p.close();
|
||||
c.drawPath(p, fill);
|
||||
c.save();
|
||||
c.scale(-1f, 1f, 50f, 0f);
|
||||
c.drawPath(p, fill);
|
||||
c.restore();
|
||||
stroke.setColor(hairLight);
|
||||
stroke.setStrokeWidth(1.3f);
|
||||
c.drawArc(oval(38f, 13.5f, 62f, 30f), 205f, 55f, false, stroke);
|
||||
}
|
||||
|
||||
private void drawHeadset(Canvas c) {
|
||||
stroke.setColor(headset);
|
||||
stroke.setStrokeWidth(2.2f);
|
||||
c.drawArc(oval(31f, 9.5f, 69f, 48f), 180f, 180f, false, stroke);
|
||||
fill.setColor(cup);
|
||||
c.drawRoundRect(oval(28.5f, 29f, 36f, 42f), 2.5f, 2.5f, fill);
|
||||
c.drawRoundRect(oval(64f, 29f, 71.5f, 42f), 2.5f, 2.5f, fill);
|
||||
fill.setColor(accent);
|
||||
c.drawRect(29.5f, 33.5f, 30.6f, 37.5f, fill);
|
||||
c.drawRect(69.4f, 33.5f, 70.5f, 37.5f, fill);
|
||||
stroke.setStrokeWidth(1f);
|
||||
p.reset();
|
||||
p.moveTo(67.5f, 41f);
|
||||
p.quadTo(66f, 50f, 58.5f, 50.5f);
|
||||
c.drawPath(p, stroke);
|
||||
fill.setColor(headset);
|
||||
c.drawCircle(57.6f, 50.5f, 1.3f, fill);
|
||||
}
|
||||
|
||||
private void eye(Canvas c, float cx, float cy, MascotMood mood, boolean blink, boolean left) {
|
||||
stroke.setColor(lash);
|
||||
if (mood == MascotMood.SLEEPING || (blink && mood != MascotMood.CHEERING)) {
|
||||
// Closed, calm: a lid line curving down.
|
||||
stroke.setStrokeWidth(1.2f);
|
||||
c.drawArc(oval(cx - 4f, cy - 3.2f, cx + 4f, cy + 1.6f), 20f, 140f, false, stroke);
|
||||
return;
|
||||
}
|
||||
if (mood == MascotMood.CHEERING) {
|
||||
// Happy, squeezed shut: ^ ^.
|
||||
stroke.setStrokeWidth(1.4f);
|
||||
c.drawArc(oval(cx - 4f, cy - 1.5f, cx + 4f, cy + 4.5f), 200f, 140f, false, stroke);
|
||||
return;
|
||||
}
|
||||
float look = mood == MascotMood.REVERSE ? -1.6f : 0f;
|
||||
float irisW = mood == MascotMood.WORRIED ? 1.7f : 2.3f;
|
||||
fill.setColor(Color.WHITE);
|
||||
oval(cx - 4f, cy - 2.6f, cx + 4f, cy + 2.8f);
|
||||
c.drawOval(r, fill);
|
||||
c.save();
|
||||
p.reset();
|
||||
p.addOval(r, Path.Direction.CW);
|
||||
c.clipPath(p);
|
||||
fill.setColor(iris);
|
||||
c.drawOval(oval(cx - irisW + look, cy - 2.8f, cx + irisW + look, cy + 2.8f), fill);
|
||||
fill.setColor(pupil);
|
||||
c.drawOval(oval(cx - irisW * 0.5f + look, cy - 1.5f, cx + irisW * 0.5f + look, cy + 1.6f), fill);
|
||||
fill.setColor(Color.WHITE);
|
||||
c.drawCircle(cx - 0.8f + look, cy - 1.2f, 0.75f, fill);
|
||||
c.drawCircle(cx + 0.8f + look, cy + 1.2f, 0.35f, fill);
|
||||
if (mood == MascotMood.DETERMINED) {
|
||||
// Narrowed: the lid comes down over the top of the eye.
|
||||
fill.setColor(skin);
|
||||
c.drawRect(cx - 4.5f, cy - 3.2f, cx + 4.5f, cy - 1.0f, fill);
|
||||
}
|
||||
c.restore();
|
||||
stroke.setStrokeWidth(1.25f);
|
||||
if (mood == MascotMood.DETERMINED) {
|
||||
c.drawLine(cx - 4.3f, cy - 0.9f, cx + 4.3f, cy - 1.1f, stroke);
|
||||
} else {
|
||||
c.drawArc(oval(cx - 4.4f, cy - 3f, cx + 4.4f, cy + 3.4f), 195f, 150f, false, stroke);
|
||||
}
|
||||
// The flick at the outer corner.
|
||||
float outer = left ? cx - 4.2f : cx + 4.2f, dir = left ? -1f : 1f;
|
||||
c.drawLine(outer, cy - 0.6f, outer + dir * 1.3f, cy - 1.6f, stroke);
|
||||
}
|
||||
|
||||
private void brows(Canvas c, MascotMood mood) {
|
||||
stroke.setColor(hair);
|
||||
stroke.setStrokeWidth(1.1f);
|
||||
switch (mood) {
|
||||
case WORRIED:
|
||||
c.drawLine(39f, 31.5f, 46.5f, 29.3f, stroke);
|
||||
c.drawLine(53.5f, 29.3f, 61f, 31.5f, stroke);
|
||||
break;
|
||||
case DETERMINED:
|
||||
c.drawLine(39f, 29.6f, 46.8f, 32f, stroke);
|
||||
c.drawLine(53.2f, 32f, 61f, 29.6f, stroke);
|
||||
break;
|
||||
default:
|
||||
float lift = mood == MascotMood.CHEERING || mood == MascotMood.FAST ? -0.8f : 0f;
|
||||
c.drawArc(oval(39f, 30f + lift, 47f, 33.5f + lift), 200f, 120f, false, stroke);
|
||||
c.drawArc(oval(53f, 30f + lift, 61f, 33.5f + lift), 220f, 120f, false, stroke);
|
||||
}
|
||||
}
|
||||
|
||||
private void mouth(Canvas c, MascotMood mood) {
|
||||
stroke.setColor(lips);
|
||||
stroke.setStrokeWidth(1f);
|
||||
fill.setColor(mouth);
|
||||
switch (mood) {
|
||||
case CHEERING:
|
||||
case FAST:
|
||||
p.reset();
|
||||
p.moveTo(45.5f, 48.8f);
|
||||
p.quadTo(50f, 56f, 54.5f, 48.8f);
|
||||
p.close();
|
||||
c.drawPath(p, fill);
|
||||
fill.setColor(0xFFE88A95);
|
||||
c.drawOval(oval(47.8f, 51.2f, 52.2f, 52.8f), fill);
|
||||
break;
|
||||
case SLEEPING:
|
||||
case REVERSE:
|
||||
c.drawOval(oval(48.8f, 49.3f, 51.2f, 51.6f), fill);
|
||||
break;
|
||||
case WORRIED:
|
||||
p.reset();
|
||||
p.moveTo(46.5f, 51f);
|
||||
p.quadTo(47.8f, 49.6f, 49.2f, 51f);
|
||||
p.quadTo(50.6f, 52.4f, 52f, 51f);
|
||||
p.quadTo(53.2f, 49.8f, 54f, 50.8f);
|
||||
c.drawPath(p, stroke);
|
||||
break;
|
||||
case DETERMINED:
|
||||
p.reset();
|
||||
p.moveTo(46f, 50.6f);
|
||||
p.quadTo(51f, 52.6f, 55f, 49f);
|
||||
c.drawPath(p, stroke);
|
||||
break;
|
||||
default:
|
||||
c.drawArc(oval(46f, 46.5f, 54f, 51.2f), 25f, 130f, false, stroke);
|
||||
}
|
||||
}
|
||||
|
||||
private void sweatDrop(Canvas c) {
|
||||
fill.setColor(0xFF8FD3FF);
|
||||
p.reset();
|
||||
p.moveTo(63f, 21f);
|
||||
p.quadTo(66.5f, 26.5f, 63f, 28f);
|
||||
p.quadTo(59.5f, 26.5f, 63f, 21f);
|
||||
c.drawPath(p, fill);
|
||||
}
|
||||
|
||||
/** Three Zs drifting up and to the left, over three seconds. */
|
||||
private void sleepZs(Canvas c, long t) {
|
||||
float phase = (t % 3000) / 3000f;
|
||||
text.setTextAlign(Paint.Align.CENTER);
|
||||
float[][] zs = { { 24f, 22f, 5f }, { 17f, 14f, 6.5f }, { 9f, 5.5f, 8f } };
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
/** Four-pointed sparkles around her head, twinkling. */
|
||||
private void sparkles(Canvas c, long t) {
|
||||
float[][] at = { { 22f, 20f }, { 78f, 14f }, { 18f, 46f }, { 84f, 40f }, { 50f, 3f } };
|
||||
for (int i = 0; i < at.length; i++) {
|
||||
float s = 2.2f + 1.6f * (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);
|
||||
}
|
||||
}
|
||||
|
||||
/** Streaks rushing past on her left. */
|
||||
private void wind(Canvas c, long t) {
|
||||
stroke.setStrokeWidth(0.9f);
|
||||
stroke.setColor(0x88FFFFFF);
|
||||
for (int i = 0; i < 4; i++) {
|
||||
float x = 26f - ((t / 6f + i * 37f) % 24f);
|
||||
float y = 16f + i * 10f;
|
||||
c.drawLine(x - 9f, y, x, y, stroke);
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
/** A small silver octagon on the jacket. */
|
||||
private void badge(Canvas c, float x, float y, float s) {
|
||||
p.reset();
|
||||
for (int i = 0; i < 8; i++) {
|
||||
double a = Math.toRadians(22.5 + i * 45);
|
||||
float px = x + s * (float) Math.cos(a), py = y + s * (float) Math.sin(a);
|
||||
if (i == 0) p.moveTo(px, py);
|
||||
else p.lineTo(px, py);
|
||||
}
|
||||
p.close();
|
||||
fill.setColor(0xFFD7DBE0);
|
||||
c.drawPath(p, fill);
|
||||
text.setTextAlign(Paint.Align.CENTER);
|
||||
text.setTextSize(3.4f);
|
||||
text.setColor(accentDark);
|
||||
c.drawText("MG", x, y + 1.2f, text);
|
||||
}
|
||||
|
||||
private void triangle(Canvas c, float x1, float y1, float x2, float y2, float x3, float y3, int colour) {
|
||||
p.reset();
|
||||
p.moveTo(x1, y1);
|
||||
p.lineTo(x2, y2);
|
||||
p.lineTo(x3, y3);
|
||||
p.close();
|
||||
fill.setColor(colour);
|
||||
c.drawPath(p, fill);
|
||||
}
|
||||
|
||||
private RectF oval(float left, float top, float right, float bottom) {
|
||||
r.set(left, top, right, bottom);
|
||||
return r;
|
||||
}
|
||||
|
||||
// ---------- 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 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;
|
||||
// Shrink a long line to fit the room left of the clock.
|
||||
text.setTextSize(h * 0.07f);
|
||||
float width = text.measureText(line);
|
||||
if (width > room && room > 0f) {
|
||||
text.setTextSize(h * 0.07f * room / width);
|
||||
width = room;
|
||||
}
|
||||
float right = x0 + width + 2f * pad;
|
||||
float top = h * 0.05f, bottom = top + h * 0.07f + 2f * pad;
|
||||
fill.setColor(0xFFF7F7FA);
|
||||
c.drawRoundRect(oval(x0, top, right, bottom), h * 0.05f, h * 0.05f, fill);
|
||||
triangle(c, x0 + h * 0.03f, bottom - 2f, x0 + h * 0.11f, bottom - 2f, tipX, tipY, 0xFFF7F7FA);
|
||||
text.setColor(0xFF1C1A24);
|
||||
c.drawText(line, x0 + pad, bottom - pad - h * 0.012f, text);
|
||||
}
|
||||
|
||||
/** Speed and gear from x0; the battery and the bottom row from xLow, clear of her shoulder. */
|
||||
private void drawValues(Canvas c, float x0, float xLow, float w, float h) {
|
||||
float right = w - 16f * dp;
|
||||
// The clock, left of the full-screen button.
|
||||
text.setTextAlign(Paint.Align.RIGHT);
|
||||
text.setTextSize(20f * dp);
|
||||
text.setColor(grey);
|
||||
c.drawText(v.clock.isEmpty() ? "—" : v.clock, w - 72f * dp, 30f * dp, text);
|
||||
|
||||
// Speed, large, and the gear in a ring at the right.
|
||||
text.setTextAlign(Paint.Align.LEFT);
|
||||
text.setTextSize(h * 0.34f);
|
||||
text.setColor(white);
|
||||
String speed = fmt(v.speedKmh, "%.0f", "—");
|
||||
float baseline = h * 0.64f;
|
||||
c.drawText(speed, x0, baseline, text);
|
||||
float after = x0 + text.measureText(speed) + 14f * dp;
|
||||
text.setTextSize(h * 0.065f);
|
||||
text.setColor(grey);
|
||||
c.drawText("km/h", after, baseline, text);
|
||||
|
||||
float gr = h * 0.11f, gx = right - gr, gy = h * 0.50f;
|
||||
stroke.setColor(accent);
|
||||
stroke.setStrokeWidth(4f * dp);
|
||||
c.drawCircle(gx, gy, gr, stroke);
|
||||
String gear = Gear.label(v.gear);
|
||||
text.setTextAlign(Paint.Align.CENTER);
|
||||
text.setTextSize(gr * 1.1f);
|
||||
text.setColor(white);
|
||||
c.drawText(gear.isEmpty() ? " " : gear, gx, gy + gr * 0.38f, text);
|
||||
|
||||
// Battery: percent and range over a bar.
|
||||
float barY = h * 0.77f, barH = 12f * dp;
|
||||
boolean low = v.socPercent != null && v.socPercent < MascotMood.LOW_SOC;
|
||||
text.setTextSize(h * 0.055f);
|
||||
text.setTextAlign(Paint.Align.LEFT);
|
||||
text.setColor(low ? warn : white);
|
||||
c.drawText(fmt(v.socPercent, "%.0f %%", "— %"), xLow, barY - 10f * dp, text);
|
||||
text.setTextAlign(Paint.Align.RIGHT);
|
||||
text.setColor(white);
|
||||
c.drawText(fmt(v.rangeKm, "%.0f km", "— km"), right, barY - 10f * dp, text);
|
||||
fill.setColor(track);
|
||||
c.drawRoundRect(oval(xLow, barY, right, barY + barH), barH / 2f, barH / 2f, fill);
|
||||
if (v.socPercent != null) {
|
||||
float soc = Math.max(0f, Math.min(100f, v.socPercent)) / 100f;
|
||||
fill.setColor(low ? warn : accent);
|
||||
c.drawRoundRect(oval(xLow, barY, xLow + Math.max(barH, (right - xLow) * soc), barY + barH), barH / 2f, barH / 2f, fill);
|
||||
}
|
||||
|
||||
// Power, usage, trip.
|
||||
float col = (right - xLow) / 3f, labelY = h * 0.885f, valueY = h * 0.965f;
|
||||
boolean regenerating = v.powerKw != null && v.powerKw < 0f;
|
||||
cell(c, xLow, labelY, valueY, "POWER", fmt(v.powerKw, "%.1f kW", "— kW"), regenerating ? regen : white);
|
||||
cell(c, xLow + col, labelY, valueY, "USAGE · 5 MIN", fmt(v.usage5Min, "%.1f kWh/100 km", "—"), white);
|
||||
cell(c, xLow + 2f * col, labelY, valueY, "TRIP", fmt(v.tripKm, "%.1f km", "— km"), white);
|
||||
}
|
||||
|
||||
private void cell(Canvas c, float x, float labelY, float valueY, String label, String value, int colour) {
|
||||
text.setTextAlign(Paint.Align.LEFT);
|
||||
text.setTextSize(13f * dp);
|
||||
text.setLetterSpacing(0.12f);
|
||||
text.setColor(grey);
|
||||
c.drawText(label, x, labelY, text);
|
||||
text.setLetterSpacing(0f);
|
||||
text.setTextSize(getHeight() * 0.055f);
|
||||
text.setColor(colour);
|
||||
c.drawText(value, x, valueY, text);
|
||||
}
|
||||
}
|
||||
@@ -27,7 +27,8 @@ enum OverviewStyle {
|
||||
TERMINAL(true, true),
|
||||
AVIATION(false, false),
|
||||
NIXIE(false, false),
|
||||
CHRONOGRAPH(true, false);
|
||||
CHRONOGRAPH(true, false),
|
||||
MASCOT(true, false);
|
||||
|
||||
static final String PREF_KEY = "overview_style";
|
||||
|
||||
|
||||
@@ -139,6 +139,41 @@
|
||||
<string name="overview_style_aviation">Aviation</string>
|
||||
<string name="overview_style_nixie">Nixie tubes</string>
|
||||
<string name="overview_style_chronograph">Chronograph</string>
|
||||
<string name="overview_style_mascot">Mascot</string>
|
||||
<!-- 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>
|
||||
<item>Hello! Is anyone there?</item>
|
||||
</string-array>
|
||||
<string-array name="mascot_reverse">
|
||||
<item>Careful behind us…</item>
|
||||
<item>I\'ll watch the back!</item>
|
||||
</string-array>
|
||||
<string-array name="mascot_worried">
|
||||
<item>Battery\'s getting low…</item>
|
||||
<item>Maybe a charger soon?</item>
|
||||
</string-array>
|
||||
<string-array name="mascot_sleeping">
|
||||
<item>Zz… wake me when we drive.</item>
|
||||
<item>Parked. Nap time.</item>
|
||||
</string-array>
|
||||
<string-array name="mascot_cheering">
|
||||
<item>Free energy, yay!</item>
|
||||
<item>Charging on the go!</item>
|
||||
</string-array>
|
||||
<string-array name="mascot_determined">
|
||||
<item>Full power!</item>
|
||||
<item>Let\'s go!</item>
|
||||
</string-array>
|
||||
<string-array name="mascot_fast">
|
||||
<item>Wheee, motorway!</item>
|
||||
<item>I love this speed!</item>
|
||||
</string-array>
|
||||
<string-array name="mascot_content">
|
||||
<item>Nice and easy.</item>
|
||||
<item>Enjoying the drive.</item>
|
||||
<item>Smooth driving!</item>
|
||||
</string-array>
|
||||
<string name="cockpit_battery">BATTERY</string>
|
||||
<string name="cockpit_usage">USAGE · 5 MIN</string>
|
||||
<string name="cockpit_usage_unit">kWh/100 km</string>
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
package com.evsuite.abrp;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
|
||||
import org.junit.Test;
|
||||
|
||||
/** The Mascot style's moods — pure JVM. */
|
||||
public class MascotMoodTest {
|
||||
|
||||
private static ClusterValues values(Integer gear, Float speed, Float soc, Float power) {
|
||||
ClusterValues v = new ClusterValues();
|
||||
v.gear = gear;
|
||||
v.speedKmh = speed;
|
||||
v.socPercent = soc;
|
||||
v.powerKw = power;
|
||||
return v;
|
||||
}
|
||||
|
||||
@Test
|
||||
public void nothingReadIsWaiting() {
|
||||
assertEquals(MascotMood.WAITING, MascotMood.of(null));
|
||||
assertEquals(MascotMood.WAITING, MascotMood.of(new ClusterValues()));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void moodsByPriority() {
|
||||
assertEquals(MascotMood.REVERSE, MascotMood.of(values(2, 3f, 10f, 5f)));
|
||||
assertEquals(MascotMood.WORRIED, MascotMood.of(values(4, 50f, 15f, 10f)));
|
||||
assertEquals(MascotMood.WORRIED, MascotMood.of(values(1, 0f, 15f, 0f)));
|
||||
assertEquals(MascotMood.SLEEPING, MascotMood.of(values(1, 0f, 80f, 0f)));
|
||||
assertEquals(MascotMood.CHEERING, MascotMood.of(values(4, 120f, 80f, -20f)));
|
||||
assertEquals(MascotMood.DETERMINED, MascotMood.of(values(4, 60f, 80f, 90f)));
|
||||
assertEquals(MascotMood.FAST, MascotMood.of(values(4, 110f, 80f, 25f)));
|
||||
assertEquals(MascotMood.CONTENT, MascotMood.of(values(4, 50f, 80f, 10f)));
|
||||
assertEquals(MascotMood.CONTENT, MascotMood.of(values(3, 0f, 80f, 0f)));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void unreadValuesAreNoReason() {
|
||||
// Speed unread, battery unread: in D she is content, not worried or fast.
|
||||
assertEquals(MascotMood.CONTENT, MascotMood.of(values(4, null, null, 10f)));
|
||||
assertEquals(MascotMood.CONTENT, MascotMood.of(values(null, 50f, null, null)));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void holderWaitsOutShortChanges() {
|
||||
MascotMood.Holder holder = new MascotMood.Holder();
|
||||
assertEquals(MascotMood.CONTENT, holder.update(MascotMood.CONTENT, 0));
|
||||
assertEquals(MascotMood.CONTENT, holder.update(MascotMood.CHEERING, 100));
|
||||
assertEquals(MascotMood.CONTENT, holder.update(MascotMood.CONTENT, 300));
|
||||
assertEquals(MascotMood.CONTENT, holder.update(MascotMood.CHEERING, 400));
|
||||
assertEquals(MascotMood.CONTENT, holder.update(MascotMood.CHEERING, 1800));
|
||||
assertEquals(MascotMood.CHEERING, holder.update(MascotMood.CHEERING, 1900));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void reverseAndWaitingShowAtOnce() {
|
||||
MascotMood.Holder holder = new MascotMood.Holder();
|
||||
holder.update(MascotMood.SLEEPING, 0);
|
||||
assertEquals(MascotMood.REVERSE, holder.update(MascotMood.REVERSE, 10));
|
||||
assertEquals(MascotMood.WAITING, holder.update(MascotMood.WAITING, 20));
|
||||
}
|
||||
}
|
||||
@@ -36,6 +36,7 @@ public class OverviewStyleTest {
|
||||
assertTrue(OverviewStyle.TERMINAL.hasOriginal);
|
||||
assertFalse(OverviewStyle.CARTOON.usesAccent);
|
||||
assertFalse(OverviewStyle.NIXIE.usesAccent);
|
||||
assertTrue(OverviewStyle.MASCOT.usesAccent);
|
||||
assertEquals("original_colours_RETRO", OverviewStyle.RETRO.originalKey());
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user