package com.evsuite.abrp; import android.content.Context; import android.graphics.Canvas; import android.graphics.Paint; import android.graphics.RectF; import android.util.AttributeSet; import android.view.View; import java.util.Locale; /** * A round instrument for the dashboard Overview: a 240° scale open at the bottom, major and minor * ticks, numbers, a needle, and up to three lines of text in the middle — the unit, the value as * digits, and a caption. * * Drawn on a Canvas with colours resolved from the accent theme, so it needs no gradient * drawables (which crash Android 9 with theme colours). A null value draws no needle and a dash: * an unread value is never shown as zero. The needle eases towards a new value instead of * jumping, and stops redrawing once it is there. */ public final class GaugeView extends View { private static final float START_DEG = 150f; private static final float SWEEP_DEG = 240f; private final Paint arc = new Paint(Paint.ANTI_ALIAS_FLAG); private final Paint glow = new Paint(Paint.ANTI_ALIAS_FLAG); private final Paint tick = new Paint(Paint.ANTI_ALIAS_FLAG); private final Paint warnTick = new Paint(Paint.ANTI_ALIAS_FLAG); private final Paint number = new Paint(Paint.ANTI_ALIAS_FLAG); private final Paint unitPaint = new Paint(Paint.ANTI_ALIAS_FLAG); private final Paint digits = new Paint(Paint.ANTI_ALIAS_FLAG); private final Paint caption = new Paint(Paint.ANTI_ALIAS_FLAG); private final Paint needle = new Paint(Paint.ANTI_ALIAS_FLAG); private final Paint hub = new Paint(Paint.ANTI_ALIAS_FLAG); private final RectF oval = new RectF(); private float min = 0f, max = 100f, major = 20f; private int minorPerMajor = 4; private boolean numbers = true; private String lowLabel, highLabel; private Float warnBelow; private String unit = "", valueText = "—", captionText; private Float target; private float shown = Float.NaN; public GaugeView(Context context, AttributeSet attrs) { super(context, attrs); int accent = AccentColor.resolve(context, R.attr.evAccent); int light = AccentColor.resolve(context, R.attr.evAccentLight); arc.setStyle(Paint.Style.STROKE); arc.setStrokeCap(Paint.Cap.ROUND); arc.setColor(accent); glow.setStyle(Paint.Style.STROKE); glow.setStrokeCap(Paint.Cap.ROUND); glow.setColor(AccentColor.withAlpha(accent, 0x40)); tick.setColor(accent); tick.setStrokeCap(Paint.Cap.ROUND); warnTick.setColor(0xFFFFB4AB); warnTick.setStrokeCap(Paint.Cap.ROUND); number.setColor(0xFFF4F3EF); number.setTextAlign(Paint.Align.CENTER); unitPaint.setColor(0xFFBDC4C6); unitPaint.setTextAlign(Paint.Align.CENTER); digits.setColor(light); digits.setTextAlign(Paint.Align.CENTER); digits.setFontFeatureSettings("tnum"); caption.setColor(0xFFBDC4C6); caption.setTextAlign(Paint.Align.CENTER); caption.setFontFeatureSettings("tnum"); needle.setColor(0xFFF4F3EF); needle.setStrokeCap(Paint.Cap.ROUND); hub.setColor(accent); } /** The scale: range, spacing of the numbered ticks, and minor ticks between two of them. */ void configure(float min, float max, float major, int minorPerMajor, String unit) { this.min = min; this.max = max; this.major = major; this.minorPerMajor = minorPerMajor; this.unit = unit; invalidate(); } /** Letters at the two ends (E and F) instead of numbers along the scale. */ void setEndLabels(String low, String high) { lowLabel = low; highLabel = high; numbers = false; invalidate(); } /** Ticks below this value drawn in the warning colour (a low battery). */ void setWarnBelow(Float value) { warnBelow = value; invalidate(); } /** * The needle's value, null when unread, and its digits ("—" then) and caption. Drawing is * left to the next frame, and only the needle animates. */ void setValue(Float value, String valueText, String captionText) { target = value == null || !Float.isFinite(value) ? null : value; this.valueText = valueText == null ? "—" : valueText; this.captionText = captionText; if (target != null && Float.isNaN(shown)) shown = target; invalidate(); } private float angleOf(float v) { float f = (Math.max(min, Math.min(max, v)) - min) / (max - min); return START_DEG + SWEEP_DEG * f; } @Override protected void onDraw(Canvas c) { super.onDraw(c); float w = getWidth(), h = getHeight(); // Below the centre: the unit, the digits and two caption lines, down to 0.9 r. float r = Math.min(w / 2f, h / 1.92f) * 0.95f; if (r <= 0f) return; float cx = w / 2f, cy = (h - 1.9f * r) / 2f + r; float stroke = r * 0.035f; arc.setStrokeWidth(stroke); glow.setStrokeWidth(stroke * 2.5f); oval.set(cx - r, cy - r, cx + r, cy + r); c.drawArc(oval, START_DEG, SWEEP_DEG, false, glow); c.drawArc(oval, START_DEG, SWEEP_DEG, false, arc); // Ticks: every minor step; the numbered ones longer and thicker. number.setTextSize(r * 0.105f); float minor = major / Math.max(1, minorPerMajor); int steps = Math.round((max - min) / minor); for (int i = 0; i <= steps; i++) { float v = min + i * minor; boolean isMajor = i % Math.max(1, minorPerMajor) == 0; double a = Math.toRadians(angleOf(v)); float outer = r * 0.90f, inner = isMajor ? r * 0.76f : r * 0.83f; Paint p = warnBelow != null && v < warnBelow ? warnTick : tick; p.setStrokeWidth(isMajor ? r * 0.022f : r * 0.012f); float cos = (float) Math.cos(a), sin = (float) Math.sin(a); c.drawLine(cx + outer * cos, cy + outer * sin, cx + inner * cos, cy + inner * sin, p); if (isMajor && numbers) { float lr = r * 0.62f; c.drawText(String.format(Locale.US, "%.0f", v), cx + lr * cos, cy + lr * sin + number.getTextSize() * 0.36f, number); } } if (!numbers) { drawEndLabel(c, cx, cy, r, min, lowLabel, warnBelow != null ? warnTick.getColor() : number.getColor()); drawEndLabel(c, cx, cy, r, max, highLabel, number.getColor()); } // Under the hub, clear of the needle's sweep: unit, digits, caption. unitPaint.setTextSize(r * 0.095f); c.drawText(unit, cx, cy + r * 0.25f, unitPaint); digits.setTextSize(r * 0.22f); c.drawText(valueText, cx, cy + r * 0.50f, digits); if (captionText != null) { caption.setTextSize(r * 0.09f); float y = cy + r * 0.68f; for (String line : captionText.split("\n")) { c.drawText(line, cx, y, caption); y += caption.getTextSize() * 1.25f; } } if (target != null) { shown += (target - shown) * 0.25f; if (Math.abs(target - shown) < (max - min) * 0.001f) shown = target; double a = Math.toRadians(angleOf(shown)); needle.setStrokeWidth(r * 0.03f); c.drawLine(cx, cy, cx + r * 0.80f * (float) Math.cos(a), cy + r * 0.80f * (float) Math.sin(a), needle); c.drawCircle(cx, cy, r * 0.065f, hub); if (shown != target) postInvalidateOnAnimation(); } else { c.drawCircle(cx, cy, r * 0.065f, hub); } } private void drawEndLabel(Canvas c, float cx, float cy, float r, float v, String label, int color) { if (label == null) return; double a = Math.toRadians(angleOf(v)); float lr = r * 0.62f; int old = number.getColor(); number.setColor(color); number.setTextSize(r * 0.16f); c.drawText(label, cx + lr * (float) Math.cos(a), cy + lr * (float) Math.sin(a) + number.getTextSize() * 0.36f, number); number.setColor(old); } }