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:
@@ -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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user