App 2.35.0: the app takes on the Overview style's own colours
While the style shows its own palette (Sci-fi, Cartoon, Aviation, Nixie; Retro and Terminal with "Original"), the navigation, buttons and titles take it instead of the accent colour, e.g. turquoise for Retro (StylePalette, style_palettes.xml). Blueprint and Mascot are removed; a stored one opens Tiles. The navigation no longer saves view state, which crashed a restore across the wide and narrow layouts. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LhzAVaUyqcw5PkGjz4mWW6
This commit is contained in:
@@ -1,283 +0,0 @@
|
||||
package com.evsuite.abrp;
|
||||
|
||||
import android.content.Context;
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.DashPathEffect;
|
||||
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 Blueprint Overview style: a technical drawing of the car from above, white lines on a navy
|
||||
* sheet with a grid.
|
||||
*
|
||||
* The battery pack between the axles is hatched as far as it is charged. Each tyre has a leader
|
||||
* line to its pressure. Power is a force arrow at the front — out of the car while drawing,
|
||||
* into it (dashed) while regenerating — its length scaled to the power. Dimension lines give the
|
||||
* range and the trip, the cabin is annotated with the climate setpoint, and a title block in the
|
||||
* corner holds speed, power, gear, usage, odometer and time. Unread values show "—", no hatch and
|
||||
* no arrow. Redrawn only when a reading arrives.
|
||||
*/
|
||||
public final class BlueprintView extends View {
|
||||
|
||||
/** The MG4's length over its width, 4.287 m by 1.836 m. */
|
||||
private static final float ASPECT = 4.287f / 1.836f;
|
||||
|
||||
private final Paint sheet = new Paint(Paint.ANTI_ALIAS_FLAG);
|
||||
private final Paint grid = new Paint();
|
||||
private final Paint line = new Paint(Paint.ANTI_ALIAS_FLAG);
|
||||
private final Paint faint = new Paint(Paint.ANTI_ALIAS_FLAG);
|
||||
private final Paint dashed = new Paint(Paint.ANTI_ALIAS_FLAG);
|
||||
private final Paint text = new Paint(Paint.ANTI_ALIAS_FLAG);
|
||||
private final Path path = new Path();
|
||||
private final RectF rect = new RectF();
|
||||
private final int ink;
|
||||
private ClusterValues v = new ClusterValues();
|
||||
|
||||
public BlueprintView(Context context, AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
float d = context.getResources().getDisplayMetrics().density;
|
||||
ink = context.getColor(R.color.blueprint_line);
|
||||
sheet.setColor(context.getColor(R.color.blueprint_sheet));
|
||||
grid.setStyle(Paint.Style.STROKE);
|
||||
line.setStyle(Paint.Style.STROKE);
|
||||
line.setColor(ink);
|
||||
line.setStrokeJoin(Paint.Join.ROUND);
|
||||
line.setStrokeCap(Paint.Cap.ROUND);
|
||||
faint.setStyle(Paint.Style.STROKE);
|
||||
faint.setColor(AccentColor.withAlpha(ink, 0x99));
|
||||
dashed.setStyle(Paint.Style.STROKE);
|
||||
dashed.setColor(AccentColor.withAlpha(ink, 0xB0));
|
||||
dashed.setPathEffect(new DashPathEffect(new float[] { 8f * d, 6f * d }, 0f));
|
||||
text.setColor(ink);
|
||||
text.setTypeface(Typeface.create(Typeface.MONOSPACE, Typeface.NORMAL));
|
||||
text.setFontFeatureSettings("tnum");
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
// The sheet and its grid: fine every 20 dp, bolder every 100 dp; a border inset.
|
||||
rect.set(0, 0, w, h);
|
||||
c.drawRect(rect, sheet);
|
||||
for (float step : new float[] { 20f * d, 100f * d }) {
|
||||
grid.setColor(AccentColor.withAlpha(ink, step > 50f * d ? 0x22 : 0x10));
|
||||
grid.setStrokeWidth(Math.max(1f, d * 0.75f));
|
||||
for (float x = 0; x <= w; x += step) c.drawLine(x, 0, x, h, grid);
|
||||
for (float y = 0; y <= h; y += step) c.drawLine(0, y, w, y, grid);
|
||||
}
|
||||
line.setStrokeWidth(2f * d);
|
||||
c.drawRect(8f * d, 8f * d, w - 8f * d, h - 8f * d, line);
|
||||
|
||||
// The car, front to the right, its size from the space left of the title block.
|
||||
// Centred, with its power arrow, in the space left of the title block.
|
||||
float carH = Math.min(h * 0.40f, w * 0.44f / ASPECT), carW = carH * ASPECT;
|
||||
float l = w * 0.04f + (w * 0.60f - carW - w * 0.12f) / 2f, t = (h - carH) / 2f, r = l + carW, b = t + carH;
|
||||
drawCar(c, l, t, r, b, d);
|
||||
drawTyres(c, l, t, r, b, d);
|
||||
drawPower(c, r, (t + b) / 2f, w * 0.10f, d);
|
||||
drawDimensions(c, l, t, r, b, d);
|
||||
drawTitleBlock(c, w * 0.66f, h * 0.10f, w - 22f * d, h - 22f * d, d);
|
||||
}
|
||||
|
||||
private void drawCar(Canvas c, float l, float t, float r, float b, float d) {
|
||||
float len = r - l, wid = b - t;
|
||||
// Body: rounded, fuller at the front.
|
||||
path.reset();
|
||||
path.moveTo(l + len * 0.10f, t);
|
||||
path.lineTo(r - len * 0.14f, t);
|
||||
path.cubicTo(r - len * 0.02f, t, r, t + wid * 0.12f, r, t + wid * 0.30f);
|
||||
path.lineTo(r, b - wid * 0.30f);
|
||||
path.cubicTo(r, b - wid * 0.12f, r - len * 0.02f, b, r - len * 0.14f, b);
|
||||
path.lineTo(l + len * 0.10f, b);
|
||||
path.cubicTo(l + len * 0.02f, b, l, b - wid * 0.14f, l, b - wid * 0.32f);
|
||||
path.lineTo(l, t + wid * 0.32f);
|
||||
path.cubicTo(l, t + wid * 0.14f, l + len * 0.02f, t, l + len * 0.10f, t);
|
||||
line.setStrokeWidth(2.5f * d);
|
||||
c.drawPath(path, line);
|
||||
// Glass: windscreen, roof and rear window, dashed.
|
||||
rect.set(l + len * 0.24f, t + wid * 0.12f, l + len * 0.74f, b - wid * 0.12f);
|
||||
c.drawRoundRect(rect, wid * 0.12f, wid * 0.12f, dashed);
|
||||
c.drawLine(l + len * 0.62f, t + wid * 0.13f, l + len * 0.62f, b - wid * 0.13f, dashed);
|
||||
c.drawLine(l + len * 0.33f, t + wid * 0.13f, l + len * 0.33f, b - wid * 0.13f, dashed);
|
||||
// Mirrors.
|
||||
float mx = l + len * 0.66f;
|
||||
line.setStrokeWidth(2f * d);
|
||||
c.drawLine(mx, t, mx - len * 0.03f, t - wid * 0.09f, line);
|
||||
c.drawLine(mx - len * 0.03f, t - wid * 0.09f, mx - len * 0.07f, t - wid * 0.02f, line);
|
||||
c.drawLine(mx, b, mx - len * 0.03f, b + wid * 0.09f, line);
|
||||
c.drawLine(mx - len * 0.03f, b + wid * 0.09f, mx - len * 0.07f, b + wid * 0.02f, line);
|
||||
|
||||
// The battery between the axles, hatched as far as it is charged (from the rear).
|
||||
float pl = l + len * 0.25f, pr = l + len * 0.73f, pt = t + wid * 0.24f, pb = b - wid * 0.24f;
|
||||
faint.setStrokeWidth(1.5f * d);
|
||||
c.drawRect(pl, pt, pr, pb, faint);
|
||||
if (v.socPercent != null) {
|
||||
float fx = pl + (pr - pl) * Math.max(0f, Math.min(100f, v.socPercent)) / 100f;
|
||||
c.save();
|
||||
c.clipRect(pl, pt, fx, pb);
|
||||
float step = 9f * d;
|
||||
for (float x = pl - (pb - pt); x < fx; x += step) c.drawLine(x, pb, x + (pb - pt), pt, faint);
|
||||
c.restore();
|
||||
line.setStrokeWidth(2f * d);
|
||||
c.drawLine(fx, pt - 6f * d, fx, pb + 6f * d, line);
|
||||
}
|
||||
text.setTextAlign(Paint.Align.CENTER);
|
||||
text.setTextSize(14f * d);
|
||||
String soc = v.socPercent == null ? "—" : String.format(Locale.US, "%.0f %%", v.socPercent);
|
||||
float labelY = (pt + pb) / 2f + 5f * d;
|
||||
rect.set((pl + pr) / 2f - 70f * d, labelY - 18f * d, (pl + pr) / 2f + 70f * d, labelY + 8f * d);
|
||||
c.drawRect(rect, sheet);
|
||||
c.drawText("BATTERY " + soc, (pl + pr) / 2f, labelY, text);
|
||||
|
||||
// The cabin's climate setpoint, as a note.
|
||||
String climate = v.climateCelsius == null ? "—" : String.format(Locale.US, "%.0f °C", v.climateCelsius);
|
||||
text.setTextSize(12f * d);
|
||||
c.drawText("CABIN SET " + climate, l + len * 0.49f, t + wid * 0.19f, text);
|
||||
}
|
||||
|
||||
private void drawTyres(Canvas c, float l, float t, float r, float b, float d) {
|
||||
float len = r - l, wid = b - t, tw = len * 0.13f, th = wid * 0.13f;
|
||||
float rear = l + len * 0.20f, front = l + len * 0.80f;
|
||||
// Driving to the right, the car's left side is at the top.
|
||||
Object[][] tyres = {
|
||||
{ front, t, v.tyreFl, true, "FL" }, { front, b, v.tyreFr, false, "FR" },
|
||||
{ rear, t, v.tyreRl, true, "RL" }, { rear, b, v.tyreRr, false, "RR" },
|
||||
};
|
||||
line.setStrokeWidth(2f * d);
|
||||
text.setTextSize(15f * d);
|
||||
for (Object[] tyre : tyres) {
|
||||
float x = (Float) tyre[0], y = (Float) tyre[1];
|
||||
boolean top = (Boolean) tyre[3];
|
||||
rect.set(x - tw / 2f, y - th / 2f, x + tw / 2f, y + th / 2f);
|
||||
c.drawRect(rect, sheet);
|
||||
c.drawRoundRect(rect, 4f * d, 4f * d, line);
|
||||
// A leader line out to the pressure.
|
||||
float ly = top ? y - th / 2f - wid * 0.24f : y + th / 2f + wid * 0.24f;
|
||||
float lx = x + len * 0.06f;
|
||||
faint.setStrokeWidth(1.5f * d);
|
||||
c.drawLine(x, top ? y - th / 2f : y + th / 2f, lx, ly, faint);
|
||||
c.drawLine(lx, ly, lx + 110f * d, ly, faint);
|
||||
c.drawCircle(x, top ? y - th / 2f : y + th / 2f, 3f * d, line);
|
||||
text.setTextAlign(Paint.Align.LEFT);
|
||||
c.drawText(tyre[4] + " " + tyre[2], lx + 4f * d, ly - 6f * d, text);
|
||||
}
|
||||
}
|
||||
|
||||
/** Power as a force arrow at the front; length scaled to 150 kW; regeneration points back. */
|
||||
private void drawPower(Canvas c, float x, float y, float maxLen, float d) {
|
||||
text.setTextAlign(Paint.Align.LEFT);
|
||||
text.setTextSize(15f * d);
|
||||
if (v.powerKw == null || Math.abs(v.powerKw) < 0.1f) {
|
||||
c.drawText(v.powerKw == null ? "P = —" : "P = 0 kW", x + 16f * d, y + 5f * d, text);
|
||||
return;
|
||||
}
|
||||
float kw = v.powerKw;
|
||||
float len = Math.max(24f * d, maxLen * Math.min(150f, Math.abs(kw)) / 150f);
|
||||
float x0 = x + 10f * d, x1 = x0 + len;
|
||||
Paint p = kw >= 0f ? line : dashed;
|
||||
p.setStrokeWidth(2.5f * d);
|
||||
c.drawLine(x0, y, x1, y, p);
|
||||
// The head: at the far end drawing, at the car regenerating.
|
||||
float hx = kw >= 0f ? x1 : x0, dir = kw >= 0f ? 1f : -1f, hs = 10f * d;
|
||||
line.setStrokeWidth(2.5f * d);
|
||||
c.drawLine(hx, y, hx - dir * hs, y - hs * 0.6f, line);
|
||||
c.drawLine(hx, y, hx - dir * hs, y + hs * 0.6f, line);
|
||||
c.drawText(String.format(Locale.US, "P = %.1f kW", kw), x0, y - 10f * d, text);
|
||||
if (kw < 0f) c.drawText("(REGEN)", x0, y + 22f * d, text);
|
||||
}
|
||||
|
||||
/** Dimension lines: the range under the car, the trip above it. */
|
||||
private void drawDimensions(Canvas c, float l, float t, float r, float b, float d) {
|
||||
float wid = b - t;
|
||||
String range = v.rangeKm == null ? "RANGE —" : String.format(Locale.US, "RANGE %.0f km", v.rangeKm);
|
||||
dimension(c, l, r, b + wid * 0.50f, range, d);
|
||||
String trip = v.tripKm == null ? "TRIP —" : String.format(Locale.US, "TRIP %.1f km", v.tripKm);
|
||||
if (v.usageTrip != null) trip += String.format(Locale.US, " @ %.1f kWh/100 km", v.usageTrip);
|
||||
dimension(c, l, r, t - wid * 0.50f, trip, d);
|
||||
}
|
||||
|
||||
private void dimension(Canvas c, float x0, float x1, float y, String label, float d) {
|
||||
faint.setStrokeWidth(1.5f * d);
|
||||
c.drawLine(x0, y - 10f * d, x0, y + 10f * d, faint);
|
||||
c.drawLine(x1, y - 10f * d, x1, y + 10f * d, faint);
|
||||
c.drawLine(x0, y, x1, y, faint);
|
||||
float hs = 9f * d;
|
||||
c.drawLine(x0, y, x0 + hs, y - hs * 0.45f, faint);
|
||||
c.drawLine(x0, y, x0 + hs, y + hs * 0.45f, faint);
|
||||
c.drawLine(x1, y, x1 - hs, y - hs * 0.45f, faint);
|
||||
c.drawLine(x1, y, x1 - hs, y + hs * 0.45f, faint);
|
||||
text.setTextAlign(Paint.Align.CENTER);
|
||||
text.setTextSize(15f * d);
|
||||
float tw = text.measureText(label);
|
||||
float mid = (x0 + x1) / 2f;
|
||||
rect.set(mid - tw / 2f - 8f * d, y - 12f * d, mid + tw / 2f + 8f * d, y + 12f * d);
|
||||
c.drawRect(rect, sheet);
|
||||
c.drawText(label, mid, y + 5f * d, text);
|
||||
}
|
||||
|
||||
/** The drawing's title block: rows of fields, the speed large. */
|
||||
private void drawTitleBlock(Canvas c, float l, float t, float r, float b, float d) {
|
||||
line.setStrokeWidth(2f * d);
|
||||
rect.set(l, t, r, b);
|
||||
c.drawRect(rect, sheet);
|
||||
c.drawRect(rect, line);
|
||||
// Eight rows: the speed takes two, the last holds scale and time.
|
||||
float rowH = (b - t) / 8f;
|
||||
String speed = v.speedKmh == null ? "—" : String.format(Locale.US, "%.0f", v.speedKmh);
|
||||
String[][] rows = {
|
||||
{ "DRAWING", "NILS' MG4 · TOP VIEW" },
|
||||
{ "SPEED", null },
|
||||
{ "POWER", v.powerKw == null ? "—" : String.format(Locale.US, "%.1f kW", v.powerKw) },
|
||||
{ "GEAR", Gear.label(v.gear).isEmpty() ? "—" : Gear.label(v.gear) },
|
||||
{ "USAGE 5 MIN", v.usage5Min == null ? "—" : String.format(Locale.US, "%.1f kWh/100 km", v.usage5Min) },
|
||||
{ "ODOMETER", v.odometerKm == null ? "—" : String.format(Locale.US, "%,.0f km", v.odometerKm) },
|
||||
};
|
||||
float split = l + (r - l) * 0.34f;
|
||||
float y = t;
|
||||
for (int i = 0; i < rows.length; i++) {
|
||||
float rh = i == 1 ? rowH * 2f : rowH;
|
||||
if (i > 0) c.drawLine(l, y, r, y, faint);
|
||||
text.setTextAlign(Paint.Align.LEFT);
|
||||
text.setTextSize(11f * d);
|
||||
c.drawText(rows[i][0], l + 10f * d, y + 18f * d, text);
|
||||
if (i == 1) {
|
||||
text.setTextSize(rh * 0.62f);
|
||||
text.setTextAlign(Paint.Align.RIGHT);
|
||||
c.drawText(speed, r - (r - l) * 0.22f, y + rh * 0.80f, text);
|
||||
text.setTextSize(14f * d);
|
||||
text.setTextAlign(Paint.Align.LEFT);
|
||||
c.drawText("km/h", r - (r - l) * 0.20f, y + rh * 0.80f, text);
|
||||
} else {
|
||||
text.setTextSize(Math.min(rh * 0.42f, 18f * d));
|
||||
c.drawText(rows[i][1], i == 0 ? l + 10f * d : split + 12f * d, y + rh * 0.72f, text);
|
||||
}
|
||||
y += rh;
|
||||
}
|
||||
// The last row: scale and time, split in two.
|
||||
c.drawLine(l, y, r, y, faint);
|
||||
c.drawLine(split, y, split, b, faint);
|
||||
text.setTextSize(11f * d);
|
||||
text.setTextAlign(Paint.Align.LEFT);
|
||||
c.drawText("SCALE 1:20", l + 10f * d, y + rowH * 0.62f, text);
|
||||
c.drawText("TIME", split + 10f * d, y + 18f * d, text);
|
||||
text.setTextSize(Math.min(rowH * 0.5f, 22f * d));
|
||||
text.setTextAlign(Paint.Align.RIGHT);
|
||||
c.drawText(v.clock.isEmpty() ? "—" : v.clock, r - 12f * d, y + rowH * 0.72f, text);
|
||||
c.drawLine(split, t + rowH * 3f, split, t + rowH * 7f, faint);
|
||||
}
|
||||
}
|
||||
@@ -1,316 +0,0 @@
|
||||
package com.evsuite.abrp;
|
||||
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.Color;
|
||||
import android.graphics.Paint;
|
||||
import android.graphics.Path;
|
||||
import android.graphics.RectF;
|
||||
|
||||
/**
|
||||
* What the chibi Mascot figures ({@link ChibiFigure}, {@link FoxFigure}) share: the frame
|
||||
* (scaled down and moved left so the big head stays clear of the speed, a shadow, a hop while
|
||||
* cheering, a lean), their big eyes and mouths per {@link MascotMood}, limbs placed by the mood,
|
||||
* and the effects around them (Zs, sparkles, wind, "?", drops). Sticker style: dark outlines.
|
||||
*
|
||||
* Draws on {@link MascotView}'s 100 × 100 grid; holds no state.
|
||||
*/
|
||||
abstract class ChibiBase {
|
||||
|
||||
protected final Paint fill = new Paint(Paint.ANTI_ALIAS_FLAG);
|
||||
protected final Paint line = new Paint(Paint.ANTI_ALIAS_FLAG);
|
||||
protected final Paint text = new Paint(Paint.ANTI_ALIAS_FLAG);
|
||||
protected final Path p = new Path();
|
||||
protected final Path q = new Path();
|
||||
protected final RectF r = new RectF();
|
||||
protected final int accent, accentLight, regen;
|
||||
protected final int ink = 0xFF3A2A33, blush = 0x66FF7A8A, mouth = 0xFF8E3A48, tongue = 0xFFE88A95;
|
||||
/** The eyes' colours, and what a narrowed lid is filled with (skin or fur). */
|
||||
private final int iris, irisLight, lid;
|
||||
/** How far left the figure moves, in grid units. */
|
||||
private final float shiftX;
|
||||
|
||||
ChibiBase(int accent, int accentLight, int regen, int iris, int irisLight, int lid, float shiftX) {
|
||||
this.accent = accent;
|
||||
this.accentLight = accentLight;
|
||||
this.regen = regen;
|
||||
this.iris = iris;
|
||||
this.irisLight = irisLight;
|
||||
this.lid = lid;
|
||||
this.shiftX = shiftX;
|
||||
line.setStyle(Paint.Style.STROKE);
|
||||
line.setStrokeCap(Paint.Cap.ROUND);
|
||||
line.setStrokeJoin(Paint.Join.ROUND);
|
||||
line.setColor(ink);
|
||||
text.setTextAlign(Paint.Align.CENTER);
|
||||
text.setFakeBoldText(true);
|
||||
}
|
||||
|
||||
/** Where the speech bubble's tail points, on the grid. */
|
||||
abstract float tipX();
|
||||
|
||||
abstract float tipY();
|
||||
|
||||
/** The figure itself, standing or sitting on y = 96, centred on x = 50. */
|
||||
protected abstract void drawFigure(Canvas c, MascotMood mood, long t, boolean blink);
|
||||
|
||||
final void draw(Canvas c, MascotMood mood, long t, boolean blink) {
|
||||
c.save();
|
||||
c.translate(-shiftX, 0f);
|
||||
c.scale(0.92f, 0.92f, 50f, 100f);
|
||||
// A hop while cheering.
|
||||
float hop = mood == MascotMood.CHEERING ? -3.5f * (float) Math.abs(Math.sin(t / 180.0)) : 0f;
|
||||
fill.setColor(0x40000000);
|
||||
float shadow = 18f + hop * 0.8f;
|
||||
c.drawOval(oval(50f - shadow, 96.5f, 50f + shadow, 100f), fill);
|
||||
|
||||
c.save();
|
||||
c.translate(0f, hop);
|
||||
float lean = mood == MascotMood.FAST ? 5f : mood == MascotMood.SLEEPING ? -6f : 0f;
|
||||
c.rotate(lean, 50f, 96f);
|
||||
drawFigure(c, mood, t, blink);
|
||||
c.restore();
|
||||
|
||||
switch (mood) {
|
||||
case SLEEPING: zs(c, t); break;
|
||||
case CHEERING: sparkles(c, t); break;
|
||||
case FAST: wind(c, t); break;
|
||||
case WAITING: question(c, t); break;
|
||||
default: break;
|
||||
}
|
||||
c.restore();
|
||||
}
|
||||
|
||||
/** Head tilt per mood, degrees. */
|
||||
static float tilt(MascotMood mood) {
|
||||
return mood == MascotMood.REVERSE ? -10f : mood == MascotMood.SLEEPING ? 10f
|
||||
: mood == MascotMood.WAITING ? 6f : 0f;
|
||||
}
|
||||
|
||||
/** Hands on the cheeks and a fist at the chest are drawn in front of the head. */
|
||||
static boolean handsInFront(MascotMood mood) {
|
||||
return mood == MascotMood.WORRIED || mood == MascotMood.DETERMINED;
|
||||
}
|
||||
|
||||
/** Left and right hand positions for the mood: {lx, ly, rx, ry}. */
|
||||
static float[] hands(MascotMood mood, long t) {
|
||||
float wave = (float) Math.sin(t / 220.0) * 3f;
|
||||
switch (mood) {
|
||||
case CONTENT: return new float[] { 33f, 82f, 82f + wave * 0.5f, 64f + wave };
|
||||
case CHEERING: return new float[] { 16f, 56f, 84f, 56f };
|
||||
case DETERMINED: return new float[] { 33f, 82f, 57f, 72f };
|
||||
case WORRIED: return new float[] { 34f, 55f, 66f, 55f };
|
||||
// Pointing back.
|
||||
case REVERSE: return new float[] { 20f, 66f, 67f, 82f };
|
||||
case FAST: return new float[] { 30f, 76f, 70f, 76f };
|
||||
default: return new float[] { 33f, 82f, 67f, 82f };
|
||||
}
|
||||
}
|
||||
|
||||
/** A limb from the shoulder to a round hand or paw, outlined. */
|
||||
protected void limb(Canvas c, float sx, float sy, float hx, float hy, int sleeve, int hand) {
|
||||
line.setStrokeWidth(7.4f);
|
||||
c.drawLine(sx, sy, hx, hy, line);
|
||||
line.setColor(sleeve);
|
||||
line.setStrokeWidth(5f);
|
||||
c.drawLine(sx, sy, hx, hy, line);
|
||||
line.setColor(ink);
|
||||
fill.setColor(hand);
|
||||
line.setStrokeWidth(1.1f);
|
||||
c.drawCircle(hx, hy, 3.2f, fill);
|
||||
c.drawCircle(hx, hy, 3.2f, line);
|
||||
}
|
||||
|
||||
/** Cheeks, eyes at y 44, brows, mouth around y 55 and the worried drops. */
|
||||
protected void face(Canvas c, MascotMood mood, long t, boolean blink) {
|
||||
if (mood == MascotMood.CHEERING || mood == MascotMood.FAST || mood == MascotMood.CONTENT
|
||||
|| mood == MascotMood.SLEEPING || mood == MascotMood.WORRIED) {
|
||||
fill.setColor(blush);
|
||||
c.drawOval(oval(29.5f, 50f, 37f, 54f), fill);
|
||||
c.drawOval(oval(63f, 50f, 70.5f, 54f), fill);
|
||||
}
|
||||
eye(c, 39f, 44f, mood, blink);
|
||||
eye(c, 61f, 44f, mood, blink);
|
||||
brows(c, mood);
|
||||
mouth(c, mood, t);
|
||||
if (mood == MascotMood.WORRIED) {
|
||||
fill.setColor(0xFF8FD3FF);
|
||||
drop(c, 72f, 30f, 3f);
|
||||
drop(c, 34.5f, 52f, 1.8f);
|
||||
}
|
||||
}
|
||||
|
||||
private void eye(Canvas c, float cx, float cy, MascotMood mood, boolean blink) {
|
||||
line.setColor(ink);
|
||||
line.setStrokeWidth(1.5f);
|
||||
if (mood == MascotMood.SLEEPING || (blink && mood != MascotMood.CHEERING && mood != MascotMood.FAST)) {
|
||||
c.drawArc(oval(cx - 4.5f, cy - 3f, cx + 4.5f, cy + 2f), 20f, 140f, false, line);
|
||||
return;
|
||||
}
|
||||
if (mood == MascotMood.FAST) {
|
||||
// ^ ^
|
||||
c.drawArc(oval(cx - 4.5f, cy - 2f, cx + 4.5f, cy + 5f), 200f, 140f, false, line);
|
||||
return;
|
||||
}
|
||||
float look = mood == MascotMood.REVERSE ? -2f : 0f;
|
||||
oval(cx - 4.6f, cy - 5.6f, cx + 4.6f, cy + 5.6f);
|
||||
if (mood == MascotMood.REVERSE) {
|
||||
fill.setColor(Color.WHITE);
|
||||
c.drawOval(r, fill);
|
||||
fill.setColor(iris);
|
||||
c.drawOval(oval(cx - 3f + look, cy - 4f, cx + 3f + look, cy + 4.5f), fill);
|
||||
} else {
|
||||
fill.setColor(iris);
|
||||
c.drawOval(r, fill);
|
||||
fill.setColor(irisLight);
|
||||
c.drawOval(oval(cx - 3.6f, cy + 0.5f, cx + 3.6f, cy + 5f), fill);
|
||||
}
|
||||
fill.setColor(Color.WHITE);
|
||||
if (mood == MascotMood.CHEERING) {
|
||||
star(c, cx, cy - 0.5f, 3.6f);
|
||||
} else {
|
||||
c.drawCircle(cx - 1.6f + look, cy - 2.4f, 2f, fill);
|
||||
c.drawCircle(cx + 1.8f + look, cy + 2.4f, 0.9f, fill);
|
||||
}
|
||||
if (mood == MascotMood.DETERMINED) {
|
||||
// A flat lid over the top: focused.
|
||||
fill.setColor(lid);
|
||||
c.drawRect(cx - 5.2f, cy - 6.4f, cx + 5.2f, cy - 2f, fill);
|
||||
line.setStrokeWidth(1.6f);
|
||||
c.drawLine(cx - 5f, cy - 2f, cx + 5f, cy - 2f, line);
|
||||
return;
|
||||
}
|
||||
line.setStrokeWidth(1.6f);
|
||||
c.drawArc(oval(cx - 5f, cy - 5.9f, cx + 5f, cy + 5.6f), 200f, 140f, false, line);
|
||||
}
|
||||
|
||||
private void brows(Canvas c, MascotMood mood) {
|
||||
line.setColor(ink);
|
||||
line.setStrokeWidth(1.4f);
|
||||
if (mood == MascotMood.DETERMINED) {
|
||||
c.drawLine(34f, 33.5f, 43.5f, 36.5f, line);
|
||||
c.drawLine(56.5f, 36.5f, 66f, 33.5f, line);
|
||||
} else if (mood == MascotMood.WORRIED) {
|
||||
c.drawLine(34f, 36f, 43f, 33f, line);
|
||||
c.drawLine(57f, 33f, 66f, 36f, line);
|
||||
}
|
||||
}
|
||||
|
||||
private void mouth(Canvas c, MascotMood mood, long t) {
|
||||
line.setColor(ink);
|
||||
line.setStrokeWidth(1.2f);
|
||||
switch (mood) {
|
||||
case CHEERING:
|
||||
case FAST:
|
||||
p.reset();
|
||||
p.moveTo(45f, 53.5f);
|
||||
p.quadTo(50f, 61.5f, 55f, 53.5f);
|
||||
p.close();
|
||||
fill.setColor(mouth);
|
||||
c.drawPath(p, fill);
|
||||
fill.setColor(tongue);
|
||||
c.drawOval(oval(47.5f, 56.4f, 52.5f, 58.4f), fill);
|
||||
c.drawPath(p, line);
|
||||
break;
|
||||
case DETERMINED:
|
||||
fill.setColor(Color.WHITE);
|
||||
oval(44.5f, 53f, 55.5f, 57f);
|
||||
c.drawRoundRect(r, 1.5f, 1.5f, fill);
|
||||
c.drawRoundRect(r, 1.5f, 1.5f, line);
|
||||
c.drawLine(44.8f, 55f, 55.2f, 55f, line);
|
||||
break;
|
||||
case WORRIED:
|
||||
p.reset();
|
||||
p.moveTo(45.5f, 56f);
|
||||
p.quadTo(47f, 54.3f, 48.5f, 56f);
|
||||
p.quadTo(50f, 57.7f, 51.5f, 56f);
|
||||
p.quadTo(53f, 54.3f, 54.5f, 56f);
|
||||
c.drawPath(p, line);
|
||||
break;
|
||||
case SLEEPING: {
|
||||
fill.setColor(mouth);
|
||||
c.drawOval(oval(48.6f, 54.5f, 51.4f, 57f), fill);
|
||||
// A nose bubble that swells and pops.
|
||||
float grow = (t % 2400) / 2400f;
|
||||
fill.setColor(0x7090D8FF);
|
||||
c.drawCircle(53.5f, 51f, 1f + grow * 4f, fill);
|
||||
break;
|
||||
}
|
||||
case REVERSE:
|
||||
fill.setColor(mouth);
|
||||
c.drawOval(oval(48.6f, 54f, 51.4f, 57f), fill);
|
||||
break;
|
||||
case WAITING:
|
||||
c.drawLine(47.5f, 55.5f, 52.5f, 55.5f, line);
|
||||
break;
|
||||
default:
|
||||
// A cat smile: w.
|
||||
p.reset();
|
||||
p.moveTo(45.5f, 54f);
|
||||
p.quadTo(47.75f, 57.5f, 50f, 54.5f);
|
||||
p.quadTo(52.25f, 57.5f, 54.5f, 54f);
|
||||
c.drawPath(p, line);
|
||||
}
|
||||
}
|
||||
|
||||
private void zs(Canvas c, long t) {
|
||||
float phase = (t % 3000) / 3000f;
|
||||
float[][] zs = { { 20f, 24f, 6f }, { 13f, 14f, 7.5f }, { 5f, 4f, 9f } };
|
||||
for (float[] z : zs) {
|
||||
text.setTextSize(z[2]);
|
||||
text.setColor(AccentColor.withAlpha(accentLight, Math.round(255 * (1f - phase * 0.5f))));
|
||||
c.drawText("Z", z[0] - phase * 2f, z[1] + z[2] - phase * 3f, text);
|
||||
}
|
||||
}
|
||||
|
||||
private void sparkles(Canvas c, long t) {
|
||||
float[][] at = { { 14f, 30f }, { 86f, 26f }, { 12f, 62f }, { 88f, 60f }, { 50f, 1.5f } };
|
||||
for (int i = 0; i < at.length; i++) {
|
||||
float s = 2.4f + 1.8f * (float) Math.abs(Math.sin(t / 260.0 + i * 1.3));
|
||||
fill.setColor(i % 2 == 0 ? regen : Color.WHITE);
|
||||
star(c, at[i][0], at[i][1], s);
|
||||
}
|
||||
}
|
||||
|
||||
private void wind(Canvas c, long t) {
|
||||
line.setStrokeWidth(1f);
|
||||
line.setColor(0x88FFFFFF);
|
||||
for (int i = 0; i < 5; i++) {
|
||||
float x = 18f - ((t / 6f + i * 37f) % 20f);
|
||||
float y = 20f + i * 14f;
|
||||
c.drawLine(x - 10f, y, x, y, line);
|
||||
}
|
||||
line.setColor(ink);
|
||||
}
|
||||
|
||||
private void question(Canvas c, long t) {
|
||||
text.setTextSize(14f);
|
||||
text.setColor(accentLight);
|
||||
float bob = (float) Math.sin(t / 400.0) * 1.2f;
|
||||
c.drawText("?", 16f, 18f + bob, text);
|
||||
}
|
||||
|
||||
protected void drop(Canvas c, float x, float y, float s) {
|
||||
p.reset();
|
||||
p.moveTo(x, y - s * 1.6f);
|
||||
p.quadTo(x + s * 1.2f, y + s * 0.2f, x, y + s);
|
||||
p.quadTo(x - s * 1.2f, y + s * 0.2f, x, y - s * 1.6f);
|
||||
c.drawPath(p, fill);
|
||||
}
|
||||
|
||||
/** A four-pointed sparkle; leaves its outline in {@link #p}. */
|
||||
protected void star(Canvas c, float x, float y, float s) {
|
||||
p.reset();
|
||||
p.moveTo(x, y - s);
|
||||
p.quadTo(x, y, x + s, y);
|
||||
p.quadTo(x, y, x, y + s);
|
||||
p.quadTo(x, y, x - s, y);
|
||||
p.quadTo(x, y, x, y - s);
|
||||
p.close();
|
||||
c.drawPath(p, fill);
|
||||
}
|
||||
|
||||
protected RectF oval(float left, float top, float right, float bottom) {
|
||||
r.set(left, top, right, bottom);
|
||||
return r;
|
||||
}
|
||||
}
|
||||
@@ -1,142 +0,0 @@
|
||||
package com.evsuite.abrp;
|
||||
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.Color;
|
||||
import android.graphics.Path;
|
||||
|
||||
/**
|
||||
* The chibi co-pilot for the Mascot Overview style ({@link MascotCharacter#CHIBI}): the small,
|
||||
* full-figure version — a big round head with a coral bob and a star clip, a hoodie in the
|
||||
* accent colour, short legs, white trainers — drawn with dark outlines like a sticker.
|
||||
*
|
||||
* Her whole body acts the mood: waves while content, throws both arms up and hops while
|
||||
* cheering (star eyes), pumps a fist when determined, holds her cheeks with a tear when worried,
|
||||
* sleeps with a nose bubble in P, glances and points back in R, leans into the wind when
|
||||
* fast, and wonders "?" while waiting. The shared parts are in {@link ChibiBase}.
|
||||
*/
|
||||
final class ChibiFigure extends ChibiBase {
|
||||
|
||||
private final int hair = 0xFFF0855C, hairLight = 0xFFFFB08A, skin = 0xFFFFE6D6, trousers = 0xFF2D3142,
|
||||
shoe = 0xFFF6F6F8;
|
||||
|
||||
ChibiFigure(int accent, int accentLight, int regen) {
|
||||
super(accent, accentLight, regen, 0xFF6E45B8, 0xFFA27BE6, 0xFFFFE6D6, 6f);
|
||||
}
|
||||
|
||||
@Override
|
||||
float tipX() {
|
||||
return 70f;
|
||||
}
|
||||
|
||||
@Override
|
||||
float tipY() {
|
||||
return 34f;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void drawFigure(Canvas c, MascotMood mood, long t, boolean blink) {
|
||||
drawLegs(c);
|
||||
drawBody(c);
|
||||
boolean inFront = handsInFront(mood);
|
||||
if (!inFront) drawArms(c, mood, t);
|
||||
drawHead(c, mood, t, blink);
|
||||
if (inFront) drawArms(c, mood, t);
|
||||
}
|
||||
|
||||
private void drawLegs(Canvas c) {
|
||||
fill.setColor(trousers);
|
||||
line.setStrokeWidth(1.2f);
|
||||
for (float x : new float[] { 42f, 52f }) {
|
||||
oval(x, 84f, x + 6f, 95f);
|
||||
c.drawRect(r, fill);
|
||||
c.drawRect(r, line);
|
||||
}
|
||||
fill.setColor(shoe);
|
||||
for (float x : new float[] { 39.5f, 50.5f }) {
|
||||
oval(x, 92.5f, x + 10f, 98.5f);
|
||||
c.drawOval(r, fill);
|
||||
c.drawOval(r, line);
|
||||
}
|
||||
}
|
||||
|
||||
private void drawBody(Canvas c) {
|
||||
p.reset();
|
||||
p.moveTo(35.5f, 88f);
|
||||
p.lineTo(38f, 67f);
|
||||
p.quadTo(50f, 60f, 62f, 67f);
|
||||
p.lineTo(64.5f, 88f);
|
||||
p.quadTo(50f, 91f, 35.5f, 88f);
|
||||
p.close();
|
||||
fill.setColor(accent);
|
||||
c.drawPath(p, fill);
|
||||
line.setStrokeWidth(1.2f);
|
||||
c.drawPath(p, line);
|
||||
// The pocket and the drawstrings.
|
||||
line.setStrokeWidth(0.9f);
|
||||
p.reset();
|
||||
p.moveTo(42.5f, 88.5f);
|
||||
p.lineTo(43.5f, 79.5f);
|
||||
p.quadTo(50f, 78f, 56.5f, 79.5f);
|
||||
p.lineTo(57.5f, 88.5f);
|
||||
c.drawPath(p, line);
|
||||
line.setColor(Color.WHITE);
|
||||
line.setStrokeWidth(1f);
|
||||
c.drawLine(46.5f, 66f, 46f, 74f, line);
|
||||
c.drawLine(53.5f, 66f, 54f, 74f, line);
|
||||
line.setColor(ink);
|
||||
}
|
||||
|
||||
private void drawArms(Canvas c, MascotMood mood, long t) {
|
||||
float[] h = hands(mood, t);
|
||||
limb(c, 39.5f, 69f, h[0], h[1], accent, skin);
|
||||
limb(c, 60.5f, 69f, h[2], h[3], accent, skin);
|
||||
}
|
||||
|
||||
private void drawHead(Canvas c, MascotMood mood, long t, boolean blink) {
|
||||
c.save();
|
||||
c.rotate(tilt(mood), 50f, 62f);
|
||||
// The bob behind the face.
|
||||
fill.setColor(hair);
|
||||
line.setStrokeWidth(1.3f);
|
||||
p.reset();
|
||||
p.addOval(oval(20f, 5f, 80f, 62f), Path.Direction.CW);
|
||||
q.reset();
|
||||
q.addRoundRect(oval(19.5f, 30f, 80.5f, 64f), 7f, 7f, Path.Direction.CW);
|
||||
p.op(q, Path.Op.UNION);
|
||||
c.drawPath(p, fill);
|
||||
c.drawPath(p, line);
|
||||
// The face.
|
||||
fill.setColor(skin);
|
||||
oval(25f, 15f, 75f, 65f);
|
||||
c.drawOval(r, fill);
|
||||
c.drawOval(r, line);
|
||||
// Bangs.
|
||||
p.reset();
|
||||
p.moveTo(22.5f, 40f);
|
||||
p.cubicTo(19f, 7f, 81f, 7f, 77.5f, 40f);
|
||||
p.lineTo(73f, 27f);
|
||||
p.lineTo(66f, 34f);
|
||||
p.lineTo(60f, 23f);
|
||||
p.lineTo(52f, 32f);
|
||||
p.lineTo(45f, 22f);
|
||||
p.lineTo(38f, 32f);
|
||||
p.lineTo(32f, 25f);
|
||||
p.lineTo(27f, 35f);
|
||||
p.close();
|
||||
fill.setColor(hair);
|
||||
c.drawPath(p, fill);
|
||||
c.drawPath(p, line);
|
||||
line.setColor(hairLight);
|
||||
line.setStrokeWidth(1.6f);
|
||||
c.drawArc(oval(30f, 10f, 56f, 30f), 200f, 60f, false, line);
|
||||
line.setColor(ink);
|
||||
// The star clip.
|
||||
fill.setColor(accentLight);
|
||||
star(c, 70f, 19f, 4.5f);
|
||||
line.setStrokeWidth(0.8f);
|
||||
c.drawPath(p, line);
|
||||
|
||||
face(c, mood, t, blink);
|
||||
c.restore();
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
package com.evsuite.abrp;
|
||||
|
||||
/**
|
||||
* One reading for the drawn Overview styles (Retro, Futuristic, Sci-fi, Cartoon, Blueprint):
|
||||
* One reading for the drawn Overview styles (Retro, Futuristic, Sci-fi, Cartoon, MG and the canvas styles):
|
||||
* what they show, already in display units. Every field is null when unread, and the styles show that as dashes, never 0.
|
||||
*/
|
||||
final class ClusterValues {
|
||||
|
||||
@@ -1,206 +0,0 @@
|
||||
package com.evsuite.abrp;
|
||||
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.Path;
|
||||
|
||||
/**
|
||||
* The chibi fox for the Mascot Overview style ({@link MascotCharacter#FOX}): a sitting fox with
|
||||
* a big head, pointed ears, cream cheeks and chest, dark paws, a scarf in the accent colour and
|
||||
* a large brush tail with a cream tip.
|
||||
*
|
||||
* Ears and tail act the mood with the face ({@link ChibiBase}): the tail wags while content and
|
||||
* fast while cheering (paws up, star eyes), stands stiff when determined (paw at the chest),
|
||||
* hangs low with drooping ears when worried (paws on the cheeks), lies flat in P (ears down,
|
||||
* nose bubble); ears go back and the scarf flies when fast; a paw points back in R.
|
||||
*/
|
||||
final class FoxFigure extends ChibiBase {
|
||||
|
||||
private final int fur = 0xFFF08A3C, furLight = 0xFFFFB46E, cream = 0xFFFFF4E6, paw = 0xFF4A3028,
|
||||
earInside = 0xFF5A3A30;
|
||||
|
||||
FoxFigure(int accent, int accentLight, int regen) {
|
||||
super(accent, accentLight, regen, 0xFF9A5A1A, 0xFFE0A040, 0xFFF08A3C, 10f);
|
||||
}
|
||||
|
||||
@Override
|
||||
float tipX() {
|
||||
return 66f;
|
||||
}
|
||||
|
||||
@Override
|
||||
float tipY() {
|
||||
return 38f;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void drawFigure(Canvas c, MascotMood mood, long t, boolean blink) {
|
||||
drawTail(c, mood, t);
|
||||
drawBody(c);
|
||||
drawScarf(c, mood, t);
|
||||
boolean inFront = handsInFront(mood);
|
||||
if (!inFront) drawPaws(c, mood, t);
|
||||
drawHead(c, mood, t, blink);
|
||||
if (inFront) drawPaws(c, mood, t);
|
||||
}
|
||||
|
||||
/** The brush, rotated about its root by the mood; wagging is a swing over time. */
|
||||
private void drawTail(Canvas c, MascotMood mood, long t) {
|
||||
// Kept within about 18° to the right, so the tail stays clear of the values.
|
||||
float angle;
|
||||
switch (mood) {
|
||||
case CONTENT: angle = (float) Math.sin(t / 250.0) * 8f; break;
|
||||
case CHEERING: angle = (float) Math.sin(t / 110.0) * 14f; break;
|
||||
case DETERMINED: angle = -8f; break;
|
||||
case WORRIED: angle = 15f; break;
|
||||
case SLEEPING: angle = 18f; break;
|
||||
case FAST: angle = 16f; break;
|
||||
case REVERSE: angle = (float) Math.sin(t / 400.0) * 5f; break;
|
||||
default: angle = (float) Math.sin(t / 600.0) * 4f;
|
||||
}
|
||||
c.save();
|
||||
c.rotate(angle, 60f, 90f);
|
||||
p.reset();
|
||||
p.moveTo(57f, 92f);
|
||||
p.cubicTo(80f, 96f, 94f, 78f, 88f, 56f);
|
||||
p.cubicTo(86f, 46f, 78f, 40f, 73f, 44f);
|
||||
p.cubicTo(78f, 56f, 76f, 74f, 57f, 86f);
|
||||
p.close();
|
||||
fill.setColor(fur);
|
||||
c.drawPath(p, fill);
|
||||
c.save();
|
||||
c.clipPath(p);
|
||||
fill.setColor(cream);
|
||||
c.drawCircle(83f, 46f, 10f, fill);
|
||||
c.restore();
|
||||
line.setStrokeWidth(1.3f);
|
||||
c.drawPath(p, line);
|
||||
c.restore();
|
||||
}
|
||||
|
||||
private void drawBody(Canvas c) {
|
||||
p.reset();
|
||||
p.moveTo(37f, 93f);
|
||||
p.cubicTo(33f, 80f, 40f, 66f, 50f, 66f);
|
||||
p.cubicTo(60f, 66f, 67f, 80f, 63f, 93f);
|
||||
p.quadTo(50f, 96f, 37f, 93f);
|
||||
p.close();
|
||||
fill.setColor(fur);
|
||||
c.drawPath(p, fill);
|
||||
line.setStrokeWidth(1.2f);
|
||||
c.drawPath(p, line);
|
||||
fill.setColor(cream);
|
||||
c.drawOval(oval(43f, 71f, 57f, 91f), fill);
|
||||
// Hind paws.
|
||||
fill.setColor(paw);
|
||||
for (float x : new float[] { 37.5f, 52.5f }) {
|
||||
oval(x, 90f, x + 10f, 97.5f);
|
||||
c.drawOval(r, fill);
|
||||
c.drawOval(r, line);
|
||||
}
|
||||
}
|
||||
|
||||
/** The scarf round the neck; its end hangs, or flies back when fast. */
|
||||
private void drawScarf(Canvas c, MascotMood mood, long t) {
|
||||
fill.setColor(accent);
|
||||
line.setStrokeWidth(1.1f);
|
||||
if (mood == MascotMood.FAST) {
|
||||
float flap = (float) Math.sin(t / 90.0) * 2f;
|
||||
p.reset();
|
||||
p.moveTo(40f, 64f);
|
||||
p.quadTo(30f, 60f + flap, 20f, 63f - flap);
|
||||
p.lineTo(21f, 69f - flap);
|
||||
p.quadTo(30f, 67f + flap, 40f, 70f);
|
||||
p.close();
|
||||
} else {
|
||||
p.reset();
|
||||
p.addRoundRect(oval(54f, 65f, 61f, 80f), 2f, 2f, Path.Direction.CW);
|
||||
}
|
||||
c.drawPath(p, fill);
|
||||
c.drawPath(p, line);
|
||||
oval(36f, 62f, 64f, 70f);
|
||||
c.drawRoundRect(r, 4f, 4f, fill);
|
||||
c.drawRoundRect(r, 4f, 4f, line);
|
||||
}
|
||||
|
||||
private void drawPaws(Canvas c, MascotMood mood, long t) {
|
||||
float[] h = hands(mood, t);
|
||||
limb(c, 41f, 72f, h[0], h[1], fur, paw);
|
||||
limb(c, 59f, 72f, h[2], h[3], fur, paw);
|
||||
}
|
||||
|
||||
private void drawHead(Canvas c, MascotMood mood, long t, boolean blink) {
|
||||
c.save();
|
||||
c.rotate(tilt(mood), 50f, 62f);
|
||||
// Ears behind the head, drooping or laid back by the mood; the right one mirrored.
|
||||
float droop = mood == MascotMood.WORRIED ? 38f : mood == MascotMood.SLEEPING ? 26f
|
||||
: mood == MascotMood.FAST ? 22f : mood == MascotMood.CONTENT ? (float) Math.sin(t / 900.0) * 3f : 0f;
|
||||
ear(c, droop);
|
||||
c.save();
|
||||
c.scale(-1f, 1f, 50f, 0f);
|
||||
ear(c, droop);
|
||||
c.restore();
|
||||
|
||||
// The head: round on top, a tuft at each cheek.
|
||||
p.reset();
|
||||
p.moveTo(50f, 16f);
|
||||
p.cubicTo(66f, 16f, 78f, 26f, 79f, 40f);
|
||||
p.lineTo(86f, 52f);
|
||||
p.lineTo(77f, 53f);
|
||||
p.cubicTo(70f, 63f, 60f, 66f, 50f, 66f);
|
||||
p.cubicTo(40f, 66f, 30f, 63f, 23f, 53f);
|
||||
p.lineTo(14f, 52f);
|
||||
p.lineTo(21f, 40f);
|
||||
p.cubicTo(22f, 26f, 34f, 16f, 50f, 16f);
|
||||
p.close();
|
||||
fill.setColor(fur);
|
||||
c.drawPath(p, fill);
|
||||
// Cream cheeks and muzzle, inside the head.
|
||||
c.save();
|
||||
c.clipPath(p);
|
||||
fill.setColor(cream);
|
||||
c.drawOval(oval(22f, 47f, 78f, 84f), fill);
|
||||
c.drawOval(oval(12f, 45f, 34f, 60f), fill);
|
||||
c.drawOval(oval(66f, 45f, 88f, 60f), fill);
|
||||
c.restore();
|
||||
line.setStrokeWidth(1.3f);
|
||||
c.drawPath(p, line);
|
||||
line.setColor(furLight);
|
||||
line.setStrokeWidth(1.6f);
|
||||
c.drawArc(oval(30f, 19f, 56f, 36f), 200f, 60f, false, line);
|
||||
line.setColor(ink);
|
||||
|
||||
// Whiskers, then the nose and the face.
|
||||
line.setStrokeWidth(0.6f);
|
||||
c.drawLine(28f, 53f, 19f, 51f, line);
|
||||
c.drawLine(28f, 55.5f, 19f, 56.5f, line);
|
||||
c.drawLine(72f, 53f, 81f, 51f, line);
|
||||
c.drawLine(72f, 55.5f, 81f, 56.5f, line);
|
||||
face(c, mood, t, blink);
|
||||
fill.setColor(ink);
|
||||
c.drawOval(oval(47.6f, 49f, 52.4f, 52f), fill);
|
||||
c.restore();
|
||||
}
|
||||
|
||||
/** The left ear, rotated outwards about its root by {@code droop} degrees. */
|
||||
private void ear(Canvas c, float droop) {
|
||||
c.save();
|
||||
c.rotate(-droop, 35f, 23f);
|
||||
p.reset();
|
||||
p.moveTo(23f, 34f);
|
||||
p.lineTo(26f, 3f);
|
||||
p.lineTo(47f, 20f);
|
||||
p.close();
|
||||
fill.setColor(fur);
|
||||
c.drawPath(p, fill);
|
||||
line.setStrokeWidth(1.3f);
|
||||
c.drawPath(p, line);
|
||||
p.reset();
|
||||
p.moveTo(27.5f, 27f);
|
||||
p.lineTo(29f, 10f);
|
||||
p.lineTo(40f, 20f);
|
||||
p.close();
|
||||
fill.setColor(earInside);
|
||||
c.drawPath(p, fill);
|
||||
c.restore();
|
||||
}
|
||||
}
|
||||
@@ -64,12 +64,10 @@ public class MainActivity extends AppCompatActivity {
|
||||
private FlowView flowView;
|
||||
private SciFiView sciFiView;
|
||||
private CartoonView cartoonView;
|
||||
private BlueprintView blueprintView;
|
||||
private OctagonView octagonView;
|
||||
/** The styles built on {@link ClusterCanvasView}, fed together. */
|
||||
private final java.util.List<ClusterCanvasView> canvasStyles = new java.util.ArrayList<>();
|
||||
private TerminalView terminalView;
|
||||
private MascotView mascotView;
|
||||
/** The pane of each Overview style, by its {@link OverviewStyle} ordinal. */
|
||||
private View[] stylePanes;
|
||||
private View consumptionPane;
|
||||
@@ -316,8 +314,10 @@ public class MainActivity extends AppCompatActivity {
|
||||
getDelegate().setLocalNightMode(AppCompatDelegate.MODE_NIGHT_YES);
|
||||
super.onCreate(savedInstanceState);
|
||||
prefs = getSharedPreferences("abrp_prefs", MODE_PRIVATE);
|
||||
// Before anything is inflated: every ?attr/evAccent… resolves against this overlay.
|
||||
getTheme().applyStyle(AccentColor.fromPref(prefs.getString(AccentColor.PREF_KEY, null)).overlay, true);
|
||||
// Before anything is inflated: every ?attr/evAccent… resolves against this overlay — the
|
||||
// accent colour, or the Overview style's own palette while it shows that (StylePalette).
|
||||
appliedOverlay = appOverlay();
|
||||
getTheme().applyStyle(appliedOverlay, true);
|
||||
setContentView(R.layout.activity_main);
|
||||
|
||||
|
||||
@@ -338,21 +338,17 @@ public class MainActivity extends AppCompatActivity {
|
||||
flowView = futuristicPane.findViewById(R.id.flow_view);
|
||||
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), mgPane = drawnPane(octagonView);
|
||||
mgPane = drawnPane(octagonView);
|
||||
terminalView = new TerminalView(this);
|
||||
mascotView = new MascotView(this);
|
||||
mascotView.setCharacter(MascotCharacter.fromPref(prefs.getString(MascotCharacter.PREF_KEY, null)));
|
||||
java.util.Collections.addAll(canvasStyles, new MinimalView(this), terminalView,
|
||||
new AviationView(this), new NixieView(this), new ChronographView(this), mascotView);
|
||||
new AviationView(this), new NixieView(this), new ChronographView(this));
|
||||
View[] canvasPanes = new View[canvasStyles.size()];
|
||||
for (int i = 0; i < canvasPanes.length; i++) canvasPanes[i] = drawnPane(canvasStyles.get(i));
|
||||
stylePanes = new View[] { vehiclePane, gaugesPane, cockpitPane, retroPane,
|
||||
futuristicPane, sciFiPane, cartoonPane, blueprintPane, mgPane,
|
||||
canvasPanes[0], canvasPanes[1], canvasPanes[2], canvasPanes[3], canvasPanes[4],
|
||||
canvasPanes[5] };
|
||||
futuristicPane, sciFiPane, cartoonPane, mgPane,
|
||||
canvasPanes[0], canvasPanes[1], canvasPanes[2], canvasPanes[3], canvasPanes[4] };
|
||||
// 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(
|
||||
@@ -365,7 +361,6 @@ public class MainActivity extends AppCompatActivity {
|
||||
overviewPage.addView(futuristicPane);
|
||||
overviewPage.addView(sciFiPane);
|
||||
overviewPage.addView(cartoonPane);
|
||||
overviewPage.addView(blueprintPane);
|
||||
overviewPage.addView(mgPane);
|
||||
for (View pane : canvasPanes) overviewPage.addView(pane);
|
||||
consumptionPane = inflatePane(R.layout.pane_consumption);
|
||||
@@ -1189,7 +1184,6 @@ public class MainActivity extends AppCompatActivity {
|
||||
voiceKeySwitch.setOnCheckedChangeListener((btn, checked) ->
|
||||
prefs.edit().putBoolean(UploadSettings.KEY_VOICE_KEY_OPENS, checked).apply());
|
||||
bindTireUnit();
|
||||
bindMascotCharacter();
|
||||
bindDriveMode();
|
||||
bindOverviewStyle();
|
||||
bindAccentColor();
|
||||
@@ -1294,7 +1288,7 @@ public class MainActivity extends AppCompatActivity {
|
||||
/**
|
||||
* The accent colour; a change recreates the activity so every view takes it. For a style
|
||||
* with an original palette, "Original colours" comes first: it keeps the style in its own
|
||||
* colours and leaves the app's accent as it is.
|
||||
* colours, and the app takes them on too (StylePalette).
|
||||
*/
|
||||
private void bindAccentColor() {
|
||||
OverviewStyle style = overviewStyle();
|
||||
@@ -1307,17 +1301,10 @@ public class MainActivity extends AppCompatActivity {
|
||||
int selected = originalColours(style) ? 0 : current.ordinal() + offset;
|
||||
bindChoices(settingsPane.findViewById(R.id.accent_choices), labels, selected, position -> {
|
||||
if (style.hasOriginal) prefs.edit().putBoolean(style.originalKey(), position == 0).apply();
|
||||
if (position < offset) {
|
||||
applyOverviewStyle();
|
||||
return;
|
||||
if (position >= offset) {
|
||||
prefs.edit().putString(AccentColor.PREF_KEY, colors[position - offset].name()).apply();
|
||||
}
|
||||
AccentColor picked = colors[position - offset];
|
||||
if (picked == current) {
|
||||
applyOverviewStyle();
|
||||
return;
|
||||
}
|
||||
prefs.edit().putString(AccentColor.PREF_KEY, picked.name()).apply();
|
||||
recreate();
|
||||
applyStyleOrRecreate();
|
||||
});
|
||||
applyAccentAvailability();
|
||||
}
|
||||
@@ -1332,7 +1319,6 @@ public class MainActivity extends AppCompatActivity {
|
||||
prefs.edit().putString(PressureUnit.PREF_KEY, units[position].name()).apply());
|
||||
}
|
||||
|
||||
/** Who sits in the Mascot style; the drop-down shows only while that style is chosen. */
|
||||
private static final int[] DRIVE_MODE_IDS = { R.id.drive_mode_eco, R.id.drive_mode_normal,
|
||||
R.id.drive_mode_sport, R.id.drive_mode_snow, R.id.drive_mode_custom };
|
||||
private static final int[] DRIVE_MODE_LABELS = { R.string.drive_mode_eco, R.string.drive_mode_normal,
|
||||
@@ -1443,17 +1429,6 @@ public class MainActivity extends AppCompatActivity {
|
||||
}
|
||||
}
|
||||
|
||||
private void bindMascotCharacter() {
|
||||
MascotCharacter[] characters = MascotCharacter.values();
|
||||
String[] labels = { getString(R.string.mascot_character_classic), getString(R.string.mascot_character_chibi),
|
||||
getString(R.string.mascot_character_fox) };
|
||||
int current = MascotCharacter.fromPref(prefs.getString(MascotCharacter.PREF_KEY, null)).ordinal();
|
||||
bindChoices(settingsPane.findViewById(R.id.mascot_choices), labels, current, position -> {
|
||||
prefs.edit().putString(MascotCharacter.PREF_KEY, characters[position].name()).apply();
|
||||
mascotView.setCharacter(characters[position]);
|
||||
});
|
||||
}
|
||||
|
||||
/** The service reads these settings on every tick, so a change applies without a restart. */
|
||||
private void bindCadenceControls() {
|
||||
UploadSettings current = UploadSettings.from(prefs);
|
||||
@@ -1712,6 +1687,24 @@ public class MainActivity extends AppCompatActivity {
|
||||
return style;
|
||||
}
|
||||
|
||||
/** The theme overlay applied in onCreate; a choice that changes it recreates the activity. */
|
||||
private int appliedOverlay;
|
||||
|
||||
private int appOverlay() {
|
||||
OverviewStyle style = overviewStyle();
|
||||
return StylePalette.appOverlay(style, originalColours(style),
|
||||
AccentColor.fromPref(prefs.getString(AccentColor.PREF_KEY, null)));
|
||||
}
|
||||
|
||||
/**
|
||||
* After a style or colour choice: the app's colours follow at once (the activity is
|
||||
* recreated when they change, as for the accent colour), otherwise the Overview is redrawn.
|
||||
*/
|
||||
private void applyStyleOrRecreate() {
|
||||
if (appOverlay() != appliedOverlay) recreate();
|
||||
else applyOverviewStyle();
|
||||
}
|
||||
|
||||
/** Whether a style with an original palette shows it (the default) or the accent colour. */
|
||||
private boolean originalColours(OverviewStyle style) {
|
||||
return style.hasOriginal && prefs.getBoolean(style.originalKey(), true);
|
||||
@@ -1742,10 +1735,6 @@ public class MainActivity extends AppCompatActivity {
|
||||
TextView note = settingsPane.findViewById(R.id.accent_note);
|
||||
note.setVisibility(style.usesAccent ? View.GONE : View.VISIBLE);
|
||||
note.setText(getString(R.string.accent_not_used, styleLabels()[style.ordinal()]));
|
||||
// The wide layout keeps the Mascot field's place so the page does not jump.
|
||||
View mascot = settingsPane.findViewById(R.id.mascot_field);
|
||||
boolean wide = getResources().getConfiguration().screenWidthDp >= 1000;
|
||||
mascot.setVisibility(style == OverviewStyle.MASCOT ? View.VISIBLE : wide ? View.INVISIBLE : View.GONE);
|
||||
}
|
||||
|
||||
/** The drop-down's labels, by {@link OverviewStyle} ordinal. */
|
||||
@@ -1754,10 +1743,10 @@ public class MainActivity extends AppCompatActivity {
|
||||
getString(R.string.overview_style_cockpit), getString(R.string.overview_style_retro),
|
||||
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_mg),
|
||||
getString(R.string.overview_style_mg),
|
||||
getString(R.string.overview_style_minimal), getString(R.string.overview_style_terminal),
|
||||
getString(R.string.overview_style_aviation), getString(R.string.overview_style_nixie),
|
||||
getString(R.string.overview_style_chronograph), getString(R.string.overview_style_mascot) };
|
||||
getString(R.string.overview_style_chronograph) };
|
||||
}
|
||||
|
||||
/** The style drop-down; every style is fed on every reading, only the chosen one is shown. */
|
||||
@@ -1770,7 +1759,7 @@ public class MainActivity extends AppCompatActivity {
|
||||
bindChoices(field, styleLabels(), overviewStyle().ordinal(),
|
||||
position -> {
|
||||
prefs.edit().putString(PREF_OVERVIEW_STYLE, styles[position].name()).apply();
|
||||
applyOverviewStyle();
|
||||
applyStyleOrRecreate();
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1827,7 +1816,6 @@ public class MainActivity extends AppCompatActivity {
|
||||
flowView.setValues(v);
|
||||
sciFiView.setValues(v);
|
||||
cartoonView.setValues(v);
|
||||
blueprintView.setValues(v);
|
||||
octagonView.setValues(v);
|
||||
for (ClusterCanvasView view : canvasStyles) view.setValues(v);
|
||||
}
|
||||
|
||||
@@ -1,24 +0,0 @@
|
||||
package com.evsuite.abrp;
|
||||
|
||||
/**
|
||||
* Who sits in the Mascot Overview style, in the order of the Settings drop-down: the classic
|
||||
* co-pilot (head and shoulders, headset, racing jacket), the chibi (small, full figure, big
|
||||
* head, hoodie, bubbly) or the chibi fox (sitting, scarf, a tail that wags). Each has their
|
||||
* own drawing and lines; all follow {@link MascotMood}.
|
||||
* Pure Java, unit-tested.
|
||||
*/
|
||||
enum MascotCharacter {
|
||||
CLASSIC,
|
||||
CHIBI,
|
||||
FOX;
|
||||
|
||||
static final String PREF_KEY = "mascot_character";
|
||||
|
||||
/** The stored character; anything unknown is the classic one. */
|
||||
static MascotCharacter fromPref(String stored) {
|
||||
for (MascotCharacter character : values()) {
|
||||
if (character.name().equals(stored)) return character;
|
||||
}
|
||||
return CLASSIC;
|
||||
}
|
||||
}
|
||||
@@ -1,70 +0,0 @@
|
||||
package com.evsuite.abrp;
|
||||
|
||||
/**
|
||||
* What the Mascot Overview style's co-pilot feels, from one reading ({@link #of}), and a
|
||||
* {@link Holder} that keeps her from flickering between moods when a value hovers at a
|
||||
* threshold. Pure Java, unit-tested.
|
||||
*/
|
||||
enum MascotMood {
|
||||
/** Nothing read yet (the emulator, or the car's services not up). */
|
||||
WAITING,
|
||||
/** Reversing: looks back, careful. */
|
||||
REVERSE,
|
||||
/** Battery under {@link #LOW_SOC} %: worried, a sweat drop. */
|
||||
WORRIED,
|
||||
/** In P: asleep. */
|
||||
SLEEPING,
|
||||
/** Regenerating more than {@link #REGEN_KW} kW: cheering. */
|
||||
CHEERING,
|
||||
/** Drawing more than {@link #BOOST_KW} kW: determined. */
|
||||
DETERMINED,
|
||||
/** {@link #FAST_KMH} km/h and more: enjoying the speed, wind in her hair. */
|
||||
FAST,
|
||||
/** Anything else: content. */
|
||||
CONTENT;
|
||||
|
||||
static final float LOW_SOC = 20f, REGEN_KW = 5f, BOOST_KW = 60f, FAST_KMH = 100f;
|
||||
/** How long a new mood must last before she shows it. */
|
||||
static final long HOLD_MS = 1500;
|
||||
|
||||
/** The mood for one reading; unread values never count as a reason. */
|
||||
static MascotMood of(ClusterValues v) {
|
||||
if (v == null || (v.gear == null && v.speedKmh == null && v.socPercent == null && v.powerKw == null)) {
|
||||
return WAITING;
|
||||
}
|
||||
if (v.gear != null && v.gear == 2) return REVERSE;
|
||||
if (v.socPercent != null && v.socPercent < LOW_SOC) return WORRIED;
|
||||
if (v.gear != null && v.gear == 1) return SLEEPING;
|
||||
if (v.powerKw != null && v.powerKw < -REGEN_KW) return CHEERING;
|
||||
if (v.powerKw != null && v.powerKw > BOOST_KW) return DETERMINED;
|
||||
if (v.speedKmh != null && v.speedKmh >= FAST_KMH) return FAST;
|
||||
return CONTENT;
|
||||
}
|
||||
|
||||
/** Moods that show at once: shifting into R and losing the data are not to be waited out. */
|
||||
boolean immediate() {
|
||||
return this == REVERSE || this == WAITING;
|
||||
}
|
||||
|
||||
/** The shown mood: a new one takes over after it has lasted {@link #HOLD_MS}. */
|
||||
static final class Holder {
|
||||
private MascotMood shown, candidate;
|
||||
private long candidateSince;
|
||||
|
||||
MascotMood update(MascotMood now, long nowMs) {
|
||||
if (shown == null || now == shown || now.immediate()) {
|
||||
shown = now;
|
||||
candidate = null;
|
||||
return shown;
|
||||
}
|
||||
if (now != candidate) {
|
||||
candidate = now;
|
||||
candidateSince = nowMs;
|
||||
} else if (nowMs - candidateSince >= HOLD_MS) {
|
||||
shown = now;
|
||||
candidate = null;
|
||||
}
|
||||
return shown;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,536 +0,0 @@
|
||||
package com.evsuite.abrp;
|
||||
|
||||
import android.content.Context;
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.Color;
|
||||
import android.graphics.Paint;
|
||||
import android.graphics.Path;
|
||||
import android.graphics.RectF;
|
||||
import android.graphics.Typeface;
|
||||
import android.os.SystemClock;
|
||||
|
||||
/**
|
||||
* The Mascot Overview style: an anime co-pilot beside the values, who reacts to the drive.
|
||||
*
|
||||
* On the left, head and shoulders: a young woman with dark plum hair, a headset with a boom mic
|
||||
* and a racing jacket in the accent colour with an octagon badge. Her face follows
|
||||
* {@link MascotMood} (held for a moment so it does not flicker): content, enjoying the speed
|
||||
* with wind lines, cheering with sparkles while regenerating, determined under hard
|
||||
* acceleration, worried with a sweat drop on a low battery, asleep with drifting Zs in P,
|
||||
* glancing back in R, and patient while nothing is read. She blinks now and then and says a
|
||||
* line for her mood in a speech bubble.
|
||||
*
|
||||
* On the right: speed large, the gear in a ring, battery as a bar with percent and range, then
|
||||
* power, 5-minute usage and the trip; the clock top right, left of the full-screen button.
|
||||
* Unread values show "—".
|
||||
*
|
||||
* Three characters ({@link MascotCharacter}): this classic co-pilot, the chibi
|
||||
* ({@link ChibiFigure}) or the chibi fox ({@link FoxFigure}), each with their own lines.
|
||||
*
|
||||
* Drawn on a 100 × 100 grid scaled to the pane. Nothing animates on its own: blinking, the Zs,
|
||||
* sparkles and wind move because readings redraw the view every 0.2 s.
|
||||
*/
|
||||
final class MascotView extends ClusterCanvasView {
|
||||
|
||||
/** Her lines per {@link MascotCharacter}, then per {@link MascotMood}, in their orders. */
|
||||
private static final int[][] LINES = {
|
||||
{ R.array.mascot_waiting, R.array.mascot_reverse, R.array.mascot_worried, R.array.mascot_sleeping,
|
||||
R.array.mascot_cheering, R.array.mascot_determined, R.array.mascot_fast, R.array.mascot_content },
|
||||
{ R.array.chibi_waiting, R.array.chibi_reverse, R.array.chibi_worried, R.array.chibi_sleeping,
|
||||
R.array.chibi_cheering, R.array.chibi_determined, R.array.chibi_fast, R.array.chibi_content },
|
||||
{ R.array.fox_waiting, R.array.fox_reverse, R.array.fox_worried, R.array.fox_sleeping,
|
||||
R.array.fox_cheering, R.array.fox_determined, R.array.fox_fast, R.array.fox_content } };
|
||||
|
||||
private final Paint fill = new Paint(Paint.ANTI_ALIAS_FLAG);
|
||||
private final Paint stroke = new Paint(Paint.ANTI_ALIAS_FLAG);
|
||||
private final Paint text = new Paint(Paint.ANTI_ALIAS_FLAG);
|
||||
private final Path p = new Path();
|
||||
private final RectF r = new RectF();
|
||||
private final MascotMood.Holder holder = new MascotMood.Holder();
|
||||
private final String[][][] lines = new String[LINES.length][][];
|
||||
/** The drawn figures by {@link MascotCharacter} ordinal; null for the classic one, drawn here. */
|
||||
private final ChibiBase[] figures;
|
||||
private MascotCharacter character = MascotCharacter.CLASSIC;
|
||||
private final int accent, accentDark, accentLight, regen, warn, white, grey, track;
|
||||
private final int hair = 0xFF2E2440, hairLight = 0xFF5A4880, skin = 0xFFFBE2D0, skinShade = 0xFFE9BFA9,
|
||||
iris = 0xFF2F8F9D, pupil = 0xFF123238, lash = 0xFF221A2E, lips = 0xFFC2606A, mouth = 0xFF8E3A48,
|
||||
shirt = 0xFFF4F4F6, headset = 0xFF2A2D33, cup = 0xFF3C414A;
|
||||
|
||||
MascotView(Context context) {
|
||||
super(context);
|
||||
accent = AccentColor.resolve(context, R.attr.evAccent);
|
||||
accentLight = AccentColor.resolve(context, R.attr.evAccentLight);
|
||||
accentDark = Color.rgb(Math.round(Color.red(accent) * 0.6f), Math.round(Color.green(accent) * 0.6f),
|
||||
Math.round(Color.blue(accent) * 0.6f));
|
||||
regen = context.getColor(R.color.ev_ok);
|
||||
warn = context.getColor(R.color.ev_error);
|
||||
white = context.getColor(R.color.ev_text_primary);
|
||||
grey = context.getColor(R.color.ev_text_secondary);
|
||||
track = context.getColor(R.color.ev_outline);
|
||||
for (int i = 0; i < LINES.length; i++) {
|
||||
lines[i] = new String[LINES[i].length][];
|
||||
for (int j = 0; j < LINES[i].length; j++) lines[i][j] = context.getResources().getStringArray(LINES[i][j]);
|
||||
}
|
||||
figures = new ChibiBase[] { null, new ChibiFigure(accent, accentLight, regen),
|
||||
new FoxFigure(accent, accentLight, regen) };
|
||||
stroke.setStyle(Paint.Style.STROKE);
|
||||
stroke.setStrokeCap(Paint.Cap.ROUND);
|
||||
stroke.setStrokeJoin(Paint.Join.ROUND);
|
||||
text.setTypeface(Typeface.create("sans-serif-medium", Typeface.NORMAL));
|
||||
text.setFontFeatureSettings("tnum");
|
||||
}
|
||||
|
||||
/** Who sits beside the values (Settings → Mascot, shown for this style only). */
|
||||
void setCharacter(MascotCharacter character) {
|
||||
this.character = character == null ? MascotCharacter.CLASSIC : character;
|
||||
invalidate();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDraw(Canvas c) {
|
||||
super.onDraw(c);
|
||||
float w = getWidth(), h = getHeight();
|
||||
if (w <= 0 || h <= 0) return;
|
||||
long t = SystemClock.uptimeMillis();
|
||||
MascotMood mood = holder.update(MascotMood.of(v), t);
|
||||
|
||||
// A soft halo behind her.
|
||||
float k = h * 0.98f / 100f, ox = w * 0.02f, oy = h - 100f * k;
|
||||
fill.setColor(AccentColor.withAlpha(accent, 0x30));
|
||||
c.drawCircle(ox + 50f * k, oy + 46f * k, 46f * k, fill);
|
||||
|
||||
c.save();
|
||||
c.translate(ox, oy);
|
||||
c.scale(k, k);
|
||||
ChibiBase figure = figures[character.ordinal()];
|
||||
if (figure != null) figure.draw(c, mood, t, t % 4200 < 260);
|
||||
else drawCharacter(c, mood, t);
|
||||
c.restore();
|
||||
|
||||
float x0 = ox + 78f * k;
|
||||
float tipX = figure != null ? figure.tipX() : 70f, tipY = figure != null ? figure.tipY() : 46f;
|
||||
drawBubble(c, mood, t, x0, h, ox + tipX * k, oy + tipY * k, w);
|
||||
drawValues(c, x0, ox + 93f * k, w, h);
|
||||
}
|
||||
|
||||
// ---------- The co-pilot, on the 100 × 100 grid ----------
|
||||
|
||||
private void drawCharacter(Canvas c, MascotMood mood, long t) {
|
||||
float tilt = mood == MascotMood.SLEEPING ? 8f : mood == MascotMood.REVERSE ? -6f
|
||||
: mood == MascotMood.CHEERING ? -4f : 0f;
|
||||
|
||||
// Hair behind the head, falling behind the shoulders.
|
||||
c.save();
|
||||
c.rotate(tilt, 50f, 62f);
|
||||
fill.setColor(hair);
|
||||
c.drawOval(oval(27f, 9f, 73f, 57f), fill);
|
||||
p.reset();
|
||||
p.moveTo(28.5f, 28f);
|
||||
p.cubicTo(26f, 46f, 25f, 64f, 22f, 84f);
|
||||
p.lineTo(78f, 84f);
|
||||
p.cubicTo(75f, 64f, 74f, 46f, 71.5f, 28f);
|
||||
p.close();
|
||||
c.drawPath(p, fill);
|
||||
c.restore();
|
||||
|
||||
// Neck, jacket, shirt collar, badge.
|
||||
fill.setColor(skin);
|
||||
c.drawRect(44.5f, 48f, 55.5f, 70f, fill);
|
||||
fill.setColor(skinShade);
|
||||
c.drawOval(oval(44f, 50f, 56f, 57f), fill);
|
||||
p.reset();
|
||||
p.moveTo(10f, 100f);
|
||||
p.lineTo(14f, 80f);
|
||||
p.cubicTo(16f, 71f, 26f, 68f, 40f, 66f);
|
||||
p.lineTo(60f, 66f);
|
||||
p.cubicTo(74f, 68f, 84f, 71f, 86f, 80f);
|
||||
p.lineTo(90f, 100f);
|
||||
p.close();
|
||||
fill.setColor(accent);
|
||||
c.drawPath(p, fill);
|
||||
triangle(c, 40.5f, 66f, 59.5f, 66f, 50f, 85f, shirt);
|
||||
triangle(c, 45f, 66f, 55f, 66f, 50f, 74f, skin);
|
||||
stroke.setColor(accentDark);
|
||||
stroke.setStrokeWidth(2.2f);
|
||||
c.drawLine(40.5f, 66.5f, 49f, 90f, stroke);
|
||||
c.drawLine(59.5f, 66.5f, 51f, 90f, stroke);
|
||||
stroke.setColor(shirt);
|
||||
stroke.setStrokeWidth(1.6f);
|
||||
c.drawLine(16f, 79f, 38f, 70f, stroke);
|
||||
c.drawLine(84f, 79f, 62f, 70f, stroke);
|
||||
badge(c, 68f, 86f, 4f);
|
||||
|
||||
// The head.
|
||||
c.save();
|
||||
c.rotate(tilt, 50f, 62f);
|
||||
p.reset();
|
||||
p.moveTo(35f, 28f);
|
||||
p.cubicTo(35f, 44f, 42f, 55f, 50f, 57f);
|
||||
p.cubicTo(58f, 55f, 65f, 44f, 65f, 28f);
|
||||
p.cubicTo(65f, 14f, 35f, 14f, 35f, 28f);
|
||||
p.close();
|
||||
fill.setColor(skin);
|
||||
c.drawPath(p, fill);
|
||||
|
||||
boolean blink = t % 4200 < 260;
|
||||
boolean blush = mood == MascotMood.CHEERING || mood == MascotMood.FAST || mood == MascotMood.SLEEPING;
|
||||
if (blush) {
|
||||
fill.setColor(0x55FF7A8A);
|
||||
c.drawOval(oval(37.5f, 43.2f, 43.5f, 46f), fill);
|
||||
c.drawOval(oval(56.5f, 43.2f, 62.5f, 46f), fill);
|
||||
}
|
||||
eye(c, 43f, 38f, mood, blink, true);
|
||||
eye(c, 57f, 38f, mood, blink, false);
|
||||
stroke.setColor(skinShade);
|
||||
stroke.setStrokeWidth(0.8f);
|
||||
c.drawLine(50f, 43.5f, 49.3f, 45.2f, stroke);
|
||||
mouth(c, mood);
|
||||
|
||||
drawFrontHair(c);
|
||||
brows(c, mood);
|
||||
drawHeadset(c);
|
||||
if (mood == MascotMood.WORRIED) sweatDrop(c);
|
||||
c.restore();
|
||||
|
||||
if (mood == MascotMood.SLEEPING) sleepZs(c, t);
|
||||
if (mood == MascotMood.CHEERING) sparkles(c, t);
|
||||
if (mood == MascotMood.FAST) wind(c, t);
|
||||
}
|
||||
|
||||
private void drawFrontHair(Canvas c) {
|
||||
fill.setColor(hair);
|
||||
p.reset();
|
||||
p.moveTo(33f, 36f);
|
||||
p.lineTo(33.5f, 24f);
|
||||
p.cubicTo(36f, 10f, 64f, 10f, 66.5f, 24f);
|
||||
p.lineTo(67f, 36f);
|
||||
p.lineTo(64f, 28f);
|
||||
p.lineTo(61.5f, 33f);
|
||||
p.lineTo(58.5f, 26.5f);
|
||||
p.lineTo(55f, 32f);
|
||||
p.lineTo(51.5f, 25.5f);
|
||||
p.lineTo(48f, 31.5f);
|
||||
p.lineTo(44.5f, 26f);
|
||||
p.lineTo(41.5f, 32.5f);
|
||||
p.lineTo(38.5f, 27f);
|
||||
p.lineTo(36f, 34f);
|
||||
p.close();
|
||||
c.drawPath(p, fill);
|
||||
// Side locks framing the face, the right one mirrored.
|
||||
p.reset();
|
||||
p.moveTo(33.5f, 26f);
|
||||
p.cubicTo(31f, 38f, 31f, 50f, 33f, 61f);
|
||||
p.lineTo(36.2f, 52f);
|
||||
p.cubicTo(35.5f, 44f, 36f, 36f, 37.5f, 30f);
|
||||
p.close();
|
||||
c.drawPath(p, fill);
|
||||
c.save();
|
||||
c.scale(-1f, 1f, 50f, 0f);
|
||||
c.drawPath(p, fill);
|
||||
c.restore();
|
||||
stroke.setColor(hairLight);
|
||||
stroke.setStrokeWidth(1.3f);
|
||||
c.drawArc(oval(38f, 13.5f, 62f, 30f), 205f, 55f, false, stroke);
|
||||
}
|
||||
|
||||
private void drawHeadset(Canvas c) {
|
||||
stroke.setColor(headset);
|
||||
stroke.setStrokeWidth(2.2f);
|
||||
c.drawArc(oval(31f, 9.5f, 69f, 48f), 180f, 180f, false, stroke);
|
||||
fill.setColor(cup);
|
||||
c.drawRoundRect(oval(28.5f, 29f, 36f, 42f), 2.5f, 2.5f, fill);
|
||||
c.drawRoundRect(oval(64f, 29f, 71.5f, 42f), 2.5f, 2.5f, fill);
|
||||
fill.setColor(accent);
|
||||
c.drawRect(29.5f, 33.5f, 30.6f, 37.5f, fill);
|
||||
c.drawRect(69.4f, 33.5f, 70.5f, 37.5f, fill);
|
||||
stroke.setStrokeWidth(1f);
|
||||
p.reset();
|
||||
p.moveTo(67.5f, 41f);
|
||||
p.quadTo(66f, 50f, 58.5f, 50.5f);
|
||||
c.drawPath(p, stroke);
|
||||
fill.setColor(headset);
|
||||
c.drawCircle(57.6f, 50.5f, 1.3f, fill);
|
||||
}
|
||||
|
||||
private void eye(Canvas c, float cx, float cy, MascotMood mood, boolean blink, boolean left) {
|
||||
stroke.setColor(lash);
|
||||
if (mood == MascotMood.SLEEPING || (blink && mood != MascotMood.CHEERING)) {
|
||||
// Closed, calm: a lid line curving down.
|
||||
stroke.setStrokeWidth(1.2f);
|
||||
c.drawArc(oval(cx - 4f, cy - 3.2f, cx + 4f, cy + 1.6f), 20f, 140f, false, stroke);
|
||||
return;
|
||||
}
|
||||
if (mood == MascotMood.CHEERING) {
|
||||
// Happy, squeezed shut: ^ ^.
|
||||
stroke.setStrokeWidth(1.4f);
|
||||
c.drawArc(oval(cx - 4f, cy - 1.5f, cx + 4f, cy + 4.5f), 200f, 140f, false, stroke);
|
||||
return;
|
||||
}
|
||||
float look = mood == MascotMood.REVERSE ? -1.6f : 0f;
|
||||
float irisW = mood == MascotMood.WORRIED ? 1.7f : 2.3f;
|
||||
fill.setColor(Color.WHITE);
|
||||
oval(cx - 4f, cy - 2.6f, cx + 4f, cy + 2.8f);
|
||||
c.drawOval(r, fill);
|
||||
c.save();
|
||||
p.reset();
|
||||
p.addOval(r, Path.Direction.CW);
|
||||
c.clipPath(p);
|
||||
fill.setColor(iris);
|
||||
c.drawOval(oval(cx - irisW + look, cy - 2.8f, cx + irisW + look, cy + 2.8f), fill);
|
||||
fill.setColor(pupil);
|
||||
c.drawOval(oval(cx - irisW * 0.5f + look, cy - 1.5f, cx + irisW * 0.5f + look, cy + 1.6f), fill);
|
||||
fill.setColor(Color.WHITE);
|
||||
c.drawCircle(cx - 0.8f + look, cy - 1.2f, 0.75f, fill);
|
||||
c.drawCircle(cx + 0.8f + look, cy + 1.2f, 0.35f, fill);
|
||||
if (mood == MascotMood.DETERMINED) {
|
||||
// Narrowed: the lid comes down over the top of the eye.
|
||||
fill.setColor(skin);
|
||||
c.drawRect(cx - 4.5f, cy - 3.2f, cx + 4.5f, cy - 1.0f, fill);
|
||||
}
|
||||
c.restore();
|
||||
stroke.setStrokeWidth(1.25f);
|
||||
if (mood == MascotMood.DETERMINED) {
|
||||
c.drawLine(cx - 4.3f, cy - 0.9f, cx + 4.3f, cy - 1.1f, stroke);
|
||||
} else {
|
||||
c.drawArc(oval(cx - 4.4f, cy - 3f, cx + 4.4f, cy + 3.4f), 195f, 150f, false, stroke);
|
||||
}
|
||||
// The flick at the outer corner.
|
||||
float outer = left ? cx - 4.2f : cx + 4.2f, dir = left ? -1f : 1f;
|
||||
c.drawLine(outer, cy - 0.6f, outer + dir * 1.3f, cy - 1.6f, stroke);
|
||||
}
|
||||
|
||||
private void brows(Canvas c, MascotMood mood) {
|
||||
stroke.setColor(hair);
|
||||
stroke.setStrokeWidth(1.1f);
|
||||
switch (mood) {
|
||||
case WORRIED:
|
||||
c.drawLine(39f, 31.5f, 46.5f, 29.3f, stroke);
|
||||
c.drawLine(53.5f, 29.3f, 61f, 31.5f, stroke);
|
||||
break;
|
||||
case DETERMINED:
|
||||
c.drawLine(39f, 29.6f, 46.8f, 32f, stroke);
|
||||
c.drawLine(53.2f, 32f, 61f, 29.6f, stroke);
|
||||
break;
|
||||
default:
|
||||
float lift = mood == MascotMood.CHEERING || mood == MascotMood.FAST ? -0.8f : 0f;
|
||||
c.drawArc(oval(39f, 30f + lift, 47f, 33.5f + lift), 200f, 120f, false, stroke);
|
||||
c.drawArc(oval(53f, 30f + lift, 61f, 33.5f + lift), 220f, 120f, false, stroke);
|
||||
}
|
||||
}
|
||||
|
||||
private void mouth(Canvas c, MascotMood mood) {
|
||||
stroke.setColor(lips);
|
||||
stroke.setStrokeWidth(1f);
|
||||
fill.setColor(mouth);
|
||||
switch (mood) {
|
||||
case CHEERING:
|
||||
case FAST:
|
||||
p.reset();
|
||||
p.moveTo(45.5f, 48.8f);
|
||||
p.quadTo(50f, 56f, 54.5f, 48.8f);
|
||||
p.close();
|
||||
c.drawPath(p, fill);
|
||||
fill.setColor(0xFFE88A95);
|
||||
c.drawOval(oval(47.8f, 51.2f, 52.2f, 52.8f), fill);
|
||||
break;
|
||||
case SLEEPING:
|
||||
case REVERSE:
|
||||
c.drawOval(oval(48.8f, 49.3f, 51.2f, 51.6f), fill);
|
||||
break;
|
||||
case WORRIED:
|
||||
p.reset();
|
||||
p.moveTo(46.5f, 51f);
|
||||
p.quadTo(47.8f, 49.6f, 49.2f, 51f);
|
||||
p.quadTo(50.6f, 52.4f, 52f, 51f);
|
||||
p.quadTo(53.2f, 49.8f, 54f, 50.8f);
|
||||
c.drawPath(p, stroke);
|
||||
break;
|
||||
case DETERMINED:
|
||||
p.reset();
|
||||
p.moveTo(46f, 50.6f);
|
||||
p.quadTo(51f, 52.6f, 55f, 49f);
|
||||
c.drawPath(p, stroke);
|
||||
break;
|
||||
default:
|
||||
c.drawArc(oval(46f, 46.5f, 54f, 51.2f), 25f, 130f, false, stroke);
|
||||
}
|
||||
}
|
||||
|
||||
private void sweatDrop(Canvas c) {
|
||||
fill.setColor(0xFF8FD3FF);
|
||||
p.reset();
|
||||
p.moveTo(63f, 21f);
|
||||
p.quadTo(66.5f, 26.5f, 63f, 28f);
|
||||
p.quadTo(59.5f, 26.5f, 63f, 21f);
|
||||
c.drawPath(p, fill);
|
||||
}
|
||||
|
||||
/** Three Zs drifting up and to the left, over three seconds. */
|
||||
private void sleepZs(Canvas c, long t) {
|
||||
float phase = (t % 3000) / 3000f;
|
||||
text.setTextAlign(Paint.Align.CENTER);
|
||||
float[][] zs = { { 24f, 22f, 5f }, { 17f, 14f, 6.5f }, { 9f, 5.5f, 8f } };
|
||||
for (float[] z : zs) {
|
||||
text.setTextSize(z[2]);
|
||||
text.setColor(AccentColor.withAlpha(accentLight, Math.round(255 * (1f - phase * 0.5f))));
|
||||
c.drawText("Z", z[0] - phase * 2f, z[1] + z[2] - phase * 3f, text);
|
||||
}
|
||||
}
|
||||
|
||||
/** Four-pointed sparkles around her head, twinkling. */
|
||||
private void sparkles(Canvas c, long t) {
|
||||
float[][] at = { { 22f, 20f }, { 78f, 14f }, { 18f, 46f }, { 84f, 40f }, { 50f, 3f } };
|
||||
for (int i = 0; i < at.length; i++) {
|
||||
float s = 2.2f + 1.6f * (float) Math.abs(Math.sin(t / 260.0 + i * 1.3));
|
||||
fill.setColor(i % 2 == 0 ? regen : Color.WHITE);
|
||||
star(c, at[i][0], at[i][1], s);
|
||||
}
|
||||
}
|
||||
|
||||
/** Streaks rushing past on her left. */
|
||||
private void wind(Canvas c, long t) {
|
||||
stroke.setStrokeWidth(0.9f);
|
||||
stroke.setColor(0x88FFFFFF);
|
||||
for (int i = 0; i < 4; i++) {
|
||||
float x = 26f - ((t / 6f + i * 37f) % 24f);
|
||||
float y = 16f + i * 10f;
|
||||
c.drawLine(x - 9f, y, x, y, stroke);
|
||||
}
|
||||
}
|
||||
|
||||
private void star(Canvas c, float x, float y, float s) {
|
||||
p.reset();
|
||||
p.moveTo(x, y - s);
|
||||
p.quadTo(x, y, x + s, y);
|
||||
p.quadTo(x, y, x, y + s);
|
||||
p.quadTo(x, y, x - s, y);
|
||||
p.quadTo(x, y, x, y - s);
|
||||
p.close();
|
||||
c.drawPath(p, fill);
|
||||
}
|
||||
|
||||
/** A small silver octagon on the jacket. */
|
||||
private void badge(Canvas c, float x, float y, float s) {
|
||||
p.reset();
|
||||
for (int i = 0; i < 8; i++) {
|
||||
double a = Math.toRadians(22.5 + i * 45);
|
||||
float px = x + s * (float) Math.cos(a), py = y + s * (float) Math.sin(a);
|
||||
if (i == 0) p.moveTo(px, py);
|
||||
else p.lineTo(px, py);
|
||||
}
|
||||
p.close();
|
||||
fill.setColor(0xFFD7DBE0);
|
||||
c.drawPath(p, fill);
|
||||
text.setTextAlign(Paint.Align.CENTER);
|
||||
text.setTextSize(3.4f);
|
||||
text.setColor(accentDark);
|
||||
c.drawText("MG", x, y + 1.2f, text);
|
||||
}
|
||||
|
||||
private void triangle(Canvas c, float x1, float y1, float x2, float y2, float x3, float y3, int colour) {
|
||||
p.reset();
|
||||
p.moveTo(x1, y1);
|
||||
p.lineTo(x2, y2);
|
||||
p.lineTo(x3, y3);
|
||||
p.close();
|
||||
fill.setColor(colour);
|
||||
c.drawPath(p, fill);
|
||||
}
|
||||
|
||||
private RectF oval(float left, float top, float right, float bottom) {
|
||||
r.set(left, top, right, bottom);
|
||||
return r;
|
||||
}
|
||||
|
||||
// ---------- Speech bubble and values, in pixels ----------
|
||||
|
||||
private void drawBubble(Canvas c, MascotMood mood, long t, float x0, float h, float tipX, float tipY, float w) {
|
||||
String[] options = lines[character.ordinal()][mood.ordinal()];
|
||||
String line = options.length == 0 ? "" : options[(int) ((t / 30000 + mood.ordinal()) % options.length)];
|
||||
text.setTextAlign(Paint.Align.LEFT);
|
||||
float pad = h * 0.045f, room = w - 140f * dp - x0 - 2f * pad;
|
||||
// Shrink a long line to fit the room left of the clock.
|
||||
text.setTextSize(h * 0.07f);
|
||||
float width = text.measureText(line);
|
||||
if (width > room && room > 0f) {
|
||||
text.setTextSize(h * 0.07f * room / width);
|
||||
width = room;
|
||||
}
|
||||
float right = x0 + width + 2f * pad;
|
||||
float top = h * 0.05f, bottom = top + h * 0.07f + 2f * pad;
|
||||
fill.setColor(0xFFF7F7FA);
|
||||
c.drawRoundRect(oval(x0, top, right, bottom), h * 0.05f, h * 0.05f, fill);
|
||||
triangle(c, x0 + h * 0.03f, bottom - 2f, x0 + h * 0.11f, bottom - 2f, tipX, tipY, 0xFFF7F7FA);
|
||||
text.setColor(0xFF1C1A24);
|
||||
c.drawText(line, x0 + pad, bottom - pad - h * 0.012f, text);
|
||||
}
|
||||
|
||||
/** Speed and gear from x0; the battery and the bottom row from xLow, clear of her shoulder. */
|
||||
private void drawValues(Canvas c, float x0, float xLow, float w, float h) {
|
||||
float right = w - 16f * dp;
|
||||
// The clock, left of the full-screen button.
|
||||
text.setTextAlign(Paint.Align.RIGHT);
|
||||
text.setTextSize(20f * dp);
|
||||
text.setColor(grey);
|
||||
c.drawText(v.clock.isEmpty() ? "—" : v.clock, w - 72f * dp, 30f * dp, text);
|
||||
|
||||
// Speed, large, and the gear in a ring at the right.
|
||||
text.setTextAlign(Paint.Align.LEFT);
|
||||
text.setTextSize(h * 0.34f);
|
||||
text.setColor(white);
|
||||
String speed = fmt(v.speedKmh, "%.0f", "—");
|
||||
float baseline = h * 0.64f;
|
||||
c.drawText(speed, x0, baseline, text);
|
||||
float after = x0 + text.measureText(speed) + 14f * dp;
|
||||
text.setTextSize(h * 0.065f);
|
||||
text.setColor(grey);
|
||||
c.drawText("km/h", after, baseline, text);
|
||||
|
||||
float gr = h * 0.11f, gx = right - gr, gy = h * 0.50f;
|
||||
stroke.setColor(accent);
|
||||
stroke.setStrokeWidth(4f * dp);
|
||||
c.drawCircle(gx, gy, gr, stroke);
|
||||
String gear = Gear.label(v.gear);
|
||||
text.setTextAlign(Paint.Align.CENTER);
|
||||
text.setTextSize(gr * 1.1f);
|
||||
text.setColor(white);
|
||||
c.drawText(gear.isEmpty() ? " " : gear, gx, gy + gr * 0.38f, text);
|
||||
|
||||
// Battery: percent and range over a bar.
|
||||
float barY = h * 0.77f, barH = 12f * dp;
|
||||
boolean low = v.socPercent != null && v.socPercent < MascotMood.LOW_SOC;
|
||||
text.setTextSize(h * 0.055f);
|
||||
text.setTextAlign(Paint.Align.LEFT);
|
||||
text.setColor(low ? warn : white);
|
||||
c.drawText(fmt(v.socPercent, "%.0f %%", "— %"), xLow, barY - 10f * dp, text);
|
||||
text.setTextAlign(Paint.Align.RIGHT);
|
||||
text.setColor(white);
|
||||
c.drawText(fmt(v.rangeKm, "%.0f km", "— km"), right, barY - 10f * dp, text);
|
||||
fill.setColor(track);
|
||||
c.drawRoundRect(oval(xLow, barY, right, barY + barH), barH / 2f, barH / 2f, fill);
|
||||
if (v.socPercent != null) {
|
||||
float soc = Math.max(0f, Math.min(100f, v.socPercent)) / 100f;
|
||||
fill.setColor(low ? warn : accent);
|
||||
c.drawRoundRect(oval(xLow, barY, xLow + Math.max(barH, (right - xLow) * soc), barY + barH), barH / 2f, barH / 2f, fill);
|
||||
}
|
||||
|
||||
// Power, usage, trip.
|
||||
float col = (right - xLow) / 3f, labelY = h * 0.885f, valueY = h * 0.965f;
|
||||
boolean regenerating = v.powerKw != null && v.powerKw < 0f;
|
||||
cell(c, xLow, labelY, valueY, "POWER", fmt(v.powerKw, "%.1f kW", "— kW"), regenerating ? regen : white);
|
||||
cell(c, xLow + col, labelY, valueY, "USAGE · 5 MIN", fmt(v.usage5Min, "%.1f kWh/100 km", "—"), white);
|
||||
cell(c, xLow + 2f * col, labelY, valueY, "TRIP", fmt(v.tripKm, "%.1f km", "— km"), white);
|
||||
}
|
||||
|
||||
private void cell(Canvas c, float x, float labelY, float valueY, String label, String value, int colour) {
|
||||
text.setTextAlign(Paint.Align.LEFT);
|
||||
text.setTextSize(13f * dp);
|
||||
text.setLetterSpacing(0.12f);
|
||||
text.setColor(grey);
|
||||
c.drawText(label, x, labelY, text);
|
||||
text.setLetterSpacing(0f);
|
||||
text.setTextSize(getHeight() * 0.055f);
|
||||
text.setColor(colour);
|
||||
c.drawText(value, x, valueY, text);
|
||||
}
|
||||
}
|
||||
@@ -6,7 +6,7 @@ package com.evsuite.abrp;
|
||||
*
|
||||
* - {@link #usesAccent}: drawn in the chosen accent colour, so Settings offers the accent
|
||||
* colour for it. Styles with only their own fixed palette (the starship console, the cartoon,
|
||||
* the blueprint, the flight display, the nixie tubes) ignore it and Settings greys it out.
|
||||
* the flight display, the nixie tubes) ignore it and Settings greys it out.
|
||||
* - {@link #hasOriginal}: has an original palette of its own as well (Retro's turquoise
|
||||
* display, the terminal's green phosphor); Settings then offers "Original colours" above the
|
||||
* accent colours, remembered per style under {@link #originalKey()}.
|
||||
@@ -21,14 +21,12 @@ enum OverviewStyle {
|
||||
FUTURISTIC(true, false),
|
||||
SCIFI(false, false),
|
||||
CARTOON(false, false),
|
||||
BLUEPRINT(false, false),
|
||||
MG(true, false),
|
||||
MINIMAL(true, false),
|
||||
TERMINAL(true, true),
|
||||
AVIATION(false, false),
|
||||
NIXIE(false, false),
|
||||
CHRONOGRAPH(true, false),
|
||||
MASCOT(true, false);
|
||||
CHRONOGRAPH(true, false);
|
||||
|
||||
static final String PREF_KEY = "overview_style";
|
||||
|
||||
@@ -40,6 +38,15 @@ enum OverviewStyle {
|
||||
this.hasOriginal = hasOriginal;
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether the style is shown in its own palette: always for one with no accent colour, and
|
||||
* for Retro and Terminal when "Original colours" is chosen. The app then matches it
|
||||
* (StylePalette).
|
||||
*/
|
||||
boolean showsOwnPalette(boolean originalChosen) {
|
||||
return !usesAccent || (hasOriginal && originalChosen);
|
||||
}
|
||||
|
||||
/** Where the choice between the original palette and the accent colour is kept. */
|
||||
String originalKey() {
|
||||
return "original_colours_" + name();
|
||||
@@ -48,7 +55,8 @@ enum OverviewStyle {
|
||||
/**
|
||||
* The stored style. Older values: "DASHBOARD" (Gauges before 2.22.3), and "RETRO_ACCENT" /
|
||||
* "TERMINAL_ACCENT" (separate entries before 2.27.2, now the style in the accent colour —
|
||||
* see {@link #wasAccentEntry}). Anything unknown is Tiles.
|
||||
* see {@link #wasAccentEntry}). Anything unknown is Tiles, including "BLUEPRINT" and
|
||||
* "MASCOT", the styles removed in 2.35.0.
|
||||
*/
|
||||
static OverviewStyle fromPref(String stored) {
|
||||
if ("DASHBOARD".equals(stored)) return GAUGES;
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
package com.evsuite.abrp;
|
||||
|
||||
/**
|
||||
* The app's colours while an Overview style shows its own palette: the theme overlay
|
||||
* (res/values/style_palettes.xml) that MainActivity lays over the accent colour, so the
|
||||
* navigation and the rest of the app match the style — Retro's turquoise, Terminal's green
|
||||
* phosphor, and so on. Styles drawn in the accent colour have none.
|
||||
*/
|
||||
final class StylePalette {
|
||||
|
||||
private StylePalette() {}
|
||||
|
||||
/** The overlay for a style's own palette, or 0 for a style that has none. */
|
||||
static int overlay(OverviewStyle style) {
|
||||
switch (style) {
|
||||
case RETRO: return R.style.ThemeOverlay_EV_Style_Retro;
|
||||
case TERMINAL: return R.style.ThemeOverlay_EV_Style_Terminal;
|
||||
case SCIFI: return R.style.ThemeOverlay_EV_Style_SciFi;
|
||||
case CARTOON: return R.style.ThemeOverlay_EV_Style_Cartoon;
|
||||
case AVIATION: return R.style.ThemeOverlay_EV_Style_Aviation;
|
||||
case NIXIE: return R.style.ThemeOverlay_EV_Style_Nixie;
|
||||
default: return 0;
|
||||
}
|
||||
}
|
||||
|
||||
/** The overlay the app wears: the style's own when it shows its palette, else the accent's. */
|
||||
static int appOverlay(OverviewStyle style, boolean originalChosen, AccentColor accent) {
|
||||
int own = overlay(style);
|
||||
return own != 0 && style.showsOwnPalette(originalChosen) ? own : accent.overlay;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user