Display value on top of bar
@ -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
|
||||
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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."
|
||||
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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())
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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"
|
||||
|
||||
@ -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")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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 ->
|
||||
|
||||
@ -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?")
|
||||
|
||||
@ -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>
|
||||
|
||||
BIN
powerbar0.png
|
Before Width: | Height: | Size: 35 KiB After Width: | Height: | Size: 33 KiB |
BIN
powerbar1.png
|
Before Width: | Height: | Size: 42 KiB After Width: | Height: | Size: 47 KiB |
BIN
powerbar2.png
Normal file
|
After Width: | Height: | Size: 39 KiB |
BIN
powerbar_min.gif
|
Before Width: | Height: | Size: 3.0 MiB After Width: | Height: | Size: 173 KiB |