@@ -0,0 +1,280 @@
package com.evsuite.abrp ;
import android.content.Context ;
import android.graphics.Canvas ;
import android.graphics.Paint ;
import android.graphics.Path ;
import android.graphics.RectF ;
import android.graphics.Typeface ;
import android.util.AttributeSet ;
import android.view.View ;
import java.util.Locale ;
/**
* The MG Overview style, after the classic MG dashboards whose dials sat in octagonal bezels.
*
* Three octagonal dials with chrome bezels, cream scales and needles in the accent colour: the
* speedometer in the middle (with the speed as digits and P R N D underneath), the battery on
* the left (E to F, red below 20 %, range underneath) and power on the right (− 50 to 150 kW,
* the charge zone below zero in green). Below, a cream line of odometer, trip, usage, climate
* and clock, separated by small octagons. Unread values draw no needle and "—".
*
* Needles ease to a new value and the view redraws only until they are there, only on screen.
* The MG octagon logo itself is not drawn.
*/
public final class OctagonView extends View {
private static final float START_DEG = 135f , SWEEP_DEG = 270f ;
private static final long FRAME_MS = 33 ;
private final Paint face = new Paint ( Paint . ANTI_ALIAS_FLAG ) ;
private final Paint chrome = new Paint ( Paint . ANTI_ALIAS_FLAG ) ;
private final Paint tick = new Paint ( Paint . ANTI_ALIAS_FLAG ) ;
private final Paint needle = new Paint ( Paint . ANTI_ALIAS_FLAG ) ;
private final Paint zone = new Paint ( Paint . ANTI_ALIAS_FLAG ) ;
private final Paint text = new Paint ( Paint . ANTI_ALIAS_FLAG ) ;
private final Paint fill = new Paint ( Paint . ANTI_ALIAS_FLAG ) ;
private final Path path = new Path ( ) ;
private final RectF rect = new RectF ( ) ;
private final int accent , light , cream , chromeLight , chromeDark , faceColour , green , red ;
private ClusterValues v = new ClusterValues ( ) ;
/** The needles as shown, easing to their values; NaN until the first reading. */
private float speedShown = Float . NaN , socShown = Float . NaN , powerShown = Float . NaN ;
public OctagonView ( Context context , AttributeSet attrs ) {
super ( context , attrs ) ;
accent = AccentColor . resolve ( context , R . attr . evAccent ) ;
light = AccentColor . resolve ( context , R . attr . evAccentLight ) ;
cream = context . getColor ( R . color . mg_cream ) ;
chromeLight = context . getColor ( R . color . mg_chrome_light ) ;
chromeDark = context . getColor ( R . color . mg_chrome_dark ) ;
faceColour = context . getColor ( R . color . mg_face ) ;
green = context . getColor ( R . color . ev_ok ) ;
red = context . getColor ( R . color . ev_error ) ;
face . setColor ( faceColour ) ;
chrome . setStyle ( Paint . Style . STROKE ) ;
chrome . setStrokeJoin ( Paint . Join . ROUND ) ;
tick . setColor ( cream ) ;
tick . setStrokeCap ( Paint . Cap . ROUND ) ;
needle . setStrokeCap ( Paint . Cap . ROUND ) ;
zone . setStyle ( Paint . Style . STROKE ) ;
text . setColor ( cream ) ;
text . setTextAlign ( Paint . Align . CENTER ) ;
text . setFontFeatureSettings ( " tnum " ) ;
text . setTypeface ( Typeface . create ( " serif " , Typeface . NORMAL ) ) ;
}
void setValues ( ClusterValues values ) {
v = values = = null ? new ClusterValues ( ) : values ;
invalidate ( ) ;
}
@Override
protected void onDraw ( Canvas c ) {
super . onDraw ( c ) ;
float w = getWidth ( ) , h = getHeight ( ) ;
if ( w < = 0 | | h < = 0 ) return ;
float d = getResources ( ) . getDisplayMetrics ( ) . density ;
speedShown = ease ( speedShown , v . speedKmh , 180f ) ;
socShown = ease ( socShown , v . socPercent , 100f ) ;
powerShown = ease ( powerShown , v . powerKw , 200f ) ;
boolean moving = ( v . speedKmh ! = null & & speedShown ! = v . speedKmh )
| | ( v . socPercent ! = null & & socShown ! = v . socPercent )
| | ( v . powerKw ! = null & & powerShown ! = v . powerKw ) ;
float dialsBottom = h * 0 . 84f ;
float big = Math . min ( dialsBottom * 0 . 5f , w * 0 . 20f ) ;
float small = big * 0 . 68f ;
float cy = dialsBottom / 2f + 4f * d ;
float cx = w / 2f ;
float sideX = Math . min ( w * 0 . 21f , cx - big - small - 20f * d ) ;
// Battery, left.
bezel ( c , sideX + small * 0 . 05f , cy + big - small , small , d ) ;
dial ( c , sideX + small * 0 . 05f , cy + big - small , small , 0f , 100f , 25f , 5 ,
v . socPercent = = null ? null : socShown , 20f , false , new String [ ] { " E " , " ½ " , " F " } , d ) ;
label ( c , sideX + small * 0 . 05f , cy + big - small , small , " BATTERY " ,
v . socPercent = = null ? " — " : String . format ( Locale . US , " %.0f%% " , v . socPercent ) ,
v . rangeKm = = null ? " — km " : String . format ( Locale . US , " %.0f km " , v . rangeKm ) , 0 . 20f ) ;
// Power, right.
float rx = w - sideX - small * 0 . 05f ;
bezel ( c , rx , cy + big - small , small , d ) ;
dial ( c , rx , cy + big - small , small , - 50f , 150f , 50f , 5 ,
v . powerKw = = null ? null : powerShown , null , true , null , d ) ;
label ( c , rx , cy + big - small , small , " kW " ,
v . powerKw = = null ? " — " : String . format ( Locale . US , " %.1f " , v . powerKw ) ,
v . powerKw ! = null & & v . powerKw < 0f ? " CHARGE " : " POWER " , 0 . 20f ) ;
// Speed, centre.
bezel ( c , cx , cy , big , d ) ;
dial ( c , cx , cy , big , 0f , 180f , 20f , 4 , v . speedKmh = = null ? null : speedShown , null , false , null , d ) ;
label ( c , cx , cy , big , " km/h " ,
v . speedKmh = = null ? " — " : String . format ( Locale . US , " %.0f " , v . speedKmh ) , null , 0 . 24f ) ;
gears ( c , cx , cy + big * 0 . 62f , big , d ) ;
drawLine ( c , w , h , d ) ;
if ( moving & & isShown ( ) ) postInvalidateDelayed ( FRAME_MS ) ;
}
/** One step of a needle towards its value; jumps there when close or on the first value. */
private static float ease ( float shown , Float target , float span ) {
if ( target = = null | | ! Float . isFinite ( target ) ) return shown ;
if ( Float . isNaN ( shown ) ) return target ;
float next = shown + ( target - shown ) * 0 . 25f ;
return Math . abs ( target - next ) < span * 0 . 002f ? target : next ;
}
/** A regular octagon with flat sides at top, bottom and both sides. */
private void octagon ( float cx , float cy , float r ) {
path . reset ( ) ;
for ( int i = 0 ; i < 8 ; i + + ) {
double a = Math . toRadians ( 22 . 5 + 45 * i ) ;
float x = cx + r * ( float ) Math . cos ( a ) , y = cy + r * ( float ) Math . sin ( a ) ;
if ( i = = 0 ) path . moveTo ( x , y ) ; else path . lineTo ( x , y ) ;
}
path . close ( ) ;
}
/** The bezel: a dark face inside a chrome octagon, lit from above (light outer, dark inner line). */
private void bezel ( Canvas c , float cx , float cy , float r , float d ) {
octagon ( cx , cy , r ) ;
c . drawPath ( path , face ) ;
chrome . setColor ( chromeLight ) ;
chrome . setStrokeWidth ( Math . max ( 3f * d , r * 0 . 035f ) ) ;
c . drawPath ( path , chrome ) ;
octagon ( cx , cy , r * 0 . 93f ) ;
chrome . setColor ( chromeDark ) ;
chrome . setStrokeWidth ( Math . max ( 2f * d , r * 0 . 02f ) ) ;
c . drawPath ( path , chrome ) ;
octagon ( cx , cy , r * 1 . 035f ) ;
chrome . setColor ( AccentColor . withAlpha ( chromeLight , 0x40 ) ) ;
chrome . setStrokeWidth ( d ) ;
c . drawPath ( path , chrome ) ;
}
/**
* A scale inside the octagon, its numbers or end labels, an optional warning zone below
* {@code warnBelow} (red) or charge zone below zero (green), and the needle when known.
*/
private void dial ( Canvas c , float cx , float cy , float r , float min , float max , float major ,
int minorPer , Float value , Float warnBelow , boolean chargeZone , String [ ] labels , float d ) {
float sr = r * 0 . 74f ;
rect . set ( cx - sr , cy - sr , cx + sr , cy + sr ) ;
zone . setStrokeWidth ( r * 0 . 05f ) ;
if ( warnBelow ! = null ) {
zone . setColor ( AccentColor . withAlpha ( red , 0xC0 ) ) ;
c . drawArc ( rect , START_DEG , SWEEP_DEG * ( warnBelow - min ) / ( max - min ) , false , zone ) ;
}
if ( chargeZone & & min < 0f ) {
zone . setColor ( AccentColor . withAlpha ( green , 0xC0 ) ) ;
c . drawArc ( rect , START_DEG , SWEEP_DEG * ( 0f - min ) / ( max - min ) , false , zone ) ;
}
float minor = major / minorPer ;
int steps = Math . round ( ( max - min ) / minor ) ;
text . setTextSize ( r * 0 . 13f ) ;
for ( int i = 0 ; i < = steps ; i + + ) {
float val = min + i * minor ;
boolean isMajor = i % minorPer = = 0 ;
double a = Math . toRadians ( START_DEG + SWEEP_DEG * ( val - min ) / ( max - min ) ) ;
float cos = ( float ) Math . cos ( a ) , sin = ( float ) Math . sin ( a ) ;
float outer = r * 0 . 82f , inner = isMajor ? r * 0 . 68f : r * 0 . 75f ;
tick . setStrokeWidth ( isMajor ? r * 0 . 025f : r * 0 . 012f ) ;
c . drawLine ( cx + outer * cos , cy + outer * sin , cx + inner * cos , cy + inner * sin , tick ) ;
if ( isMajor & & labels = = null ) {
float lr = r * 0 . 54f ;
c . drawText ( String . format ( Locale . US , " %.0f " , val ) , cx + lr * cos ,
cy + lr * sin + text . getTextSize ( ) * 0 . 36f , text ) ;
}
}
if ( labels ! = null ) {
text . setTextSize ( r * 0 . 16f ) ;
for ( int i = 0 ; i < labels . length ; i + + ) {
double a = Math . toRadians ( START_DEG + SWEEP_DEG * i / ( labels . length - 1f ) ) ;
float lr = r * 0 . 52f ;
c . drawText ( labels [ i ] , cx + lr * ( float ) Math . cos ( a ) , cy + lr * ( float ) Math . sin ( a ) + text . getTextSize ( ) * 0 . 36f , text ) ;
}
}
if ( value ! = null ) {
double a = Math . toRadians ( START_DEG + SWEEP_DEG * ( Math . max ( min , Math . min ( max , value ) ) - min ) / ( max - min ) ) ;
float cos = ( float ) Math . cos ( a ) , sin = ( float ) Math . sin ( a ) ;
needle . setColor ( AccentColor . withAlpha ( accent , 0x50 ) ) ;
needle . setStrokeWidth ( r * 0 . 06f ) ;
c . drawLine ( cx - r * 0 . 12f * cos , cy - r * 0 . 12f * sin , cx + r * 0 . 80f * cos , cy + r * 0 . 80f * sin , needle ) ;
needle . setColor ( accent ) ;
needle . setStrokeWidth ( r * 0 . 03f ) ;
c . drawLine ( cx - r * 0 . 12f * cos , cy - r * 0 . 12f * sin , cx + r * 0 . 80f * cos , cy + r * 0 . 80f * sin , needle ) ;
}
// The hub: a small chrome octagon.
octagon ( cx , cy , r * 0 . 08f ) ;
fill . setColor ( chromeLight ) ;
c . drawPath ( path , fill ) ;
octagon ( cx , cy , r * 0 . 045f ) ;
fill . setColor ( faceColour ) ;
c . drawPath ( path , fill ) ;
}
/** Under the hub: a small caption, the value large, and a line below it. */
private void label ( Canvas c , float cx , float cy , float r , String caption , String value , String below ,
float valueSize ) {
text . setTextSize ( r * 0 . 10f ) ;
text . setColor ( AccentColor . withAlpha ( cream , 0xB0 ) ) ;
text . setLetterSpacing ( 0 . 15f ) ;
c . drawText ( caption , cx , cy + r * 0 . 25f , text ) ;
text . setLetterSpacing ( 0f ) ;
text . setColor ( cream ) ;
text . setTextSize ( r * valueSize ) ;
c . drawText ( value , cx , cy + r * 0 . 50f , text ) ;
if ( below ! = null ) {
text . setTextSize ( r * 0 . 11f ) ;
text . setColor ( light ) ;
c . drawText ( below , cx , cy + r * 0 . 67f , text ) ;
text . setColor ( cream ) ;
}
}
/** P R N D under the speed; the engaged one in the accent colour. */
private void gears ( Canvas c , float cx , float y , float r , float d ) {
String [ ] letters = { " P " , " R " , " N " , " D " } ;
text . setTextSize ( r * 0 . 10f ) ;
float pitch = r * 0 . 13f ;
for ( int i = 0 ; i < 4 ; i + + ) {
boolean on = v . gear ! = null & & v . gear = = i + 1 ;
text . setColor ( on ? light : AccentColor . withAlpha ( cream , 0x55 ) ) ;
text . setFakeBoldText ( on ) ;
c . drawText ( letters [ i ] , cx + ( i - 1 . 5f ) * pitch , y + r * 0 . 10f , text ) ;
}
text . setFakeBoldText ( false ) ;
text . setColor ( cream ) ;
}
/** The cream line along the bottom, items separated by small octagons. */
private void drawLine ( Canvas c , float w , float h , float d ) {
String [ ] items = {
v . odometerKm = = null ? " ODO — " : String . format ( Locale . US , " ODO %,.0f km " , v . odometerKm ) ,
v . tripKm = = null ? " TRIP — " : String . format ( Locale . US , " TRIP %.1f km " , v . tripKm ) ,
v . usage5Min = = null ? " — kWh/100 km " : String . format ( Locale . US , " %.1f kWh/100 km " , v . usage5Min ) ,
v . climateCelsius = = null ? " — °C " : String . format ( Locale . US , " %.0f °C " , v . climateCelsius ) ,
v . clock . isEmpty ( ) ? " — " : v . clock ,
} ;
float y = h * 0 . 95f ;
text . setTextSize ( 20f * d ) ;
text . setLetterSpacing ( 0 . 08f ) ;
float pitch = w / items . length ;
for ( int i = 0 ; i < items . length ; i + + ) {
c . drawText ( items [ i ] , pitch * ( i + 0 . 5f ) , y , text ) ;
if ( i > 0 ) {
octagon ( pitch * i , y - 7f * d , 5f * d ) ;
fill . setColor ( accent ) ;
c . drawPath ( path , fill ) ;
}
}
text . setLetterSpacing ( 0f ) ;
chrome . setColor ( chromeDark ) ;
chrome . setStrokeWidth ( d ) ;
c . drawLine ( w * 0 . 06f , h * 0 . 885f , w * 0 . 94f , h * 0 . 885f , chrome ) ;
}
}