package com.evsuite.abrp; import android.content.Context; import android.graphics.Canvas; import android.graphics.Paint; import android.graphics.Path; import android.graphics.RadialGradient; import android.graphics.RectF; import android.graphics.Shader; import android.graphics.Typeface; import android.os.SystemClock; import java.util.Locale; /** * The Visor Overview style, after the helmet display of a science-fiction shooter like Halo: * thin light-blue lines on a dark visor, slanted panels and a reticle in the middle. * * Top centre, the shield bar: the battery, segmented, red and blinking below 20 % with * "BATTERY LOW". Top right, the ammunition counter: the range as a number and as rounds, one * per 10 km. Under it, four grenade slots for P R N D, the current one lit. Middle, the * reticle with the speed inside it and power round it — clockwise while drawing, green and * anticlockwise while regenerating. Bottom left, the motion tracker: its sweep turns while the * car moves, and its blips are the last two minutes of power (red drawing, green regenerating), further out the more * power (the rim is 60 kW, 40 kW regenerating). * Top left the clock, bottom right the objective with trip, usage, climate and odometer. * Unread values show "—" and light nothing. * * Two palettes: the original visor blue, or the chosen accent colour ({@link #setAccentPalette}). * The tracker animates (about 30 frames a second) only while the car moves and the view is on * screen; the low-battery blink rides on the readings that redraw it. */ final class VisorView extends ClusterCanvasView { /** Shield segments, rounds in the counter (10 km each). */ private static final int SHIELD = 24, ROUNDS = 50; private static final int RED = 0xFFFF3B30, REGEN = 0xFF4CFF7A, ENEMY = 0xFFFF4A3D; 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 vignette = new Paint(); private final Path path = new Path(); private final RectF r = new RectF(); private final Typeface face = Typeface.create("sans-serif-condensed", Typeface.BOLD); private final Typeface thin = Typeface.create("sans-serif-condensed", Typeface.NORMAL); private final int accent, accentLight; private int main, light, dim; private float sweep; private long lastFrame; private int vignetteFor = -1; VisorView(Context context) { super(context); accent = AccentColor.resolve(context, R.attr.evAccent); accentLight = AccentColor.resolve(context, R.attr.evAccentLight); line.setStyle(Paint.Style.STROKE); line.setStrokeJoin(Paint.Join.MITER); setAccentPalette(false); } /** The chosen accent colour instead of the original visor blue. */ void setAccentPalette(boolean useAccent) { main = useAccent ? accent : 0xFF5CC8FF; light = useAccent ? accentLight : 0xFFD2F0FF; dim = AccentColor.withAlpha(main, 0x40); vignetteFor = -1; invalidate(); } @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); boolean moving = v.speedKmh != null && v.speedKmh > 0.5f; advance(moving); drawVisor(c, w, h, u); drawClock(c, u); drawShield(c, w, u); drawAmmo(c, w, u); drawReticle(c, w, h, u); drawTracker(c, h, u); drawObjective(c, w, h, u); keepAnimating(moving, 33); } private void advance(boolean moving) { long now = SystemClock.uptimeMillis(); float dt = lastFrame == 0 ? 0f : Math.min(0.2f, (now - lastFrame) / 1000f); lastFrame = now; if (moving) sweep = (sweep + dt * 180f) % 360f; } // ---------- Visor ---------- /** Dark glass, the helmet's rim darkening the corners, and its edge lines at the top. */ private void drawVisor(Canvas c, float w, float h, float u) { c.drawColor(0xFF03080D); if (vignetteFor != (int) (w + h)) { vignette.setShader(new RadialGradient(w / 2f, h * 0.48f, Math.max(w, h) * 0.62f, new int[] { AccentColor.withAlpha(main, 0x16), 0x00000000, 0xE0000000 }, new float[] { 0f, 0.62f, 1f }, Shader.TileMode.CLAMP)); vignetteFor = (int) (w + h); } c.drawRect(0, 0, w, h, vignette); line.setStrokeWidth(1.5f * u); line.setColor(AccentColor.withAlpha(main, 0x50)); path.reset(); path.moveTo(0, 150f * u); path.quadTo(w * 0.08f, 16f * u, w * 0.3f, 8f * u); path.moveTo(w, 150f * u); path.quadTo(w * 0.92f, 16f * u, w * 0.7f, 8f * u); c.drawPath(path, line); } /** A slanted panel, as the display draws its frames. */ private void slant(Canvas c, float l, float t, float rr, float b, float lean, int stroke, int bg, float u) { path.reset(); path.moveTo(l + lean, t); path.lineTo(rr, t); path.lineTo(rr - lean, b); path.lineTo(l, b); path.close(); fill.setColor(bg); c.drawPath(path, fill); line.setColor(stroke); line.setStrokeWidth(1.5f * u); c.drawPath(path, line); } private void label(Canvas c, String s, float x, float y, float size, int colour, Typeface tf, Paint.Align a) { text.setTypeface(tf); text.setTextSize(size); text.setTextAlign(a); text.setColor(colour); c.drawText(s, x, y, text); } // ---------- Clock ---------- private void drawClock(Canvas c, float u) { float l = 28f * u, t = 26f * u; slant(c, l, t, l + 150f * u, t + 44f * u, 12f * u, main, AccentColor.withAlpha(main, 0x18), u); label(c, v.clock.isEmpty() ? "—" : v.clock, l + 75f * u, t + 33f * u, 30f * u, light, face, Paint.Align.CENTER); label(c, "LOCAL TIME", l + 12f * u, t + 64f * u, 14f * u, main, face, Paint.Align.LEFT); } // ---------- Shield ---------- private void drawShield(Canvas c, float w, float u) { boolean low = v.socPercent != null && v.socPercent < 20f; boolean blinkOn = (SystemClock.uptimeMillis() / 500) % 2 == 0; int colour = low ? RED : main; float bw = Math.min(520f * u, w * 0.36f), l = (w - bw) / 2f, t = 28f * u, b = t + 30f * u; slant(c, l - 14f * u, t - 8f * u, l + bw + 14f * u, b + 8f * u, 14f * u, AccentColor.withAlpha(colour, 0x90), 0x66000000, u); int lit = v.socPercent == null ? 0 : Math.round(Math.max(0f, Math.min(100f, v.socPercent)) / 100f * SHIELD); float gap = 3f * u, seg = (bw - (SHIELD - 1) * gap) / SHIELD; for (int i = 0; i < SHIELD; i++) { float x = l + i * (seg + gap); path.reset(); path.moveTo(x + 6f * u, t); path.lineTo(x + seg + 6f * u, t); path.lineTo(x + seg, b); path.lineTo(x, b); path.close(); boolean on = i < lit && (!low || blinkOn); fill.setColor(on ? colour : AccentColor.withAlpha(colour, 0x22)); c.drawPath(path, fill); } label(c, fmt(v.socPercent, "%.0f %%", "—"), w / 2f, b + 36f * u, 26f * u, low ? RED : light, face, Paint.Align.CENTER); if (low && blinkOn) { label(c, "BATTERY LOW", w / 2f, b + 66f * u, 22f * u, RED, face, Paint.Align.CENTER); } } // ---------- Ammunition and grenades ---------- private void drawAmmo(Canvas c, float w, float u) { float rr = w - 30f * u, t = 26f * u; // The count: the range. label(c, fmt(v.rangeKm, "%.0f", "—"), rr, t + 52f * u, 60f * u, light, face, Paint.Align.RIGHT); label(c, "KM RANGE", rr, t + 74f * u, 15f * u, main, face, Paint.Align.RIGHT); // The rounds: two rows of 25, one per 10 km. int lit = v.rangeKm == null ? 0 : Math.round(Math.max(0f, v.rangeKm) / 10f); float rw = 5f * u, rh = 20f * u, gap = 3f * u, perRow = ROUNDS / 2f; float rowW = perRow * rw + (perRow - 1) * gap, l = rr - 150f * u - rowW; for (int i = 0; i < ROUNDS; i++) { int row = i / (ROUNDS / 2), col = i % (ROUNDS / 2); float x = l + col * (rw + gap), y = t + 8f * u + row * (rh + 8f * u); r.set(x, y, x + rw, y + rh); fill.setColor(i < lit ? light : AccentColor.withAlpha(main, 0x26)); c.drawRect(r, fill); } // The grenade slots: P R N D. String[] gears = { "P", "R", "N", "D" }; String now = Gear.label(v.gear); float gs = 34f * u, gx = l, gy = t + 66f * u; for (int i = 0; i < gears.length; i++) { boolean on = gears[i].equals(now); float x = gx + i * (gs + 6f * u); path.reset(); path.moveTo(x + gs / 2f, gy); path.lineTo(x + gs, gy + gs * 0.5f); path.lineTo(x + gs / 2f, gy + gs); path.lineTo(x, gy + gs * 0.5f); path.close(); fill.setColor(on ? main : 0x55000000); c.drawPath(path, fill); line.setColor(on ? light : dim); line.setStrokeWidth(1.5f * u); c.drawPath(path, line); label(c, gears[i], x + gs / 2f, gy + gs * 0.5f + 7f * u, 20f * u, on ? 0xFF03080D : main, face, Paint.Align.CENTER); } } // ---------- Reticle ---------- private void drawReticle(Canvas c, float w, float h, float u) { float cx = w / 2f, cy = h * 0.53f, rad = 122f * u; line.setStrokeWidth(2f * u); line.setColor(AccentColor.withAlpha(main, 0x70)); c.drawCircle(cx, cy, rad, line); // Four ticks. line.setColor(main); line.setStrokeWidth(3f * u); c.drawLine(cx, cy - rad - 16f * u, cx, cy - rad + 10f * u, line); c.drawLine(cx, cy + rad - 10f * u, cx, cy + rad + 16f * u, line); c.drawLine(cx - rad - 16f * u, cy, cx - rad + 10f * u, cy, line); c.drawLine(cx + rad - 10f * u, cy, cx + rad + 16f * u, cy, line); // Power round it: 150 kW is a full turn, regeneration 50 kW the other way. r.set(cx - rad - 10f * u, cy - rad - 10f * u, cx + rad + 10f * u, cy + rad + 10f * u); line.setStrokeWidth(8f * u); line.setStrokeCap(Paint.Cap.BUTT); line.setColor(AccentColor.withAlpha(main, 0x1C)); c.drawArc(r, 0, 360, false, line); if (v.powerKw != null && Math.abs(v.powerKw) > 0.1f) { boolean regen = v.powerKw < 0; float sweepDeg = regen ? Math.max(-360f, v.powerKw / 50f * 360f) : Math.min(360f, v.powerKw / 150f * 360f); line.setColor(regen ? REGEN : light); c.drawArc(r, -90f, sweepDeg, false, line); } label(c, fmt(v.speedKmh, "%.0f", "—"), cx, cy + 34f * u, 104f * u, light, face, Paint.Align.CENTER); label(c, "KM/H", cx, cy + 66f * u, 18f * u, main, face, Paint.Align.CENTER); String power = v.powerKw == null ? "— kW" : String.format(Locale.US, v.powerKw < -0.05f ? "+%.1f kW" : "%.1f kW", Math.abs(v.powerKw)); label(c, power, cx, cy + rad + 48f * u, 24f * u, v.powerKw != null && v.powerKw < -0.05f ? REGEN : light, face, Paint.Align.CENTER); } // ---------- Motion tracker ---------- /** The tracker: rings, the sweep while moving, and the power trail as blips round the car. */ private void drawTracker(Canvas c, float h, float u) { float rad = 112f * u, cx = 40f * u + rad, cy = h - 34f * u - rad; fill.setColor(AccentColor.withAlpha(main, 0x14)); c.drawCircle(cx, cy, rad, fill); line.setStrokeWidth(1.5f * u); line.setColor(AccentColor.withAlpha(main, 0x90)); c.drawCircle(cx, cy, rad, line); line.setColor(AccentColor.withAlpha(main, 0x3A)); c.drawCircle(cx, cy, rad * 0.66f, line); c.drawCircle(cx, cy, rad * 0.33f, line); c.drawLine(cx - rad, cy, cx + rad, cy, line); c.drawLine(cx, cy - rad, cx, cy + rad, line); if (v.speedKmh != null && v.speedKmh > 0.5f) { float a = (float) Math.toRadians(sweep - 90f); line.setColor(AccentColor.withAlpha(light, 0x90)); line.setStrokeWidth(2f * u); c.drawLine(cx, cy, cx + (float) Math.cos(a) * rad, cy + (float) Math.sin(a) * rad, line); } // Blips: the last two minutes, the newest nearest the top, each at the distance its power gives. float[] trail = v.powerTrail; if (trail != null && trail.length > 0) { int step = Math.max(1, trail.length / 24); for (int i = trail.length - 1, k = 0; i >= 0; i -= step, k++) { float p = trail[i]; if (Float.isNaN(p) || Math.abs(p) < 1f) continue; float dist = (0.18f + 0.78f * Math.min(1f, Math.abs(p) / (p < 0 ? 40f : 60f))) * rad; float ang = (float) Math.toRadians(-90f + k * 15f); float bx = cx + (float) Math.cos(ang) * dist, by = cy + (float) Math.sin(ang) * dist; int colour = p < 0 ? REGEN : ENEMY; fill.setColor(AccentColor.withAlpha(colour, Math.max(0x40, 0xFF - k * 8))); c.drawCircle(bx, by, 6f * u, fill); } } // The car in the middle. path.reset(); path.moveTo(cx, cy - 10f * u); path.lineTo(cx + 7f * u, cy + 8f * u); path.lineTo(cx - 7f * u, cy + 8f * u); path.close(); fill.setColor(light); c.drawPath(path, fill); label(c, "60 kW", cx, cy - rad - 8f * u, 13f * u, main, thin, Paint.Align.CENTER); } // ---------- Objective ---------- private void drawObjective(Canvas c, float w, float h, float u) { float rr = w - 30f * u, l = rr - 330f * u, t = h - 214f * u, b = h - 34f * u; slant(c, l, t, rr, b, 18f * u, AccentColor.withAlpha(main, 0x80), 0x80000000, u); label(c, "OBJECTIVE", l + 30f * u, t + 30f * u, 18f * u, main, face, Paint.Align.LEFT); String[][] rows = { { "TRIP", fmt(v.tripKm, "%.1f km", "—") }, { "5 MIN", fmt(v.usage5Min, "%.1f kWh/100", "—") }, { "SINCE START", fmt(v.usageTrip, "%.1f kWh/100", "—") }, { "CLIMATE", fmt(v.climateCelsius, "%.0f °C", "—") }, { "ODOMETER", fmt(v.odometerKm, "%,.0f km", "—") }, }; for (int i = 0; i < rows.length; i++) { float y = t + (62f + i * 27f) * u, x = l + 26f * u - i * 3f * u; label(c, rows[i][0], x, y, 17f * u, main, thin, Paint.Align.LEFT); label(c, rows[i][1], rr - 30f * u - i * 3f * u, y, 19f * u, light, face, Paint.Align.RIGHT); } } }