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"
minSdk = 26
targetSdk = 33
versionCode = 8
versionName = "1.2.3"
versionCode = 9
versionName = "1.3"
}
signingConfigs {

View File

@ -3,9 +3,9 @@
"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.2.3",
"latestVersionCode": 8,
"latestVersion": "1.3",
"latestVersionCode": 9,
"developer": "timklge",
"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 androidx.annotation.ColorInt
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(
context: Context, attrs: AttributeSet? = null
@ -21,8 +29,11 @@ class CustomProgressBar @JvmOverloads constructor(
var showLabel: Boolean = true
@ColorInt var progressColor: Int = 0xFF2b86e6.toInt()
val fontSize = 40f
val barHeight = 14f
var size = CustomProgressBarSize.MEDIUM
set(value) {
field = value
textPaint.textSize = value.fontSize
}
private val linePaint = Paint().apply {
isAntiAlias = true
@ -69,7 +80,7 @@ class CustomProgressBar @JvmOverloads constructor(
private val textPaint = Paint().apply {
color = Color.WHITE
strokeWidth = 3f
textSize = fontSize
textSize = size.fontSize
typeface = Typeface.create(Typeface.MONOSPACE, Typeface.BOLD);
textAlign = Paint.Align.CENTER
}
@ -88,10 +99,10 @@ class CustomProgressBar @JvmOverloads constructor(
1f,
15f,
((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) {
canvas.drawRoundRect(rect, 2f, 2f, blurPaint)
@ -102,7 +113,10 @@ class CustomProgressBar @JvmOverloads constructor(
if (showLabel){
val textBounds = textPaint.measureText(label)
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 y = rect.top - yOffset
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, lineStrokePaint)
canvas.drawText(label, x + xOffset, rect.top + barHeight + 6, textPaint)
canvas.drawText(label, x + xOffset, rect.top + size.barHeight + 6, textPaint)
}
}
}
PowerbarLocation.BOTTOM -> {
val rect = RectF(
1f,
canvas.height.toFloat() - 1f - barHeight,
canvas.height.toFloat() - 1f - size.barHeight,
((canvas.width.toDouble() - 1f) * progress.coerceIn(0.0, 1.0)).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) {
canvas.drawRoundRect(rect, 2f, 2f, blurPaint)
@ -135,7 +149,11 @@ class CustomProgressBar @JvmOverloads constructor(
if (showLabel){
val textBounds = textPaint.measureText(label)
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 y = (rect.top - yOffset)
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, 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 showLabelOnBars: Boolean = true,
val useZoneColors: Boolean = true,
val barSize: CustomProgressBarSize = CustomProgressBarSize.MEDIUM
){
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, settings.showLabelOnBars).apply {
Window(this@ForegroundService, PowerbarLocation.BOTTOM, settings.showLabelOnBars, settings.barSize).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, settings.showLabelOnBars).apply {
Window(this@ForegroundService, PowerbarLocation.TOP, settings.showLabelOnBars, settings.barSize).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.2.3") {
class KarooPowerbarExtension : KarooExtension("karoo-powerbar", "1.3") {
companion object {
const val TAG = "karoo-powerbar"

View File

@ -44,7 +44,8 @@ enum class PowerbarLocation {
class Window(
private val context: Context,
val powerbarLocation: PowerbarLocation = PowerbarLocation.BOTTOM,
val showLabel: Boolean
val showLabel: Boolean,
val powerbarSize: CustomProgressBarSize
) {
private val rootView: View
private var layoutParams: WindowManager.LayoutParams? = null
@ -120,6 +121,7 @@ class Window(
powerbar.progress = 0.0
powerbar.location = powerbarLocation
powerbar.showLabel = showLabel
powerbar.size = powerbarSize
powerbar.invalidate()
Log.i(TAG, "Streaming $selectedSource")
@ -129,6 +131,10 @@ class Window(
SelectedSource.POWER_3S -> streamPower(PowerStreamSmoothing.SMOOTHED_3S)
SelectedSource.POWER_10S -> streamPower(PowerStreamSmoothing.SMOOTHED_10S)
SelectedSource.HEART_RATE -> streamHeartrate()
SelectedSource.SPEED -> streamSpeed(false)
SelectedSource.SPEED_3S -> streamSpeed(true)
SelectedSource.CADENCE -> streamCadence(false)
SelectedSource.CADENCE_3S -> streamCadence(true)
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() {
val hrFlow = karooSystem.streamDataFlow(DataType.Type.HEART_RATE)
.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.core.content.ContextCompat.startActivity
import androidx.lifecycle.compose.LifecycleResumeEffect
import de.timklge.karoopowerbar.CustomProgressBarSize
import de.timklge.karoopowerbar.PowerbarSettings
import de.timklge.karoopowerbar.saveSettings
import de.timklge.karoopowerbar.streamSettings
@ -52,6 +53,10 @@ enum class SelectedSource(val id: String, val label: String) {
POWER("power", "Power"),
POWER_3S("power_3s", "Power (3 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)
@ -72,6 +77,7 @@ fun MainScreen() {
var onlyShowWhileRiding by remember { mutableStateOf(false) }
var colorBasedOnZones by remember { mutableStateOf(false) }
var showLabelOnBars by remember { mutableStateOf(true) }
var barSize by remember { mutableStateOf(CustomProgressBarSize.MEDIUM) }
LaunchedEffect(Unit) {
givenPermissions = Settings.canDrawOverlays(ctx)
@ -82,6 +88,7 @@ fun MainScreen() {
onlyShowWhileRiding = settings.onlyShowWhileRiding
showLabelOnBars = settings.showLabelOnBars
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) {
Switch(checked = colorBasedOnZones, onCheckedChange = { colorBasedOnZones = it})
Spacer(modifier = Modifier.width(10.dp))
@ -156,7 +173,8 @@ fun MainScreen() {
val newSettings = PowerbarSettings(
source = bottomSelectedSource, topBarSource = topSelectedSource,
onlyShowWhileRiding = onlyShowWhileRiding, showLabelOnBars = showLabelOnBars,
useZoneColors = colorBasedOnZones
useZoneColors = colorBasedOnZones,
barSize = barSize
)
coroutineScope.launch {