package com.evsuite.abrp; import android.content.Context; import android.graphics.Canvas; import android.graphics.LinearGradient; import android.graphics.Paint; import android.graphics.RectF; import android.graphics.Shader; import android.graphics.Typeface; import android.os.SystemClock; import java.util.Calendar; import java.util.Locale; /** * The Time circuits Overview style, after the time machine in Back to the Future: three rows of * LED read-outs in a metal panel, labelled with embossed tape, a red speed display and a flux * capacitor. * * The rows keep the film's look, each in its colour: red "DESTINATION" — the gear as a word * (DRV, REV, NTL, PRK), the charge, the range (where you can still get to), power or * regeneration on the lamps, and the usage over 5 minutes; green "PRESENT TIME" — today's date * and time; amber "LAST TIME DEPARTED" — when the car last left P, dashes until it has been seen * to. Right, the speed in large red digits and the flux capacitor, whose light runs to its * centre faster with more power and outwards while regenerating; at 88 km/h it flashes. Along * the bottom, trip, usage since start, climate and odometer. Unread values show dashes and light * nothing. It animates (about 30 frames a second) only while there is power to show or at 88, * and only on screen. */ final class TimeCircuitsView extends ClusterCanvasView { private static final int RED = 0xFFFF3A26, GREEN = 0xFF3DFF5A, AMBER = 0xFFFFA726, WHITE_HOT = 0xFFFFF4D6; private final Paint line = new Paint(Paint.ANTI_ALIAS_FLAG); private final Paint fill = new Paint(Paint.ANTI_ALIAS_FLAG); private final Paint text = new Paint(Paint.ANTI_ALIAS_FLAG); private final Paint metal = new Paint(Paint.ANTI_ALIAS_FLAG); private final RectF r = new RectF(); private final Typeface tape = Typeface.create("sans-serif-condensed", Typeface.BOLD); private int metalFor = -1; /** When the car last left P, wall clock; 0 until that has been seen. */ private long departedAt; private Integer lastGear; private float fluxPhase; private long lastFrame; TimeCircuitsView(Context context) { super(context); line.setStyle(Paint.Style.STROKE); line.setStrokeCap(Paint.Cap.ROUND); } @Override void setValues(ClusterValues values) { // Readings arrive whether or not the style is shown, so the departure is never missed. Integer gear = values == null ? null : values.gear; if (gear != null && lastGear != null && lastGear == 1 && gear != 1) { departedAt = System.currentTimeMillis(); } if (gear != null) lastGear = gear; super.setValues(values); } @Override protected void onDraw(Canvas c) { super.onDraw(c); float w = getWidth(), h = getHeight(); if (w <= 0 || h <= 0) return; float u = Math.min(h / 640f, w / 1100f); advance(); c.drawColor(0xFF050505); float split = w * 0.64f; drawPanel(c, 12f * u, 10f * u, split - 6f * u, h - 10f * u, u); float rowLeft = 34f * u, rowRight = split - 24f * u; drawCarRow(c, rowLeft, rowRight, 22f * u, u); drawTimeRow(c, rowLeft, rowRight, 170f * u, u, GREEN, "PRESENT TIME", System.currentTimeMillis()); drawTimeRow(c, rowLeft, rowRight, 318f * u, u, AMBER, "LAST TIME DEPARTED", departedAt); drawBottom(c, rowLeft, rowRight, 470f * u, u); drawPanel(c, split + 6f * u, 10f * u, w - 12f * u, h - 10f * u, u); drawSpeed(c, split + 28f * u, w - 28f * u, 26f * u, u); drawFlux(c, split + 28f * u, w - 28f * u, 262f * u, h - 26f * u, u); boolean moving = (v.powerKw != null && Math.abs(v.powerKw) > 0.5f) || at88(); keepAnimating(moving, 33); } private boolean at88() { return v.speedKmh != null && v.speedKmh >= 88f; } private void advance() { long now = SystemClock.uptimeMillis(); float dt = lastFrame == 0 ? 0f : Math.min(0.2f, (now - lastFrame) / 1000f); lastFrame = now; if (v.powerKw != null) fluxPhase = (fluxPhase + v.powerKw * 0.025f * dt + 1000f) % 1f; } // ---------- Panels and tape ---------- private void drawPanel(Canvas c, float l, float t, float rr, float b, float u) { if (metalFor != (int) (b - t)) { metal.setShader(new LinearGradient(0, t, 0, b, 0xFF2C2D31, 0xFF18191B, Shader.TileMode.CLAMP)); metalFor = (int) (b - t); } r.set(l, t, rr, b); c.drawRoundRect(r, 10f * u, 10f * u, metal); line.setColor(0xFF45464B); line.setStrokeWidth(2f * u); c.drawRoundRect(r, 10f * u, 10f * u, line); fill.setColor(0xFF5A5B60); for (float x : new float[] { l + 14f * u, rr - 14f * u }) { for (float y : new float[] { t + 14f * u, b - 14f * u }) c.drawCircle(x, y, 4f * u, fill); } } /** Embossed label tape: black, raised white capitals, centred on x. */ private void tape(Canvas c, String s, float x, float y, float u, int tint) { text.setTypeface(tape); text.setTextSize(18f * u); text.setLetterSpacing(0.18f); text.setTextAlign(Paint.Align.CENTER); float tw = text.measureText(s); r.set(x - tw / 2f - 14f * u, y - 18f * u, x + tw / 2f + 14f * u, y + 7f * u); fill.setColor(tint); c.drawRoundRect(r, 4f * u, 4f * u, fill); fill.setColor(0x22FFFFFF); c.drawRect(r.left + 4f * u, r.top + 3f * u, r.right - 4f * u, r.top + 5f * u, fill); text.setColor(0xFFF2F2F2); c.drawText(s, x, y, text); text.setLetterSpacing(0f); } /** A small caption above a field. */ private void caption(Canvas c, String s, float x, float y, float u) { text.setTypeface(tape); text.setTextSize(13f * u); text.setLetterSpacing(0.12f); text.setTextAlign(Paint.Align.CENTER); text.setColor(0xFFBFC1C6); c.drawText(s, x, y, text); text.setLetterSpacing(0f); } // ---------- Rows ---------- /** The cells of one field: 'a' alphanumeric (fourteen segments), 'n' digits, 'l' lamps, ':' colon. */ private static final class Field { final char kind; final int cells; final String caption; Field(char kind, int cells, String caption) { this.kind = kind; this.cells = cells; this.caption = caption; } } private static final Field[] CAR = { new Field('a', 3, "GEAR"), new Field('n', 3, "CHARGE %"), new Field('n', 4, "RANGE KM"), new Field('l', 0, ""), new Field('n', 3, "USAGE"), }; private static final Field[] TIME = { new Field('a', 3, "MONTH"), new Field('n', 2, "DAY"), new Field('n', 4, "YEAR"), new Field('l', 0, ""), new Field('n', 2, "HOUR"), new Field(':', 0, ""), new Field('n', 2, "MIN"), }; private void drawCarRow(Canvas c, float left, float right, float top, float u) { String charge = v.socPercent == null ? "---" : String.format(Locale.US, "%3.0f", v.socPercent).replace(' ', '0'); String range = v.rangeKm == null ? "----" : String.format(Locale.US, "%04.0f", Math.min(9999f, v.rangeKm)); String usage = v.usage5Min == null ? "---" : String.format(Locale.US, "%.1f", Math.min(99.9, Math.abs(v.usage5Min))); boolean regen = v.powerKw != null && v.powerKw < -0.5f, power = v.powerKw != null && v.powerKw > 0.5f; drawRow(c, left, right, top, u, RED, "DESTINATION", CAR, new String[] { Segments14.gearWord(v.gear), charge, range, null, usage }, new String[] { "POWER", "REGEN" }, new boolean[] { power, regen }); } private void drawTimeRow(Canvas c, float left, float right, float top, float u, int colour, String title, long when) { String[] values; boolean[] lamps = { false, false }; if (when <= 0) { values = new String[] { "---", "--", "----", null, "--", null, "--" }; } else { Calendar cal = Calendar.getInstance(); cal.setTimeInMillis(when); int hour = cal.get(Calendar.HOUR_OF_DAY); values = new String[] { Segments14.month(cal.get(Calendar.MONTH)), String.format(Locale.US, "%02d", cal.get(Calendar.DAY_OF_MONTH)), String.format(Locale.US, "%04d", cal.get(Calendar.YEAR)), null, String.format(Locale.US, "%02d", hour), null, String.format(Locale.US, "%02d", cal.get(Calendar.MINUTE)) }; lamps = new boolean[] { hour < 12, hour >= 12 }; } drawRow(c, left, right, top, u, colour, title, TIME, values, new String[] { "AM", "PM" }, lamps); } private void drawRow(Canvas c, float left, float right, float top, float u, int colour, String title, Field[] fields, String[] values, String[] lampNames, boolean[] lampsOn) { float boxTop = top + 22f * u, boxH = 78f * u, gap = 14f * u, pad = 8f * u; float lampW = 74f * u, colonW = 16f * u; int cells = 0; float fixed = 0; for (Field f : fields) { cells += f.cells; if (f.kind == 'l') fixed += lampW; else if (f.kind == ':') fixed += colonW; else fixed += 2 * pad; } fixed += gap * (fields.length - 1); float cw = Math.min(boxH * 0.58f, (right - left - fixed) / cells); float total = cells * cw + fixed, x = left + (right - left - total) / 2f; for (int i = 0; i < fields.length; i++) { Field f = fields[i]; if (f.kind == 'l') { drawLamps(c, x, boxTop, lampW, boxH, u, colour, lampNames, lampsOn); x += lampW + gap; continue; } if (f.kind == ':') { fill.setColor(colour); c.drawCircle(x + colonW / 2f, boxTop + boxH * 0.36f, 3.5f * u, fill); c.drawCircle(x + colonW / 2f, boxTop + boxH * 0.64f, 3.5f * u, fill); x += colonW + gap; continue; } float bw = f.cells * cw + 2 * pad; r.set(x, boxTop, x + bw, boxTop + boxH); fill.setColor(0xFF020202); c.drawRoundRect(r, 4f * u, 4f * u, fill); line.setColor(0xFF5C5D62); line.setStrokeWidth(2f * u); c.drawRoundRect(r, 4f * u, 4f * u, line); caption(c, f.caption, x + bw / 2f, top + 14f * u, u); drawText(c, values[i], f.kind == 'a', x + pad, boxTop + 8f * u, cw, boxH - 16f * u, f.cells, colour, u); x += bw + gap; } tape(c, title, (left + right) / 2f, boxTop + boxH + 34f * u, u, 0xFF0E0E0F); } private void drawLamps(Canvas c, float x, float top, float w, float h, float u, int colour, String[] names, boolean[] on) { for (int k = 0; k < 2; k++) { float cy = top + h * (k == 0 ? 0.3f : 0.72f), cx = x + w - 16f * u; caption(c, names[k], x + 22f * u, cy + 5f * u, u); fill.setColor(on[k] ? AccentColor.withAlpha(colour, 0x55) : 0x00000000); c.drawCircle(cx, cy, 11f * u, fill); fill.setColor(on[k] ? colour : AccentColor.withAlpha(colour, 0x30)); c.drawCircle(cx, cy, 6f * u, fill); } } // ---------- Segments ---------- /** Right-aligned text in cells; a '.' joins the cell before it. */ private void drawText(Canvas c, String s, boolean alpha, float x, float y, float cw, float ch, int cells, int colour, float u) { char[] chars = SevenSegment.cells(s == null ? "" : s, cells); boolean[] dots = SevenSegment.dots(s == null ? "" : s, cells); for (int i = 0; i < cells; i++) { int mask = alpha ? Segments14.mask(chars[i]) : from7(SevenSegment.mask(chars[i])); drawCell(c, x + i * cw, y, cw, ch, mask, alpha, colour, u); if (dots[i]) { fill.setColor(colour); c.drawCircle(x + (i + 1) * cw - cw * 0.06f, y + ch - cw * 0.1f, cw * 0.07f, fill); } } } /** Seven-segment bits as fourteen-segment ones (the middle bar is both halves). */ private static int from7(int m) { int out = m & 63; // a–f share their bits if ((m & SevenSegment.G) != 0) out |= Segments14.G1 | Segments14.G2; return out; } private void drawCell(Canvas c, float x, float y, float cw, float ch, int mask, boolean alpha, int colour, float u) { float m = cw * 0.16f, t = cw * 0.12f; float x0 = x + m, x1 = x + cw - m, xc = (x0 + x1) / 2f, y0 = y + t, y1 = y + ch - t, ym = (y0 + y1) / 2f; float[][] segs = { { x0, y0, x1, y0, Segments14.A }, { x1, y0, x1, ym, Segments14.B }, { x1, ym, x1, y1, Segments14.C }, { x0, y1, x1, y1, Segments14.D }, { x0, ym, x0, y1, Segments14.E }, { x0, y0, x0, ym, Segments14.F }, { x0, ym, xc, ym, Segments14.G1 }, { xc, ym, x1, ym, Segments14.G2 }, { x0, y0, xc, ym, Segments14.H }, { xc, y0, xc, ym, Segments14.I }, { x1, y0, xc, ym, Segments14.J }, { x0, y1, xc, ym, Segments14.K }, { xc, ym, xc, y1, Segments14.L }, { x1, y1, xc, ym, Segments14.M }, }; for (float[] s : segs) { int bit = (int) s[4]; if (!alpha && bit >= Segments14.H) continue; // digits have no inner segments boolean on = (mask & bit) != 0; float dx = s[2] - s[0], dy = s[3] - s[1], len = (float) Math.hypot(dx, dy); float g = Math.min(t * 0.8f, len * 0.3f), ux = dx / len * g, uy = dy / len * g; float ax = s[0] + ux, ay = s[1] + uy, bx = s[2] - ux, by = s[3] - uy; if (on) { line.setColor(AccentColor.withAlpha(colour, 0x38)); line.setStrokeWidth(t * 2.4f); c.drawLine(ax, ay, bx, by, line); } line.setColor(on ? colour : AccentColor.withAlpha(colour, 0x1C)); line.setStrokeWidth(bit >= Segments14.H ? t * 0.8f : t); c.drawLine(ax, ay, bx, by, line); } } // ---------- Bottom read-outs ---------- private void drawBottom(Canvas c, float left, float right, float top, float u) { String[][] cells = { { "TRIP KM", fmt(v.tripKm, "%.1f", "---"), "5" }, { "SINCE START", fmt(v.usageTrip, "%.1f", "---"), "4" }, { "CLIMATE °C", fmt(v.climateCelsius, "%.0f", "--"), "3" }, { "ODOMETER KM", fmt(v.odometerKm, "%.0f", "------"), "6" }, }; float gap = 18f * u, boxH = 62f * u, boxTop = top + 22f * u, pad = 8f * u; int total = 0; for (String[] cell : cells) total += Integer.parseInt(cell[2]); float cw = Math.min(boxH * 0.55f, (right - left - gap * (cells.length - 1) - cells.length * 2 * pad) / total); float width = total * cw + gap * (cells.length - 1) + cells.length * 2 * pad; float x = left + (right - left - width) / 2f; for (String[] cell : cells) { int n = Integer.parseInt(cell[2]); float bw = n * cw + 2 * pad; r.set(x, boxTop, x + bw, boxTop + boxH); fill.setColor(0xFF020202); c.drawRoundRect(r, 4f * u, 4f * u, fill); line.setColor(0xFF5C5D62); line.setStrokeWidth(2f * u); c.drawRoundRect(r, 4f * u, 4f * u, line); caption(c, cell[0], x + bw / 2f, top + 14f * u, u); drawText(c, cell[1], false, x + pad, boxTop + 7f * u, cw, boxH - 14f * u, n, AMBER, u); x += bw + gap; } } // ---------- Speed and flux capacitor ---------- private void drawSpeed(Canvas c, float left, float right, float top, float u) { float boxH = 168f * u, cells = 3; float cw = Math.min(boxH * 0.56f, (right - left - 40f * u) / cells); float bw = cells * cw + 40f * u, x = (left + right - bw) / 2f; r.set(x, top, x + bw, top + boxH); fill.setColor(0xFF020202); c.drawRoundRect(r, 6f * u, 6f * u, fill); line.setColor(0xFF5C5D62); line.setStrokeWidth(2.5f * u); c.drawRoundRect(r, 6f * u, 6f * u, line); String speed = v.speedKmh == null ? "---" : String.format(Locale.US, "%.0f", Math.min(999f, v.speedKmh)); // At 88 the digits burn white-hot, and flicker. boolean hot = at88() && (SystemClock.uptimeMillis() / 90) % 3 != 0; drawText(c, speed, false, x + 20f * u, top + 14f * u, cw, boxH - 28f * u, 3, hot ? WHITE_HOT : RED, u); tape(c, at88() ? "88 KM/H — HOLD ON" : "KM/H", (left + right) / 2f, top + boxH + 34f * u, u, 0xFF0E0E0F); } /** Three glass tubes in a Y, light running to the centre with the power (outwards in regen). */ private void drawFlux(Canvas c, float left, float right, float top, float bottom, float u) { float labelSpace = 70f * u, boxBottom = bottom - labelSpace; float size = Math.min(right - left, boxBottom - top); float cx = (left + right) / 2f, bx = cx - size / 2f; r.set(bx, top, bx + size, boxBottom); fill.setColor(0xFF0B0C0E); c.drawRoundRect(r, 8f * u, 8f * u, fill); line.setColor(0xFF6A6B70); line.setStrokeWidth(3f * u); c.drawRoundRect(r, 8f * u, 8f * u, line); float cy = top + (boxBottom - top) * 0.52f, inset = size * 0.14f; float[][] ends = { { bx + inset, top + inset }, { bx + size - inset, top + inset }, { cx, boxBottom - inset } }; boolean flash = at88(); boolean live = v.powerKw != null; float strength = live ? Math.min(1f, Math.abs(v.powerKw) / 60f) : 0f; for (float[] e : ends) { line.setColor(0xFF2A2C30); line.setStrokeWidth(16f * u); c.drawLine(e[0], e[1], cx, cy, line); line.setColor(flash ? WHITE_HOT : AccentColor.withAlpha(WHITE_HOT, live ? 0x40 + (int) (0x60 * strength) : 0x18)); line.setStrokeWidth(5f * u); c.drawLine(e[0], e[1], cx, cy, line); if (live && Math.abs(v.powerKw) > 0.5f) { // Pulses along the tube: phase 0 at the end, 1 at the centre. for (int k = 0; k < 4; k++) { float p = (fluxPhase + k / 4f) % 1f; float px = e[0] + (cx - e[0]) * p, py = e[1] + (cy - e[1]) * p; fill.setColor(AccentColor.withAlpha(WHITE_HOT, 0x50)); c.drawCircle(px, py, 12f * u, fill); fill.setColor(WHITE_HOT); c.drawCircle(px, py, 5f * u, fill); } } fill.setColor(0xFF6A6B70); c.drawCircle(e[0], e[1], 12f * u, fill); } fill.setColor(AccentColor.withAlpha(WHITE_HOT, flash ? 0xA0 : live ? 0x30 + (int) (0x70 * strength) : 0x14)); c.drawCircle(cx, cy, 34f * u, fill); fill.setColor(flash || (live && strength > 0.05f) ? WHITE_HOT : 0xFF3A3B40); c.drawCircle(cx, cy, 13f * u, fill); tape(c, "FLUX CAPACITOR", cx, boxBottom + 30f * u, u, 0xFF0E0E0F); String power = !live ? "— KILOWATTS" : v.powerKw < -0.5f ? String.format(Locale.US, "REGEN %.1f KILOWATTS", -v.powerKw) : String.format(Locale.US, "%.1f KILOWATTS", v.powerKw); tape(c, power, cx, boxBottom + 62f * u, u, 0xFF5A1410); } }