* Add size setting (#12) and speed, cadence data sources (#2) * Fix top offset in large bar mode
This commit is contained in:
parent
f5e0491df8
commit
b67a53c0ad
@ -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 {
|
||||
|
||||
@ -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"
|
||||
}
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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())
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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"
|
||||
|
||||
@ -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 }
|
||||
|
||||
@ -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 {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user