Files
mg4/app/src/main/java/com/evsuite/abrp/GaugeView.java
T
nilsandClaude Opus 5.5 234a7e1b60 App 2.21.0: Overview style Dashboard, an instrument cluster
Power, speed and battery as gauges and a strip of gear, climate,
clock, usage and battery; Settings picks Tiles or Dashboard.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LhzAVaUyqcw5PkGjz4mWW6
2026-10-24 12:21:58 +02:00

196 lines
8.0 KiB
Java

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);
}
}