Display value on top of bar

This commit is contained in:
Tim Kluge 2024-12-09 21:11:05 +01:00
parent bc782e7f90
commit b3547f6051
15 changed files with 148 additions and 51 deletions

View File

@ -22,7 +22,7 @@ to be displayed at the bottom or at the top of the screen:
- Average power over the last 10 seconds
Subsequently, the bar(s) will be shown when riding. Bars are filled and colored according
to your current power output / heart rate zone as setup in your Karoo settings.
to your current power output / heart rate zone as setup in your Karoo settings. Optionally, the actual data value can be displayed on top of the bar.
## Installation

View File

@ -13,8 +13,8 @@ android {
applicationId = "de.timklge.karoopowerbar"
minSdk = 26
targetSdk = 33
versionCode = 4
versionName = "1.1.1"
versionCode = 5
versionName = "1.2.0"
}
buildTypes {

View File

@ -3,8 +3,8 @@
"packageName": "de.timklge.karoopowerbar",
"iconUrl": "https://github.com/timklge/karoo-powerbar/releases/latest/download/karoo-powerbar.png",
"latestApkUrl": "https://github.com/timklge/karoo-powerbar/releases/latest/download/app-release.apk",
"latestVersion": "1.1.1",
"latestVersionCode": 4,
"latestVersion": "1.2.0",
"latestVersionCode": 5,
"developer": "timklge",
"description": "Adds a colored power bar to the bottom of the screen",
"releaseNotes": "Add options to add secondary power bar and to hide bar when not riding. Fix manually set up power/hr zones."

View File

@ -6,6 +6,7 @@ import android.graphics.Canvas
import android.graphics.Color
import android.graphics.Paint
import android.graphics.RectF
import android.graphics.Typeface
import android.util.AttributeSet
import android.view.View
import androidx.annotation.ColorInt
@ -16,71 +17,135 @@ class CustomProgressBar @JvmOverloads constructor(
) : View(context, attrs) {
var progress: Double = 0.5
var location: PowerbarLocation = PowerbarLocation.BOTTOM
var label: String = ""
var showLabel: Boolean = true
@ColorInt var progressColor: Int = 0xFF2b86e6.toInt()
val fontSize = 40f
private val linePaint = Paint().apply {
isAntiAlias = true
strokeWidth = 1f
style = Paint.Style.FILL_AND_STROKE
color = progressColor
}
private val lineStrokePaint = Paint().apply {
isAntiAlias = true
strokeWidth = 4f
style = Paint.Style.STROKE
color = progressColor
}
private val blurPaint = Paint().apply {
isAntiAlias = true
strokeWidth = 4f
style = Paint.Style.STROKE
color = progressColor
maskFilter = BlurMaskFilter(3f, BlurMaskFilter.Blur.NORMAL)
}
private val blurPaintHighlight = Paint().apply {
isAntiAlias = true
strokeWidth = 8f
style = Paint.Style.FILL_AND_STROKE
color = ColorUtils.blendARGB(progressColor, 0xFFFFFF, 0.5f)
maskFilter = BlurMaskFilter(6f, BlurMaskFilter.Blur.NORMAL)
}
private val backgroundPaint = Paint().apply {
style = Paint.Style.FILL
color = Color.argb(1.0f, 0f, 0f, 0f)
strokeWidth = 2f
}
private val textBackgroundPaint = Paint().apply {
style = Paint.Style.FILL
color = Color.argb(0.8f, 0f, 0f, 0f)
strokeWidth = 2f
}
private val textPaint = Paint().apply {
color = Color.WHITE
strokeWidth = 3f
textSize = fontSize
typeface = Typeface.create(Typeface.MONOSPACE, Typeface.BOLD);
textAlign = Paint.Align.CENTER
}
override fun onDrawForeground(canvas: Canvas) {
super.onDrawForeground(canvas)
val linePaint = Paint().apply {
isAntiAlias = true
strokeWidth = 1f
style = Paint.Style.FILL_AND_STROKE
color = progressColor
}
val blurPaint = Paint().apply {
isAntiAlias = true
strokeWidth = 2f
style = Paint.Style.STROKE
color = progressColor
maskFilter = BlurMaskFilter(3f, BlurMaskFilter.Blur.NORMAL)
}
val blurPaintHighlight = Paint().apply {
isAntiAlias = true
strokeWidth = 4f
style = Paint.Style.FILL_AND_STROKE
color = ColorUtils.blendARGB(progressColor, 0xFFFFFF, 0.5f)
maskFilter = BlurMaskFilter(3f, BlurMaskFilter.Blur.NORMAL)
}
val background = Paint().apply {
style = Paint.Style.FILL_AND_STROKE
color = Color.argb(1.0f, 0f, 0f, 0f)
strokeWidth = 2f
}
linePaint.color = progressColor
lineStrokePaint.color = progressColor
blurPaint.color = progressColor
blurPaintHighlight.color = ColorUtils.blendARGB(progressColor, 0xFFFFFF, 0.5f)
when(location){
PowerbarLocation.TOP -> {
val rect = RectF(
1f,
1f,
15f,
((canvas.width.toDouble() - 1f) * progress.coerceIn(0.0, 1.0)).toFloat(),
canvas.height.toFloat() - 1f - 4f
15f + 20f
)
canvas.drawRoundRect(0f, 2f, canvas.width.toFloat(), canvas.height.toFloat() - 4f, 2f, 2f, background)
canvas.drawRoundRect(0f, 15f, canvas.width.toFloat(), 15f + 20f, 2f, 2f, backgroundPaint)
if (progress > 0.0) {
canvas.drawRoundRect(rect, 2f, 2f, blurPaint)
canvas.drawRoundRect(rect, 2f, 2f, linePaint)
canvas.drawRoundRect(rect.right-4, rect.top, rect.right+4, rect.bottom, 2f, 2f, blurPaintHighlight)
if (showLabel){
val textBounds = textPaint.measureText(label)
val xOffset = (textBounds + 20).coerceAtLeast(10f) / 2f
val yOffset = (fontSize - 15f) / 2
val x = (rect.right - xOffset).coerceIn(0f..canvas.width-xOffset*2f)
val y = rect.top - yOffset
val r = x + xOffset * 2
val b = rect.bottom + yOffset
canvas.drawRoundRect(x, y, r, b, 2f, 2f, textBackgroundPaint)
canvas.drawRoundRect(x, y, r, b, 2f, 2f, blurPaint)
canvas.drawRoundRect(x, y, r, b, 2f, 2f, lineStrokePaint)
canvas.drawText(label, x + xOffset, rect.top + 23, textPaint)
}
}
}
PowerbarLocation.BOTTOM -> {
val rect = RectF(
1f,
1f + 4f,
canvas.height.toFloat() - 1f - 20f,
((canvas.width.toDouble() - 1f) * progress.coerceIn(0.0, 1.0)).toFloat(),
canvas.height.toFloat() - 1f
)
canvas.drawRoundRect(0f, 2f + 4f, canvas.width.toFloat(), canvas.height.toFloat(), 2f, 2f, background)
canvas.drawRoundRect(0f, canvas.height.toFloat() - 20f, canvas.width.toFloat(), canvas.height.toFloat(), 2f, 2f, backgroundPaint)
if (progress > 0.0) {
canvas.drawRoundRect(rect, 2f, 2f, blurPaint)
canvas.drawRoundRect(rect, 2f, 2f, linePaint)
canvas.drawRoundRect(rect.right-4, rect.top, rect.right+4, rect.bottom, 2f, 2f, blurPaintHighlight)
if (showLabel){
val textBounds = textPaint.measureText(label)
val xOffset = (textBounds + 20).coerceAtLeast(10f) / 2f
val yOffset = (fontSize + 0f) / 2
val x = (rect.right - xOffset).coerceIn(0f..canvas.width-xOffset*2f)
val y = (rect.top - yOffset)
val r = x + xOffset * 2
val b = rect.bottom + 5
canvas.drawRoundRect(x, y, r, b, 2f, 2f, textBackgroundPaint)
canvas.drawRoundRect(x, y, r, b, 2f, 2f, blurPaint)
canvas.drawRoundRect(x, y, r, b, 2f, 2f, lineStrokePaint)
canvas.drawText(label, x + xOffset, rect.top + 16, textPaint)
}
}
}
}

View File

@ -28,7 +28,8 @@ val settingsKey = stringPreferencesKey("settings")
data class PowerbarSettings(
val source: SelectedSource = SelectedSource.POWER,
val topBarSource: SelectedSource = SelectedSource.NONE,
val onlyShowWhileRiding: Boolean = true
val onlyShowWhileRiding: Boolean = true,
val showLabelOnBars: Boolean = true
){
companion object {
val defaultSettings = Json.encodeToString(PowerbarSettings())

View File

@ -53,7 +53,7 @@ class ForegroundService : Service() {
windows.clear()
if (settings.source != SelectedSource.NONE && showBars) {
Window(this@ForegroundService, PowerbarLocation.BOTTOM).apply {
Window(this@ForegroundService, PowerbarLocation.BOTTOM, settings.showLabelOnBars).apply {
selectedSource = settings.source
windows.add(this)
open()
@ -61,7 +61,7 @@ class ForegroundService : Service() {
}
if (settings.topBarSource != SelectedSource.NONE && showBars){
Window(this@ForegroundService, PowerbarLocation.TOP).apply {
Window(this@ForegroundService, PowerbarLocation.TOP, settings.showLabelOnBars).apply {
selectedSource = settings.topBarSource
open()
windows.add(this)

View File

@ -10,7 +10,7 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
class KarooPowerbarExtension : KarooExtension("karoo-powerbar", "1.1.1") {
class KarooPowerbarExtension : KarooExtension("karoo-powerbar", "1.2.0") {
companion object {
const val TAG = "karoo-powerbar"

View File

@ -39,7 +39,8 @@ enum class PowerbarLocation {
class Window(
private val context: Context,
val powerbarLocation: PowerbarLocation = PowerbarLocation.BOTTOM
val powerbarLocation: PowerbarLocation = PowerbarLocation.BOTTOM,
val showLabel: Boolean
) {
private val rootView: View
private var layoutParams: WindowManager.LayoutParams? = null
@ -83,7 +84,7 @@ class Window(
PowerbarLocation.BOTTOM -> Gravity.BOTTOM
}
if (powerbarLocation == PowerbarLocation.TOP) {
layoutParams?.y = 10
layoutParams?.y = 0
} else {
layoutParams?.y = 0
}
@ -105,6 +106,8 @@ class Window(
powerbar.progressColor = context.resources.getColor(R.color.zone7)
powerbar.progress = 0.0
powerbar.location = powerbarLocation
powerbar.showLabel = showLabel
powerbar.invalidate()
Log.i(TAG, "Streaming $selectedSource")
@ -142,7 +145,7 @@ class Window(
.collect { streamData ->
val value = streamData.value.roundToInt()
val color = context.getColor(
streamData.userProfile.getZone(streamData.userProfile.heartRateZones, value)?.colorResource
getZone(streamData.userProfile.heartRateZones, value)?.colorResource
?: R.color.zone7
)
val minHr = streamData.userProfile.restingHr
@ -152,6 +155,7 @@ class Window(
powerbar.progressColor = color
powerbar.progress = progress
powerbar.label = "$value"
powerbar.invalidate()
Log.d(TAG, "Hr: $value min: $minHr max: $maxHr")
@ -177,7 +181,7 @@ class Window(
.collect { streamData ->
val value = streamData.value.roundToInt()
val color = context.getColor(
streamData.userProfile.getZone(streamData.userProfile.powerZones, value)?.colorResource
getZone(streamData.userProfile.powerZones, value)?.colorResource
?: R.color.zone7
)
val minPower = streamData.userProfile.powerZones.first().min
@ -187,9 +191,10 @@ class Window(
powerbar.progressColor = color
powerbar.progress = progress
powerbar.label = "${value}W"
powerbar.invalidate()
Log.d(TAG, "Power: ${value} min: $minPower max: $maxPower")
Log.d(TAG, "Power: $value min: $minPower max: $maxPower")
}
}

View File

@ -26,7 +26,7 @@ val zones = mapOf(
9 to listOf(Zone.Zone0, Zone.Zone1, Zone.Zone2, Zone.Zone3, Zone.Zone4, Zone.Zone5, Zone.Zone6, Zone.Zone7, Zone.Zone8)
)
fun UserProfile.getZone(userZones: List<UserProfile.Zone>, value: Int): Zone? {
fun getZone(userZones: List<UserProfile.Zone>, value: Int): Zone? {
val zoneList = zones[userZones.size] ?: return null
userZones.forEachIndexed { index, zone ->

View File

@ -28,6 +28,7 @@ import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
@ -41,8 +42,10 @@ import androidx.core.content.ContextCompat.startActivity
import androidx.lifecycle.compose.LifecycleResumeEffect
import de.timklge.karoopowerbar.PowerbarSettings
import de.timklge.karoopowerbar.saveSettings
import de.timklge.karoopowerbar.streamRideState
import de.timklge.karoopowerbar.streamSettings
import io.hammerhead.karooext.KarooSystemService
import io.hammerhead.karooext.models.RideState
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
@ -62,6 +65,8 @@ fun MainScreen() {
val coroutineScope = rememberCoroutineScope()
val karooSystem = remember { KarooSystemService(ctx) }
val rideState: RideState by karooSystem.streamRideState().collectAsState(RideState.Idle)
var bottomSelectedSource by remember { mutableStateOf(SelectedSource.POWER) }
var topSelectedSource by remember { mutableStateOf(SelectedSource.NONE) }
@ -70,6 +75,7 @@ fun MainScreen() {
var givenPermissions by remember { mutableStateOf(false) }
var onlyShowWhileRiding by remember { mutableStateOf(false) }
var showLabelOnBars by remember { mutableStateOf(true) }
LaunchedEffect(Unit) {
givenPermissions = Settings.canDrawOverlays(ctx)
@ -78,6 +84,7 @@ fun MainScreen() {
bottomSelectedSource = settings.source
topSelectedSource = settings.topBarSource
onlyShowWhileRiding = settings.onlyShowWhileRiding
showLabelOnBars = settings.showLabelOnBars
}
}
@ -128,6 +135,12 @@ fun MainScreen() {
}
}
Row(verticalAlignment = Alignment.CenterVertically) {
Switch(checked = showLabelOnBars, onCheckedChange = { showLabelOnBars = it})
Spacer(modifier = Modifier.width(10.dp))
Text("Show value on bars")
}
Row(verticalAlignment = Alignment.CenterVertically) {
Switch(checked = onlyShowWhileRiding, onCheckedChange = { onlyShowWhileRiding = it})
Spacer(modifier = Modifier.width(10.dp))
@ -137,7 +150,10 @@ fun MainScreen() {
FilledTonalButton(modifier = Modifier
.fillMaxWidth()
.height(50.dp), onClick = {
val newSettings = PowerbarSettings(source = bottomSelectedSource, topBarSource = topSelectedSource, onlyShowWhileRiding = onlyShowWhileRiding)
val newSettings = PowerbarSettings(
source = bottomSelectedSource, topBarSource = topSelectedSource,
onlyShowWhileRiding = onlyShowWhileRiding, showLabelOnBars = showLabelOnBars
)
coroutineScope.launch {
saveSettings(ctx, newSettings)
@ -149,6 +165,16 @@ fun MainScreen() {
Text("Save")
}
if (onlyShowWhileRiding && karooConnected) {
val hardwareName = karooSystem.hardwareType?.name ?: "unknown device"
val state = when (rideState) {
RideState.Idle -> "No ride started"
is RideState.Paused -> "Ride is paused"
RideState.Recording -> "Currently riding"
}
Text(modifier = Modifier.padding(5.dp), text = "Running on $hardwareName. $state.")
}
if (showAlerts){
if(!karooConnected){
Text(modifier = Modifier.padding(5.dp), text = "Could not read device status. Is your Karoo updated?")

View File

@ -2,12 +2,12 @@
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="14dp">
android:layout_height="40dp">
<de.timklge.karoopowerbar.CustomProgressBar
android:id="@+id/progressBar"
android:layout_width="match_parent"
android:layout_height="14dp"
android:layout_height="40dp"
android:layout_gravity="center" />
</FrameLayout>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 35 KiB

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 42 KiB

After

Width:  |  Height:  |  Size: 47 KiB

BIN
powerbar2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.0 MiB

After

Width:  |  Height:  |  Size: 173 KiB