Vehicle data screen: Add speed and range + gauge icons

This commit is contained in:
johan12345
2021-08-15 15:36:52 +02:00
parent 71f1ee8d7b
commit 6c2243078b
8 changed files with 269 additions and 34 deletions

View File

@@ -6,6 +6,7 @@
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="androidx.car.app.MAP_TEMPLATES" />
<uses-permission android:name="com.google.android.gms.permission.CAR_FUEL" />
<uses-permission android:name="com.google.android.gms.permission.CAR_SPEED" />
<uses-sdk tools:overrideLibrary="androidx.car.app,androidx.car.app.projected" />

View File

@@ -26,15 +26,11 @@ class FilterScreen(ctx: CarContext) : Screen(ctx) {
init {
val size = (ctx.resources.displayMetrics.density * 24).roundToInt()
emptyIcon = CarIcon.Builder(
IconCompat.createWithBitmap(
Bitmap.createBitmap(
size,
size,
Bitmap.Config.ARGB_8888
)
)
).build()
emptyIcon = Bitmap.createBitmap(
size,
size,
Bitmap.Config.ARGB_8888
).asCarIcon()
}
init {

View File

@@ -1,9 +1,14 @@
package net.vonforst.evmap.auto
import android.graphics.Bitmap
import androidx.car.app.CarContext
import androidx.car.app.constraints.ConstraintManager
import androidx.car.app.hardware.common.CarUnit
import androidx.car.app.model.CarColor
import androidx.car.app.model.CarIcon
import androidx.core.graphics.drawable.IconCompat
import net.vonforst.evmap.api.availability.ChargepointStatus
import java.util.*
fun carAvailabilityColor(status: List<ChargepointStatus>): CarColor {
val unknown = status.any { it == ChargepointStatus.UNKNOWN }
@@ -22,4 +27,45 @@ fun carAvailabilityColor(status: List<ChargepointStatus>): CarColor {
}
val CarContext.constraintManager
get() = getCarService(CarContext.CONSTRAINT_SERVICE) as ConstraintManager
get() = getCarService(CarContext.CONSTRAINT_SERVICE) as ConstraintManager
fun Bitmap.asCarIcon(): CarIcon = CarIcon.Builder(IconCompat.createWithBitmap(this)).build()
private const val kmPerMile = 1.609344
fun getDefaultDistanceUnit(): Int {
return when (Locale.getDefault().country) {
"US", "GB", "MM", "LR" -> CarUnit.MILE
else -> CarUnit.KILOMETER
}
}
fun getDefaultSpeedUnit(): Int {
return when (Locale.getDefault().country) {
"US", "GB", "MM", "LR" -> CarUnit.MILES_PER_HOUR
else -> CarUnit.KILOMETERS_PER_HOUR
}
}
fun formatCarUnitDistance(value: Float?, unit: Int?): String {
if (value == null) return ""
return when (unit ?: getDefaultDistanceUnit()) {
// distance units: base unit is meters
CarUnit.METER -> "%.0f m".format(value)
CarUnit.KILOMETER -> "%.1f km".format(value / 1000)
CarUnit.MILLIMETER -> "%.0f mm".format(value * 1000) // whoever uses that...
CarUnit.MILE -> "%.1f mi".format(value / 1000 / kmPerMile)
else -> ""
}
}
fun formatCarUnitSpeed(value: Float?, unit: Int?): String {
if (value == null) return ""
return when (unit ?: getDefaultSpeedUnit()) {
// speed units: base unit is meters per second
CarUnit.METERS_PER_SEC -> "%.0f m/s".format(value)
CarUnit.KILOMETERS_PER_HOUR -> "%.0f km/h".format(value * 3.6)
CarUnit.MILES_PER_HOUR -> "%.0f mph".format(value * 3.6 / kmPerMile)
else -> ""
}
}

View File

@@ -8,17 +8,28 @@ import androidx.car.app.Screen
import androidx.car.app.hardware.CarHardwareManager
import androidx.car.app.hardware.info.EnergyLevel
import androidx.car.app.hardware.info.Model
import androidx.car.app.hardware.info.Speed
import androidx.car.app.model.*
import androidx.core.content.ContextCompat
import androidx.core.graphics.drawable.IconCompat
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.OnLifecycleEvent
import net.vonforst.evmap.R
import net.vonforst.evmap.ui.Gauge
import kotlin.math.min
import kotlin.math.roundToInt
class VehicleDataScreen(ctx: CarContext) : Screen(ctx) {
private val hardwareMan = ctx.getCarService(CarContext.HARDWARE_SERVICE) as CarHardwareManager
private var model: Model? = null
private var energyLevel: EnergyLevel? = null
private var speed: Speed? = null
private var gauge = Gauge((ctx.resources.displayMetrics.density * 128).roundToInt(), ctx)
private val maxSpeed = 160f / 3.6f // m/s, speed gauge will show max if speed is higher
private val permissions = listOf(
"com.google.android.gms.permission.CAR_FUEL"
"com.google.android.gms.permission.CAR_FUEL",
"com.google.android.gms.permission.CAR_SPEED"
)
override fun onGetTemplate(): Template {
@@ -29,7 +40,7 @@ class VehicleDataScreen(ctx: CarContext) : Screen(ctx) {
screenManager.pushForResult(
PermissionScreen(
carContext,
R.string.auto_location_permission_needed,
R.string.auto_vehicle_data_permission_needed,
permissions
)
) {
@@ -40,6 +51,7 @@ class VehicleDataScreen(ctx: CarContext) : Screen(ctx) {
val energyLevel = energyLevel
val model = model
val speed = speed
return GridTemplate.Builder().apply {
setTitle(
@@ -55,36 +67,122 @@ class VehicleDataScreen(ctx: CarContext) : Screen(ctx) {
} else {
setSingleList(
ItemList.Builder().apply {
addItem(
GridItem.Builder().apply {
setTitle("Battery")
energyLevel?.batteryPercent?.value?.let { percent ->
setText("%.1f".format(percent))
setImage(CarIcon.APP_ICON)
} ?: setLoading(true)
}.build()
)
addItem(
GridItem.Builder().apply {
setTitle("Fuel")
energyLevel?.fuelPercent?.value?.let { percent ->
setText("%.1f".format(percent))
setImage(CarIcon.APP_ICON)
} ?: setLoading(true)
}.build()
)
addItem(GridItem.Builder().apply {
setTitle(carContext.getString(R.string.auto_charging_level))
if (energyLevel == null) {
setLoading(true)
} else if (energyLevel.batteryPercent.value != null && energyLevel.fuelPercent.value != null) {
// both battery and fuel (Plug-in hybrid)
setText(
"\uD83D\uDD0C %.0f %% ⛽ %.0f %%".format(
energyLevel.batteryPercent.value,
energyLevel.fuelPercent.value
)
)
setImage(
gauge.draw(
energyLevel.batteryPercent.value,
energyLevel.fuelPercent.value
).asCarIcon()
)
} else if (energyLevel.batteryPercent.value != null) {
// BEV
setText("%.0f %%".format(energyLevel.batteryPercent.value))
setImage(gauge.draw(energyLevel.batteryPercent.value).asCarIcon())
} else if (energyLevel.fuelPercent.value != null) {
// ICE
setText("⛽ %.0f %%".format(energyLevel.fuelPercent.value))
setImage(gauge.draw(energyLevel.fuelPercent.value).asCarIcon())
} else {
setText(carContext.getString(R.string.auto_no_data))
setImage(gauge.draw(0f).asCarIcon())
}
}.build())
addItem(GridItem.Builder().apply {
setTitle(carContext.getString(R.string.auto_range))
if (energyLevel == null) {
setLoading(true)
} else if (energyLevel.rangeRemainingMeters.value != null) {
setText(
formatCarUnitDistance(
energyLevel.rangeRemainingMeters.value,
energyLevel.distanceDisplayUnit.value
)
)
setImage(
CarIcon.Builder(
IconCompat.createWithResource(
carContext,
R.drawable.ic_car
)
).build()
)
} else {
setText(carContext.getString(R.string.auto_no_data))
setImage(
CarIcon.Builder(
IconCompat.createWithResource(
carContext,
R.drawable.ic_car
)
).build()
)
}
}.build())
addItem(GridItem.Builder().apply {
setTitle(carContext.getString(R.string.auto_speed))
if (speed == null) {
setLoading(true)
} else {
val rawSpeed = speed.rawSpeedMetersPerSecond.value
val displaySpeed = speed.displaySpeedMetersPerSecond.value
if (rawSpeed != null) {
setText(
formatCarUnitSpeed(
rawSpeed,
speed.speedDisplayUnit.value
)
)
setImage(
gauge.draw(min(rawSpeed / maxSpeed * 100, 100f)).asCarIcon()
)
} else if (displaySpeed != null) {
setText(
formatCarUnitSpeed(
speed.displaySpeedMetersPerSecond.value,
speed.speedDisplayUnit.value
)
)
setImage(
gauge.draw(min(displaySpeed / maxSpeed * 100, 100f))
.asCarIcon()
)
} else {
setText(carContext.getString(R.string.auto_no_data))
setImage(gauge.draw(0f).asCarIcon())
}
}
}.build())
}.build()
)
}
}.build()
}
private fun onEnergyLevelUpdated(energyLevel: EnergyLevel) {
this.energyLevel = energyLevel
invalidate()
}
private fun onSpeedUpdated(speed: Speed) {
this.speed = speed
invalidate()
}
private fun setupListeners() {
val exec = ContextCompat.getMainExecutor(carContext)
hardwareMan.carInfo.addEnergyLevelListener(exec) {
this.energyLevel = it
invalidate()
}
hardwareMan.carInfo.addEnergyLevelListener(exec, ::onEnergyLevelUpdated)
hardwareMan.carInfo.addSpeedListener(exec, ::onSpeedUpdated)
hardwareMan.carInfo.fetchModel(exec) {
this.model = it
@@ -92,6 +190,12 @@ class VehicleDataScreen(ctx: CarContext) : Screen(ctx) {
}
}
@OnLifecycleEvent(Lifecycle.Event.ON_PAUSE)
private fun removeListeners() {
hardwareMan.carInfo.removeEnergyLevelListener(::onEnergyLevelUpdated)
hardwareMan.carInfo.removeSpeedListener(::onSpeedUpdated)
}
private fun permissionsGranted(): Boolean =
permissions.all {
ContextCompat.checkSelfPermission(

View File

@@ -0,0 +1,74 @@
package net.vonforst.evmap.ui
import android.content.Context
import android.graphics.*
import androidx.annotation.ColorInt
import androidx.core.content.ContextCompat
import net.vonforst.evmap.R
import kotlin.math.max
import kotlin.math.min
class Gauge(val size: Int, ctx: Context) {
val arcPaint = Paint().apply {
style = Paint.Style.STROKE
strokeWidth = size * 0.15f
}
val gaugePaint = Paint()
val activeColor = ContextCompat.getColor(ctx, R.color.gauge_active)
val middleColor = ContextCompat.getColor(ctx, R.color.gauge_middle)
val inactiveColor = ContextCompat.getColor(ctx, R.color.gauge_inactive)
fun draw(valuePercent: Float?, secondValuePercent: Float? = null): Bitmap {
val bitmap = Bitmap.createBitmap(size, size, Bitmap.Config.ARGB_8888)
val canvas = Canvas(bitmap)
canvas.drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR)
val angle = valuePercent?.let { 180f * it / 100 } ?: 0f
val secondAngle = secondValuePercent?.let { 180f * it / 100 }
drawArc(angle, secondAngle, canvas)
if (secondAngle != null) drawGauge(secondAngle, inactiveColor, canvas)
drawGauge(angle, Color.WHITE, canvas)
return bitmap
}
private fun drawGauge(angle: Float, @ColorInt color: Int, canvas: Canvas) {
gaugePaint.color = color
canvas.save()
canvas.rotate(angle - 90, size / 2f, 3 * size / 4f)
canvas.drawCircle(size / 2f, 3 * size / 4f, size * 0.1F, gaugePaint)
canvas.drawRect(size * 0.48f, 3 * size / 4f, size * 0.53f, size * 0.325f, gaugePaint)
canvas.restore()
}
private fun drawArc(angle: Float, secondAngle: Float?, canvas: Canvas) {
val (angle1, angle2) = if (secondAngle != null) {
min(angle, secondAngle) to max(angle, secondAngle)
} else {
angle to null
}
arcPaint.color = activeColor
val arcBounds = RectF(
arcPaint.strokeWidth / 2,
size / 4f + arcPaint.strokeWidth / 2,
size - arcPaint.strokeWidth / 2,
5 * size / 4f - arcPaint.strokeWidth / 2
)
canvas.drawArc(arcBounds, 180f, angle1, false, arcPaint)
if (angle2 != null) {
arcPaint.color = middleColor
canvas.drawArc(arcBounds, 180f + angle1, angle2 - angle1, false, arcPaint)
}
arcPaint.color = inactiveColor
canvas.drawArc(
arcBounds,
180f + (angle2 ?: angle1),
180f - (angle2 ?: angle1),
false,
arcPaint
)
}
}

View File

@@ -22,6 +22,10 @@
<string name="auto_fault_report_date">⚠️ Störungsmeldung (%s)</string>
<string name="auto_no_refresh_possible">Weitere Aktualisierung nicht möglich. Bitte zurück gehen und neu starten.</string>
<string name="auto_vehicle_data">Fahrzeugdaten</string>
<string name="auto_charging_level">Ladezustand</string>
<string name="auto_no_data">Nicht verfügbar</string>
<string name="auto_range">Reichweite</string>
<string name="auto_speed">Geschwindigkeit</string>
<string name="welcome_android_auto">Android Auto-Unterstützung</string>
<string name="welcome_android_auto_detail">Auf unterstützen Autos kannst du EVMap auch mit Android Auto nutzen. Öffne dazu einfach die EVMap-App aus dem Menü von Android Auto.</string>
<string name="sounds_cool">klingt cool</string>

View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="gauge_active">#00e676</color>
<color name="gauge_middle">#087f23</color>
<color name="gauge_inactive">#9e9e9e</color>
</resources>

View File

@@ -32,6 +32,10 @@
<string name="auto_fault_report_date">⚠️ Fault report (%s)</string>
<string name="auto_no_refresh_possible">Further updates not possible. Please go back and restart.</string>
<string name="auto_vehicle_data">Vehicle data</string>
<string name="auto_charging_level">Charging level</string>
<string name="auto_no_data">Unavailable</string>
<string name="auto_range">Range</string>
<string name="auto_speed">Speed</string>
<string name="welcome_android_auto">Android Auto support</string>
<string name="welcome_android_auto_detail">You can also use EVMap from within Android Auto on supported cars. Simply select the EVMap app in the Android Auto menu.</string>
<string name="sounds_cool">sounds cool</string>