diff --git a/README.md b/README.md index 5b84d51..6bcccdf 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 289cf01..52f2b6b 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -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 { diff --git a/app/manifest.json b/app/manifest.json index 34fc2bd..9cc56d8 100644 --- a/app/manifest.json +++ b/app/manifest.json @@ -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." diff --git a/app/src/main/kotlin/de/timklge/karoopowerbar/CustomProgressBar.kt b/app/src/main/kotlin/de/timklge/karoopowerbar/CustomProgressBar.kt index 2d63c40..b4f5a7b 100644 --- a/app/src/main/kotlin/de/timklge/karoopowerbar/CustomProgressBar.kt +++ b/app/src/main/kotlin/de/timklge/karoopowerbar/CustomProgressBar.kt @@ -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) + } } } } diff --git a/app/src/main/kotlin/de/timklge/karoopowerbar/Extensions.kt b/app/src/main/kotlin/de/timklge/karoopowerbar/Extensions.kt index 53d68f0..26a9a3e 100644 --- a/app/src/main/kotlin/de/timklge/karoopowerbar/Extensions.kt +++ b/app/src/main/kotlin/de/timklge/karoopowerbar/Extensions.kt @@ -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()) diff --git a/app/src/main/kotlin/de/timklge/karoopowerbar/ForegroundService.kt b/app/src/main/kotlin/de/timklge/karoopowerbar/ForegroundService.kt index 629effa..674bd91 100644 --- a/app/src/main/kotlin/de/timklge/karoopowerbar/ForegroundService.kt +++ b/app/src/main/kotlin/de/timklge/karoopowerbar/ForegroundService.kt @@ -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) diff --git a/app/src/main/kotlin/de/timklge/karoopowerbar/KarooPowerbarExtension.kt b/app/src/main/kotlin/de/timklge/karoopowerbar/KarooPowerbarExtension.kt index ee5f8f6..ac8c3c3 100644 --- a/app/src/main/kotlin/de/timklge/karoopowerbar/KarooPowerbarExtension.kt +++ b/app/src/main/kotlin/de/timklge/karoopowerbar/KarooPowerbarExtension.kt @@ -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" diff --git a/app/src/main/kotlin/de/timklge/karoopowerbar/Window.kt b/app/src/main/kotlin/de/timklge/karoopowerbar/Window.kt index d9114f3..39354c3 100644 --- a/app/src/main/kotlin/de/timklge/karoopowerbar/Window.kt +++ b/app/src/main/kotlin/de/timklge/karoopowerbar/Window.kt @@ -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") } } diff --git a/app/src/main/kotlin/de/timklge/karoopowerbar/Zones.kt b/app/src/main/kotlin/de/timklge/karoopowerbar/Zones.kt index e14bf64..efdfa4e 100644 --- a/app/src/main/kotlin/de/timklge/karoopowerbar/Zones.kt +++ b/app/src/main/kotlin/de/timklge/karoopowerbar/Zones.kt @@ -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, value: Int): Zone? { +fun getZone(userZones: List, value: Int): Zone? { val zoneList = zones[userZones.size] ?: return null userZones.forEachIndexed { index, zone -> diff --git a/app/src/main/kotlin/de/timklge/karoopowerbar/screens/MainScreen.kt b/app/src/main/kotlin/de/timklge/karoopowerbar/screens/MainScreen.kt index e746ab9..d4e1b28 100644 --- a/app/src/main/kotlin/de/timklge/karoopowerbar/screens/MainScreen.kt +++ b/app/src/main/kotlin/de/timklge/karoopowerbar/screens/MainScreen.kt @@ -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?") diff --git a/app/src/main/res/layout/popup_window.xml b/app/src/main/res/layout/popup_window.xml index e533e4f..6acc92f 100644 --- a/app/src/main/res/layout/popup_window.xml +++ b/app/src/main/res/layout/popup_window.xml @@ -2,12 +2,12 @@ + android:layout_height="40dp"> diff --git a/powerbar0.png b/powerbar0.png index b5a62f9..3ec0ce3 100644 Binary files a/powerbar0.png and b/powerbar0.png differ diff --git a/powerbar1.png b/powerbar1.png index c488742..427a624 100644 Binary files a/powerbar1.png and b/powerbar1.png differ diff --git a/powerbar2.png b/powerbar2.png new file mode 100644 index 0000000..8b9d853 Binary files /dev/null and b/powerbar2.png differ diff --git a/powerbar_min.gif b/powerbar_min.gif index 5a890f6..d75e2dc 100644 Binary files a/powerbar_min.gif and b/powerbar_min.gif differ