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
533 lines
22 KiB
Java
533 lines
22 KiB
Java
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 "—".
|
||
*
|
||
* 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 {
|
||
|
||
/** 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);
|
||
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 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,
|
||
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] = 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);
|
||
text.setTypeface(Typeface.create("sans-serif-medium", Typeface.NORMAL));
|
||
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);
|
||
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);
|
||
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;
|
||
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);
|
||
}
|
||
|
||
// ---------- 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[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;
|
||
// 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);
|
||
}
|
||
}
|