Add size setting (#12) and speed, cadence data sources (#2) (#13)

* Add size setting (#12) and speed, cadence data sources (#2)

* Fix top offset in large bar mode
This commit is contained in:
timklge 2025-01-01 14:00:05 +01:00 committed by GitHub
parent f5e0491df8
commit b67a53c0ad
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 172 additions and 21 deletions

View File

@ -15,8 +15,8 @@ android {
applicationId = "de.timklge.karoopowerbar" applicationId = "de.timklge.karoopowerbar"
minSdk = 26 minSdk = 26
targetSdk = 33 targetSdk = 33
versionCode = 8 versionCode = 9
versionName = "1.2.3" versionName = "1.3"
} }
signingConfigs { signingConfigs {

View File

@ -3,9 +3,9 @@
"packageName": "de.timklge.karoopowerbar", "packageName": "de.timklge.karoopowerbar",
"iconUrl": "https://github.com/timklge/karoo-powerbar/releases/latest/download/karoo-powerbar.png", "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", "latestApkUrl": "https://github.com/timklge/karoo-powerbar/releases/latest/download/app-release.apk",
"latestVersion": "1.2.3", "latestVersion": "1.3",
"latestVersionCode": 8, "latestVersionCode": 9,
"developer": "timklge", "developer": "timklge",
"description": "Adds a colored power bar to the bottom of the screen", "description": "Adds a colored power bar to the bottom of the screen",
"releaseNotes": "Add option to set bar to single color" "releaseNotes": "Add size setting, cadence and speed data sources"
} }

View File

@ -11,6 +11,14 @@ import android.util.AttributeSet
import android.view.View import android.view.View
import androidx.annotation.ColorInt import androidx.annotation.ColorInt
import androidx.core.graphics.ColorUtils import androidx.core.graphics.ColorUtils
import kotlinx.serialization.Serializable
@Serializable
enum class CustomProgressBarSize(val id: String, val label: String, val fontSize: Float, val barHeight: Float) {
SMALL("small", "Small", 35f, 10f),
MEDIUM("medium", "Medium", 40f, 15f),
LARGE("large", "Large", 60f, 25f),
}
class CustomProgressBar @JvmOverloads constructor( class CustomProgressBar @JvmOverloads constructor(
context: Context, attrs: AttributeSet? = null context: Context, attrs: AttributeSet? = null
@ -21,8 +29,11 @@ class CustomProgressBar @JvmOverloads constructor(
var showLabel: Boolean = true var showLabel: Boolean = true
@ColorInt var progressColor: Int = 0xFF2b86e6.toInt() @ColorInt var progressColor: Int = 0xFF2b86e6.toInt()
val fontSize = 40f var size = CustomProgressBarSize.MEDIUM
val barHeight = 14f set(value) {
field = value
textPaint.textSize = value.fontSize
}
private val linePaint = Paint().apply { private val linePaint = Paint().apply {
isAntiAlias = true isAntiAlias = true
@ -69,7 +80,7 @@ class CustomProgressBar @JvmOverloads constructor(
private val textPaint = Paint().apply { private val textPaint = Paint().apply {
color = Color.WHITE color = Color.WHITE
strokeWidth = 3f strokeWidth = 3f
textSize = fontSize textSize = size.fontSize
typeface = Typeface.create(Typeface.MONOSPACE, Typeface.BOLD); typeface = Typeface.create(Typeface.MONOSPACE, Typeface.BOLD);
textAlign = Paint.Align.CENTER textAlign = Paint.Align.CENTER
} }
@ -88,10 +99,10 @@ class CustomProgressBar @JvmOverloads constructor(
1f, 1f,
15f, 15f,
((canvas.width.toDouble() - 1f) * progress.coerceIn(0.0, 1.0)).toFloat(), ((canvas.width.toDouble() - 1f) * progress.coerceIn(0.0, 1.0)).toFloat(),
15f + barHeight 15f + size.barHeight
) )
canvas.drawRect(0f, 15f, canvas.width.toFloat(), 15f + barHeight, backgroundPaint) canvas.drawRect(0f, 15f, canvas.width.toFloat(), 15f + size.barHeight, backgroundPaint)
if (progress > 0.0) { if (progress > 0.0) {
canvas.drawRoundRect(rect, 2f, 2f, blurPaint) canvas.drawRoundRect(rect, 2f, 2f, blurPaint)
@ -102,7 +113,10 @@ class CustomProgressBar @JvmOverloads constructor(
if (showLabel){ if (showLabel){
val textBounds = textPaint.measureText(label) val textBounds = textPaint.measureText(label)
val xOffset = (textBounds + 20).coerceAtLeast(10f) / 2f val xOffset = (textBounds + 20).coerceAtLeast(10f) / 2f
val yOffset = (fontSize - 15f) / 2 val yOffset = when(size){
CustomProgressBarSize.SMALL -> (size.fontSize - size.barHeight) / 2 + 2f
CustomProgressBarSize.MEDIUM, CustomProgressBarSize.LARGE -> (size.fontSize - size.barHeight) / 2
}
val x = (rect.right - xOffset).coerceIn(0f..canvas.width-xOffset*2f) val x = (rect.right - xOffset).coerceIn(0f..canvas.width-xOffset*2f)
val y = rect.top - yOffset val y = rect.top - yOffset
val r = x + xOffset * 2 val r = x + xOffset * 2
@ -112,19 +126,19 @@ class CustomProgressBar @JvmOverloads constructor(
canvas.drawRoundRect(x, y, r, b, 2f, 2f, blurPaint) canvas.drawRoundRect(x, y, r, b, 2f, 2f, blurPaint)
canvas.drawRoundRect(x, y, r, b, 2f, 2f, lineStrokePaint) canvas.drawRoundRect(x, y, r, b, 2f, 2f, lineStrokePaint)
canvas.drawText(label, x + xOffset, rect.top + barHeight + 6, textPaint) canvas.drawText(label, x + xOffset, rect.top + size.barHeight + 6, textPaint)
} }
} }
} }
PowerbarLocation.BOTTOM -> { PowerbarLocation.BOTTOM -> {
val rect = RectF( val rect = RectF(
1f, 1f,
canvas.height.toFloat() - 1f - barHeight, canvas.height.toFloat() - 1f - size.barHeight,
((canvas.width.toDouble() - 1f) * progress.coerceIn(0.0, 1.0)).toFloat(), ((canvas.width.toDouble() - 1f) * progress.coerceIn(0.0, 1.0)).toFloat(),
canvas.height.toFloat() canvas.height.toFloat()
) )
canvas.drawRect(0f, canvas.height.toFloat() - barHeight - 1f, canvas.width.toFloat(), canvas.height.toFloat(), backgroundPaint) canvas.drawRect(0f, canvas.height.toFloat() - size.barHeight, canvas.width.toFloat(), canvas.height.toFloat(), backgroundPaint)
if (progress > 0.0) { if (progress > 0.0) {
canvas.drawRoundRect(rect, 2f, 2f, blurPaint) canvas.drawRoundRect(rect, 2f, 2f, blurPaint)
@ -135,7 +149,11 @@ class CustomProgressBar @JvmOverloads constructor(
if (showLabel){ if (showLabel){
val textBounds = textPaint.measureText(label) val textBounds = textPaint.measureText(label)
val xOffset = (textBounds + 20).coerceAtLeast(10f) / 2f val xOffset = (textBounds + 20).coerceAtLeast(10f) / 2f
val yOffset = (fontSize + 0f) / 2 val yOffset = when(size){
CustomProgressBarSize.SMALL -> size.fontSize / 2 + 2f
CustomProgressBarSize.MEDIUM -> size.fontSize / 2
CustomProgressBarSize.LARGE -> size.fontSize / 2 - 5f
}
val x = (rect.right - xOffset).coerceIn(0f..canvas.width-xOffset*2f) val x = (rect.right - xOffset).coerceIn(0f..canvas.width-xOffset*2f)
val y = (rect.top - yOffset) val y = (rect.top - yOffset)
val r = x + xOffset * 2 val r = x + xOffset * 2
@ -145,7 +163,7 @@ class CustomProgressBar @JvmOverloads constructor(
canvas.drawRoundRect(x, y, r, b, 2f, 2f, blurPaint) canvas.drawRoundRect(x, y, r, b, 2f, 2f, blurPaint)
canvas.drawRoundRect(x, y, r, b, 2f, 2f, lineStrokePaint) canvas.drawRoundRect(x, y, r, b, 2f, 2f, lineStrokePaint)
canvas.drawText(label, x + xOffset, rect.top + barHeight - 1, textPaint) canvas.drawText(label, x + xOffset, rect.top + size.barHeight - 1, textPaint)
} }
} }
} }

View File

@ -31,6 +31,7 @@ data class PowerbarSettings(
val onlyShowWhileRiding: Boolean = true, val onlyShowWhileRiding: Boolean = true,
val showLabelOnBars: Boolean = true, val showLabelOnBars: Boolean = true,
val useZoneColors: Boolean = true, val useZoneColors: Boolean = true,
val barSize: CustomProgressBarSize = CustomProgressBarSize.MEDIUM
){ ){
companion object { companion object {
val defaultSettings = Json.encodeToString(PowerbarSettings()) val defaultSettings = Json.encodeToString(PowerbarSettings())

View File

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

View File

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

View File

@ -44,7 +44,8 @@ enum class PowerbarLocation {
class Window( class Window(
private val context: Context, private val context: Context,
val powerbarLocation: PowerbarLocation = PowerbarLocation.BOTTOM, val powerbarLocation: PowerbarLocation = PowerbarLocation.BOTTOM,
val showLabel: Boolean val showLabel: Boolean,
val powerbarSize: CustomProgressBarSize
) { ) {
private val rootView: View private val rootView: View
private var layoutParams: WindowManager.LayoutParams? = null private var layoutParams: WindowManager.LayoutParams? = null
@ -120,6 +121,7 @@ class Window(
powerbar.progress = 0.0 powerbar.progress = 0.0
powerbar.location = powerbarLocation powerbar.location = powerbarLocation
powerbar.showLabel = showLabel powerbar.showLabel = showLabel
powerbar.size = powerbarSize
powerbar.invalidate() powerbar.invalidate()
Log.i(TAG, "Streaming $selectedSource") Log.i(TAG, "Streaming $selectedSource")
@ -129,6 +131,10 @@ class Window(
SelectedSource.POWER_3S -> streamPower(PowerStreamSmoothing.SMOOTHED_3S) SelectedSource.POWER_3S -> streamPower(PowerStreamSmoothing.SMOOTHED_3S)
SelectedSource.POWER_10S -> streamPower(PowerStreamSmoothing.SMOOTHED_10S) SelectedSource.POWER_10S -> streamPower(PowerStreamSmoothing.SMOOTHED_10S)
SelectedSource.HEART_RATE -> streamHeartrate() SelectedSource.HEART_RATE -> streamHeartrate()
SelectedSource.SPEED -> streamSpeed(false)
SelectedSource.SPEED_3S -> streamSpeed(true)
SelectedSource.CADENCE -> streamCadence(false)
SelectedSource.CADENCE_3S -> streamCadence(true)
else -> {} else -> {}
} }
} }
@ -144,6 +150,114 @@ class Window(
} }
} }
companion object {
val speedZones = listOf(
UserProfile.Zone(0, 9),
UserProfile.Zone(10, 19),
UserProfile.Zone(20, 24),
UserProfile.Zone(25, 29),
UserProfile.Zone(30, 34),
UserProfile.Zone(34, 39),
UserProfile.Zone(40, 44),
)
val cadenceZones = listOf(
UserProfile.Zone(0, 59),
UserProfile.Zone(60, 79),
UserProfile.Zone(80, 89),
UserProfile.Zone(90, 99),
UserProfile.Zone(100, 109),
UserProfile.Zone(110, 119),
UserProfile.Zone(120, 129),
)
}
private suspend fun streamSpeed(smoothed: Boolean) {
val speedFlow = karooSystem.streamDataFlow(if(smoothed) DataType.Type.SMOOTHED_3S_AVERAGE_SPEED else DataType.Type.SPEED)
.map { (it as? StreamState.Streaming)?.dataPoint?.singleValue }
.distinctUntilChanged()
val settingsFlow = context.streamSettings()
karooSystem.streamUserProfile()
.distinctUntilChanged()
.combine(speedFlow) { userProfile, speed -> StreamData(userProfile, speed) }
.combine(settingsFlow) { streamData, settings -> streamData.copy(settings = settings) }
.distinctUntilChanged()
.collect { streamData ->
val valueMetersPerSecond = streamData.value?.roundToInt()
val value = when (streamData.userProfile.preferredUnit.distance){
UserProfile.PreferredUnit.UnitType.IMPERIAL -> valueMetersPerSecond?.times(2.23694)
else -> valueMetersPerSecond?.times(3.6)
}?.roundToInt()
if (value != null) {
val minSpeed = speedZones.first().min
val maxSpeed = speedZones.last().min + 5
val progress =
remap(value.toDouble(), minSpeed.toDouble(), maxSpeed.toDouble(), 0.0, 1.0)
powerbar.progressColor = if (streamData.settings?.useZoneColors == true) {
context.getColor(getZone(speedZones, value)?.colorResource ?: R.color.zone7)
} else {
context.getColor(R.color.zone0)
}
powerbar.progress = progress
powerbar.label = "$value"
Log.d(TAG, "Speed: $value min: $minSpeed max: $maxSpeed")
} else {
powerbar.progressColor = context.getColor(R.color.zone0)
powerbar.progress = 0.0
powerbar.label = "?"
Log.d(TAG, "Speed: Unavailable")
}
powerbar.invalidate()
}
}
private suspend fun streamCadence(smoothed: Boolean) {
val speedFlow = karooSystem.streamDataFlow(if(smoothed) DataType.Type.SMOOTHED_3S_AVERAGE_CADENCE else DataType.Type.CADENCE)
.map { (it as? StreamState.Streaming)?.dataPoint?.singleValue }
.distinctUntilChanged()
val settingsFlow = context.streamSettings()
karooSystem.streamUserProfile()
.distinctUntilChanged()
.combine(speedFlow) { userProfile, speed -> StreamData(userProfile, speed) }
.combine(settingsFlow) { streamData, settings -> streamData.copy(settings = settings) }
.distinctUntilChanged()
.collect { streamData ->
val value = streamData.value?.roundToInt()
if (value != null) {
val minCadence = cadenceZones.first().min
val maxCadence = cadenceZones.last().min + 5
val progress =
remap(value.toDouble(), minCadence.toDouble(), maxCadence.toDouble(), 0.0, 1.0)
powerbar.progressColor = if (streamData.settings?.useZoneColors == true) {
context.getColor(getZone(cadenceZones, value)?.colorResource ?: R.color.zone7)
} else {
context.getColor(R.color.zone0)
}
powerbar.progress = progress
powerbar.label = "$value"
Log.d(TAG, "Cadence: $value min: $minCadence max: $maxCadence")
} else {
powerbar.progressColor = context.getColor(R.color.zone0)
powerbar.progress = 0.0
powerbar.label = "?"
Log.d(TAG, "Cadence: Unavailable")
}
powerbar.invalidate()
}
}
private suspend fun streamHeartrate() { private suspend fun streamHeartrate() {
val hrFlow = karooSystem.streamDataFlow(DataType.Type.HEART_RATE) val hrFlow = karooSystem.streamDataFlow(DataType.Type.HEART_RATE)
.map { (it as? StreamState.Streaming)?.dataPoint?.singleValue } .map { (it as? StreamState.Streaming)?.dataPoint?.singleValue }

View File

@ -39,6 +39,7 @@ import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.core.content.ContextCompat.startActivity import androidx.core.content.ContextCompat.startActivity
import androidx.lifecycle.compose.LifecycleResumeEffect import androidx.lifecycle.compose.LifecycleResumeEffect
import de.timklge.karoopowerbar.CustomProgressBarSize
import de.timklge.karoopowerbar.PowerbarSettings import de.timklge.karoopowerbar.PowerbarSettings
import de.timklge.karoopowerbar.saveSettings import de.timklge.karoopowerbar.saveSettings
import de.timklge.karoopowerbar.streamSettings import de.timklge.karoopowerbar.streamSettings
@ -52,6 +53,10 @@ enum class SelectedSource(val id: String, val label: String) {
POWER("power", "Power"), POWER("power", "Power"),
POWER_3S("power_3s", "Power (3 second avg)"), POWER_3S("power_3s", "Power (3 second avg)"),
POWER_10S("power_10s", "Power (10 second avg)"), POWER_10S("power_10s", "Power (10 second avg)"),
SPEED("speed", "Speed"),
SPEED_3S("speed_3s", "Speed (3 second avg"),
CADENCE("cadence", "Cadence"),
CADENCE_3S("cadence_3s", "Cadence (3 second avg)"),
} }
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@ -72,6 +77,7 @@ fun MainScreen() {
var onlyShowWhileRiding by remember { mutableStateOf(false) } var onlyShowWhileRiding by remember { mutableStateOf(false) }
var colorBasedOnZones by remember { mutableStateOf(false) } var colorBasedOnZones by remember { mutableStateOf(false) }
var showLabelOnBars by remember { mutableStateOf(true) } var showLabelOnBars by remember { mutableStateOf(true) }
var barSize by remember { mutableStateOf(CustomProgressBarSize.MEDIUM) }
LaunchedEffect(Unit) { LaunchedEffect(Unit) {
givenPermissions = Settings.canDrawOverlays(ctx) givenPermissions = Settings.canDrawOverlays(ctx)
@ -82,6 +88,7 @@ fun MainScreen() {
onlyShowWhileRiding = settings.onlyShowWhileRiding onlyShowWhileRiding = settings.onlyShowWhileRiding
showLabelOnBars = settings.showLabelOnBars showLabelOnBars = settings.showLabelOnBars
colorBasedOnZones = settings.useZoneColors colorBasedOnZones = settings.useZoneColors
barSize = settings.barSize
} }
} }
@ -132,6 +139,16 @@ fun MainScreen() {
} }
} }
apply {
val dropdownOptions = CustomProgressBarSize.entries.toList().map { unit -> DropdownOption(unit.id, unit.label) }
val dropdownInitialSelection by remember(barSize) {
mutableStateOf(dropdownOptions.find { option -> option.id == barSize.id }!!)
}
Dropdown(label = "Bar Size", options = dropdownOptions, selected = dropdownInitialSelection) { selectedOption ->
barSize = CustomProgressBarSize.entries.find { unit -> unit.id == selectedOption.id }!!
}
}
Row(verticalAlignment = Alignment.CenterVertically) { Row(verticalAlignment = Alignment.CenterVertically) {
Switch(checked = colorBasedOnZones, onCheckedChange = { colorBasedOnZones = it}) Switch(checked = colorBasedOnZones, onCheckedChange = { colorBasedOnZones = it})
Spacer(modifier = Modifier.width(10.dp)) Spacer(modifier = Modifier.width(10.dp))
@ -156,7 +173,8 @@ fun MainScreen() {
val newSettings = PowerbarSettings( val newSettings = PowerbarSettings(
source = bottomSelectedSource, topBarSource = topSelectedSource, source = bottomSelectedSource, topBarSource = topSelectedSource,
onlyShowWhileRiding = onlyShowWhileRiding, showLabelOnBars = showLabelOnBars, onlyShowWhileRiding = onlyShowWhileRiding, showLabelOnBars = showLabelOnBars,
useZoneColors = colorBasedOnZones useZoneColors = colorBasedOnZones,
barSize = barSize
) )
coroutineScope.launch { coroutineScope.launch {