@@ -0,0 +1,511 @@
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 "—".
*
* 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 {
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 } ;
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 ] [ ] ;
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 ] = context . getResources ( ) . getStringArray ( LINES [ i ] ) ;
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 " ) ;
}
@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 ) ;
drawCharacter ( c , mood , t ) ;
c . restore ( ) ;
float x0 = ox + 78f * k ;
drawBubble ( c , mood , t , x0 , h , ox + 70f * k , oy + 46f * 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 [ 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 ) ;
}
}