App 2.25.0: Overview style MG, octagonal dials
After the classic MG dashboards: three octagonal dials with chrome bezels, cream scales and accent-coloured needles for speed, battery and power, and a cream line of odometer, trip, usage, climate and clock. No MG logo. Emulator only. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LhzAVaUyqcw5PkGjz4mWW6
This commit is contained in:
@@ -65,6 +65,7 @@ public class MainActivity extends AppCompatActivity {
|
||||
private SciFiView sciFiView;
|
||||
private CartoonView cartoonView;
|
||||
private BlueprintView blueprintView;
|
||||
private OctagonView octagonView;
|
||||
/** The pane of each Overview style, by its index in {@link #OVERVIEW_STYLES}. */
|
||||
private View[] stylePanes;
|
||||
private View consumptionPane;
|
||||
@@ -250,10 +251,11 @@ public class MainActivity extends AppCompatActivity {
|
||||
sciFiView = new SciFiView(this, null);
|
||||
cartoonView = new CartoonView(this, null);
|
||||
blueprintView = new BlueprintView(this, null);
|
||||
octagonView = new OctagonView(this, null);
|
||||
View sciFiPane = drawnPane(sciFiView), cartoonPane = drawnPane(cartoonView),
|
||||
blueprintPane = drawnPane(blueprintView);
|
||||
blueprintPane = drawnPane(blueprintView), mgPane = drawnPane(octagonView);
|
||||
stylePanes = new View[] { vehiclePane, gaugesPane, cockpitPane, retroPane, retroPane,
|
||||
futuristicPane, sciFiPane, cartoonPane, blueprintPane };
|
||||
futuristicPane, sciFiPane, cartoonPane, blueprintPane, mgPane };
|
||||
// The Overview page holds every style; Settings → Overview style picks the one shown.
|
||||
android.widget.FrameLayout overviewPage = new android.widget.FrameLayout(this);
|
||||
overviewPage.setLayoutParams(new android.view.ViewGroup.LayoutParams(
|
||||
@@ -267,6 +269,7 @@ public class MainActivity extends AppCompatActivity {
|
||||
overviewPage.addView(sciFiPane);
|
||||
overviewPage.addView(cartoonPane);
|
||||
overviewPage.addView(blueprintPane);
|
||||
overviewPage.addView(mgPane);
|
||||
consumptionPane = inflatePane(R.layout.pane_consumption);
|
||||
chargingPane = inflatePane(R.layout.pane_charging);
|
||||
settingsPane = inflatePane(R.layout.pane_settings);
|
||||
@@ -1451,7 +1454,7 @@ public class MainActivity extends AppCompatActivity {
|
||||
|
||||
/** The stored values of the Overview styles, in the order of the drop-down. */
|
||||
private static final String[] OVERVIEW_STYLES = { "TILES", "GAUGES", "COCKPIT", "RETRO", "RETRO_ACCENT", "FUTURISTIC",
|
||||
"SCIFI", "CARTOON", "BLUEPRINT" };
|
||||
"SCIFI", "CARTOON", "BLUEPRINT", "MG" };
|
||||
|
||||
/** The chosen style's index in {@link #OVERVIEW_STYLES}; anything unknown is Tiles. */
|
||||
private int overviewStyle() {
|
||||
@@ -1474,7 +1477,7 @@ public class MainActivity extends AppCompatActivity {
|
||||
getString(R.string.overview_style_cockpit), getString(R.string.overview_style_retro),
|
||||
getString(R.string.overview_style_retro_accent), getString(R.string.overview_style_futuristic),
|
||||
getString(R.string.overview_style_scifi), getString(R.string.overview_style_cartoon),
|
||||
getString(R.string.overview_style_blueprint) };
|
||||
getString(R.string.overview_style_blueprint), getString(R.string.overview_style_mg) };
|
||||
bindChoices(settingsPane.findViewById(R.id.overview_style_choices), labels, overviewStyle(),
|
||||
position -> {
|
||||
prefs.edit().putString(PREF_OVERVIEW_STYLE, OVERVIEW_STYLES[position]).apply();
|
||||
@@ -1536,6 +1539,7 @@ public class MainActivity extends AppCompatActivity {
|
||||
sciFiView.setValues(v);
|
||||
cartoonView.setValues(v);
|
||||
blueprintView.setValues(v);
|
||||
octagonView.setValues(v);
|
||||
}
|
||||
|
||||
// ---------- Cockpit (Overview style) ----------
|
||||
|
||||
@@ -0,0 +1,280 @@
|
||||
package com.evsuite.abrp;
|
||||
|
||||
import android.content.Context;
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.Paint;
|
||||
import android.graphics.Path;
|
||||
import android.graphics.RectF;
|
||||
import android.graphics.Typeface;
|
||||
import android.util.AttributeSet;
|
||||
import android.view.View;
|
||||
|
||||
import java.util.Locale;
|
||||
|
||||
/**
|
||||
* The MG Overview style, after the classic MG dashboards whose dials sat in octagonal bezels.
|
||||
*
|
||||
* Three octagonal dials with chrome bezels, cream scales and needles in the accent colour: the
|
||||
* speedometer in the middle (with the speed as digits and P R N D underneath), the battery on
|
||||
* the left (E to F, red below 20 %, range underneath) and power on the right (−50 to 150 kW,
|
||||
* the charge zone below zero in green). Below, a cream line of odometer, trip, usage, climate
|
||||
* and clock, separated by small octagons. Unread values draw no needle and "—".
|
||||
*
|
||||
* Needles ease to a new value and the view redraws only until they are there, only on screen.
|
||||
* The MG octagon logo itself is not drawn.
|
||||
*/
|
||||
public final class OctagonView extends View {
|
||||
|
||||
private static final float START_DEG = 135f, SWEEP_DEG = 270f;
|
||||
private static final long FRAME_MS = 33;
|
||||
|
||||
private final Paint face = new Paint(Paint.ANTI_ALIAS_FLAG);
|
||||
private final Paint chrome = new Paint(Paint.ANTI_ALIAS_FLAG);
|
||||
private final Paint tick = new Paint(Paint.ANTI_ALIAS_FLAG);
|
||||
private final Paint needle = new Paint(Paint.ANTI_ALIAS_FLAG);
|
||||
private final Paint zone = new Paint(Paint.ANTI_ALIAS_FLAG);
|
||||
private final Paint text = new Paint(Paint.ANTI_ALIAS_FLAG);
|
||||
private final Paint fill = new Paint(Paint.ANTI_ALIAS_FLAG);
|
||||
private final Path path = new Path();
|
||||
private final RectF rect = new RectF();
|
||||
private final int accent, light, cream, chromeLight, chromeDark, faceColour, green, red;
|
||||
private ClusterValues v = new ClusterValues();
|
||||
/** The needles as shown, easing to their values; NaN until the first reading. */
|
||||
private float speedShown = Float.NaN, socShown = Float.NaN, powerShown = Float.NaN;
|
||||
|
||||
public OctagonView(Context context, AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
accent = AccentColor.resolve(context, R.attr.evAccent);
|
||||
light = AccentColor.resolve(context, R.attr.evAccentLight);
|
||||
cream = context.getColor(R.color.mg_cream);
|
||||
chromeLight = context.getColor(R.color.mg_chrome_light);
|
||||
chromeDark = context.getColor(R.color.mg_chrome_dark);
|
||||
faceColour = context.getColor(R.color.mg_face);
|
||||
green = context.getColor(R.color.ev_ok);
|
||||
red = context.getColor(R.color.ev_error);
|
||||
face.setColor(faceColour);
|
||||
chrome.setStyle(Paint.Style.STROKE);
|
||||
chrome.setStrokeJoin(Paint.Join.ROUND);
|
||||
tick.setColor(cream);
|
||||
tick.setStrokeCap(Paint.Cap.ROUND);
|
||||
needle.setStrokeCap(Paint.Cap.ROUND);
|
||||
zone.setStyle(Paint.Style.STROKE);
|
||||
text.setColor(cream);
|
||||
text.setTextAlign(Paint.Align.CENTER);
|
||||
text.setFontFeatureSettings("tnum");
|
||||
text.setTypeface(Typeface.create("serif", Typeface.NORMAL));
|
||||
}
|
||||
|
||||
void setValues(ClusterValues values) {
|
||||
v = values == null ? new ClusterValues() : values;
|
||||
invalidate();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDraw(Canvas c) {
|
||||
super.onDraw(c);
|
||||
float w = getWidth(), h = getHeight();
|
||||
if (w <= 0 || h <= 0) return;
|
||||
float d = getResources().getDisplayMetrics().density;
|
||||
|
||||
speedShown = ease(speedShown, v.speedKmh, 180f);
|
||||
socShown = ease(socShown, v.socPercent, 100f);
|
||||
powerShown = ease(powerShown, v.powerKw, 200f);
|
||||
boolean moving = (v.speedKmh != null && speedShown != v.speedKmh)
|
||||
|| (v.socPercent != null && socShown != v.socPercent)
|
||||
|| (v.powerKw != null && powerShown != v.powerKw);
|
||||
|
||||
float dialsBottom = h * 0.84f;
|
||||
float big = Math.min(dialsBottom * 0.5f, w * 0.20f);
|
||||
float small = big * 0.68f;
|
||||
float cy = dialsBottom / 2f + 4f * d;
|
||||
float cx = w / 2f;
|
||||
float sideX = Math.min(w * 0.21f, cx - big - small - 20f * d);
|
||||
|
||||
// Battery, left.
|
||||
bezel(c, sideX + small * 0.05f, cy + big - small, small, d);
|
||||
dial(c, sideX + small * 0.05f, cy + big - small, small, 0f, 100f, 25f, 5,
|
||||
v.socPercent == null ? null : socShown, 20f, false, new String[] { "E", "½", "F" }, d);
|
||||
label(c, sideX + small * 0.05f, cy + big - small, small, "BATTERY",
|
||||
v.socPercent == null ? "—" : String.format(Locale.US, "%.0f%%", v.socPercent),
|
||||
v.rangeKm == null ? "— km" : String.format(Locale.US, "%.0f km", v.rangeKm), 0.20f);
|
||||
|
||||
// Power, right.
|
||||
float rx = w - sideX - small * 0.05f;
|
||||
bezel(c, rx, cy + big - small, small, d);
|
||||
dial(c, rx, cy + big - small, small, -50f, 150f, 50f, 5,
|
||||
v.powerKw == null ? null : powerShown, null, true, null, d);
|
||||
label(c, rx, cy + big - small, small, "kW",
|
||||
v.powerKw == null ? "—" : String.format(Locale.US, "%.1f", v.powerKw),
|
||||
v.powerKw != null && v.powerKw < 0f ? "CHARGE" : "POWER", 0.20f);
|
||||
|
||||
// Speed, centre.
|
||||
bezel(c, cx, cy, big, d);
|
||||
dial(c, cx, cy, big, 0f, 180f, 20f, 4, v.speedKmh == null ? null : speedShown, null, false, null, d);
|
||||
label(c, cx, cy, big, "km/h",
|
||||
v.speedKmh == null ? "—" : String.format(Locale.US, "%.0f", v.speedKmh), null, 0.24f);
|
||||
gears(c, cx, cy + big * 0.62f, big, d);
|
||||
|
||||
drawLine(c, w, h, d);
|
||||
|
||||
if (moving && isShown()) postInvalidateDelayed(FRAME_MS);
|
||||
}
|
||||
|
||||
/** One step of a needle towards its value; jumps there when close or on the first value. */
|
||||
private static float ease(float shown, Float target, float span) {
|
||||
if (target == null || !Float.isFinite(target)) return shown;
|
||||
if (Float.isNaN(shown)) return target;
|
||||
float next = shown + (target - shown) * 0.25f;
|
||||
return Math.abs(target - next) < span * 0.002f ? target : next;
|
||||
}
|
||||
|
||||
/** A regular octagon with flat sides at top, bottom and both sides. */
|
||||
private void octagon(float cx, float cy, float r) {
|
||||
path.reset();
|
||||
for (int i = 0; i < 8; i++) {
|
||||
double a = Math.toRadians(22.5 + 45 * i);
|
||||
float x = cx + r * (float) Math.cos(a), y = cy + r * (float) Math.sin(a);
|
||||
if (i == 0) path.moveTo(x, y); else path.lineTo(x, y);
|
||||
}
|
||||
path.close();
|
||||
}
|
||||
|
||||
/** The bezel: a dark face inside a chrome octagon, lit from above (light outer, dark inner line). */
|
||||
private void bezel(Canvas c, float cx, float cy, float r, float d) {
|
||||
octagon(cx, cy, r);
|
||||
c.drawPath(path, face);
|
||||
chrome.setColor(chromeLight);
|
||||
chrome.setStrokeWidth(Math.max(3f * d, r * 0.035f));
|
||||
c.drawPath(path, chrome);
|
||||
octagon(cx, cy, r * 0.93f);
|
||||
chrome.setColor(chromeDark);
|
||||
chrome.setStrokeWidth(Math.max(2f * d, r * 0.02f));
|
||||
c.drawPath(path, chrome);
|
||||
octagon(cx, cy, r * 1.035f);
|
||||
chrome.setColor(AccentColor.withAlpha(chromeLight, 0x40));
|
||||
chrome.setStrokeWidth(d);
|
||||
c.drawPath(path, chrome);
|
||||
}
|
||||
|
||||
/**
|
||||
* A scale inside the octagon, its numbers or end labels, an optional warning zone below
|
||||
* {@code warnBelow} (red) or charge zone below zero (green), and the needle when known.
|
||||
*/
|
||||
private void dial(Canvas c, float cx, float cy, float r, float min, float max, float major,
|
||||
int minorPer, Float value, Float warnBelow, boolean chargeZone, String[] labels, float d) {
|
||||
float sr = r * 0.74f;
|
||||
rect.set(cx - sr, cy - sr, cx + sr, cy + sr);
|
||||
zone.setStrokeWidth(r * 0.05f);
|
||||
if (warnBelow != null) {
|
||||
zone.setColor(AccentColor.withAlpha(red, 0xC0));
|
||||
c.drawArc(rect, START_DEG, SWEEP_DEG * (warnBelow - min) / (max - min), false, zone);
|
||||
}
|
||||
if (chargeZone && min < 0f) {
|
||||
zone.setColor(AccentColor.withAlpha(green, 0xC0));
|
||||
c.drawArc(rect, START_DEG, SWEEP_DEG * (0f - min) / (max - min), false, zone);
|
||||
}
|
||||
float minor = major / minorPer;
|
||||
int steps = Math.round((max - min) / minor);
|
||||
text.setTextSize(r * 0.13f);
|
||||
for (int i = 0; i <= steps; i++) {
|
||||
float val = min + i * minor;
|
||||
boolean isMajor = i % minorPer == 0;
|
||||
double a = Math.toRadians(START_DEG + SWEEP_DEG * (val - min) / (max - min));
|
||||
float cos = (float) Math.cos(a), sin = (float) Math.sin(a);
|
||||
float outer = r * 0.82f, inner = isMajor ? r * 0.68f : r * 0.75f;
|
||||
tick.setStrokeWidth(isMajor ? r * 0.025f : r * 0.012f);
|
||||
c.drawLine(cx + outer * cos, cy + outer * sin, cx + inner * cos, cy + inner * sin, tick);
|
||||
if (isMajor && labels == null) {
|
||||
float lr = r * 0.54f;
|
||||
c.drawText(String.format(Locale.US, "%.0f", val), cx + lr * cos,
|
||||
cy + lr * sin + text.getTextSize() * 0.36f, text);
|
||||
}
|
||||
}
|
||||
if (labels != null) {
|
||||
text.setTextSize(r * 0.16f);
|
||||
for (int i = 0; i < labels.length; i++) {
|
||||
double a = Math.toRadians(START_DEG + SWEEP_DEG * i / (labels.length - 1f));
|
||||
float lr = r * 0.52f;
|
||||
c.drawText(labels[i], cx + lr * (float) Math.cos(a), cy + lr * (float) Math.sin(a) + text.getTextSize() * 0.36f, text);
|
||||
}
|
||||
}
|
||||
if (value != null) {
|
||||
double a = Math.toRadians(START_DEG + SWEEP_DEG * (Math.max(min, Math.min(max, value)) - min) / (max - min));
|
||||
float cos = (float) Math.cos(a), sin = (float) Math.sin(a);
|
||||
needle.setColor(AccentColor.withAlpha(accent, 0x50));
|
||||
needle.setStrokeWidth(r * 0.06f);
|
||||
c.drawLine(cx - r * 0.12f * cos, cy - r * 0.12f * sin, cx + r * 0.80f * cos, cy + r * 0.80f * sin, needle);
|
||||
needle.setColor(accent);
|
||||
needle.setStrokeWidth(r * 0.03f);
|
||||
c.drawLine(cx - r * 0.12f * cos, cy - r * 0.12f * sin, cx + r * 0.80f * cos, cy + r * 0.80f * sin, needle);
|
||||
}
|
||||
// The hub: a small chrome octagon.
|
||||
octagon(cx, cy, r * 0.08f);
|
||||
fill.setColor(chromeLight);
|
||||
c.drawPath(path, fill);
|
||||
octagon(cx, cy, r * 0.045f);
|
||||
fill.setColor(faceColour);
|
||||
c.drawPath(path, fill);
|
||||
}
|
||||
|
||||
/** Under the hub: a small caption, the value large, and a line below it. */
|
||||
private void label(Canvas c, float cx, float cy, float r, String caption, String value, String below,
|
||||
float valueSize) {
|
||||
text.setTextSize(r * 0.10f);
|
||||
text.setColor(AccentColor.withAlpha(cream, 0xB0));
|
||||
text.setLetterSpacing(0.15f);
|
||||
c.drawText(caption, cx, cy + r * 0.25f, text);
|
||||
text.setLetterSpacing(0f);
|
||||
text.setColor(cream);
|
||||
text.setTextSize(r * valueSize);
|
||||
c.drawText(value, cx, cy + r * 0.50f, text);
|
||||
if (below != null) {
|
||||
text.setTextSize(r * 0.11f);
|
||||
text.setColor(light);
|
||||
c.drawText(below, cx, cy + r * 0.67f, text);
|
||||
text.setColor(cream);
|
||||
}
|
||||
}
|
||||
|
||||
/** P R N D under the speed; the engaged one in the accent colour. */
|
||||
private void gears(Canvas c, float cx, float y, float r, float d) {
|
||||
String[] letters = { "P", "R", "N", "D" };
|
||||
text.setTextSize(r * 0.10f);
|
||||
float pitch = r * 0.13f;
|
||||
for (int i = 0; i < 4; i++) {
|
||||
boolean on = v.gear != null && v.gear == i + 1;
|
||||
text.setColor(on ? light : AccentColor.withAlpha(cream, 0x55));
|
||||
text.setFakeBoldText(on);
|
||||
c.drawText(letters[i], cx + (i - 1.5f) * pitch, y + r * 0.10f, text);
|
||||
}
|
||||
text.setFakeBoldText(false);
|
||||
text.setColor(cream);
|
||||
}
|
||||
|
||||
/** The cream line along the bottom, items separated by small octagons. */
|
||||
private void drawLine(Canvas c, float w, float h, float d) {
|
||||
String[] items = {
|
||||
v.odometerKm == null ? "ODO —" : String.format(Locale.US, "ODO %,.0f km", v.odometerKm),
|
||||
v.tripKm == null ? "TRIP —" : String.format(Locale.US, "TRIP %.1f km", v.tripKm),
|
||||
v.usage5Min == null ? "— kWh/100 km" : String.format(Locale.US, "%.1f kWh/100 km", v.usage5Min),
|
||||
v.climateCelsius == null ? "— °C" : String.format(Locale.US, "%.0f °C", v.climateCelsius),
|
||||
v.clock.isEmpty() ? "—" : v.clock,
|
||||
};
|
||||
float y = h * 0.95f;
|
||||
text.setTextSize(20f * d);
|
||||
text.setLetterSpacing(0.08f);
|
||||
float pitch = w / items.length;
|
||||
for (int i = 0; i < items.length; i++) {
|
||||
c.drawText(items[i], pitch * (i + 0.5f), y, text);
|
||||
if (i > 0) {
|
||||
octagon(pitch * i, y - 7f * d, 5f * d);
|
||||
fill.setColor(accent);
|
||||
c.drawPath(path, fill);
|
||||
}
|
||||
}
|
||||
text.setLetterSpacing(0f);
|
||||
chrome.setColor(chromeDark);
|
||||
chrome.setStrokeWidth(d);
|
||||
c.drawLine(w * 0.06f, h * 0.885f, w * 0.94f, h * 0.885f, chrome);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user