From 97fadc742e52c1b3e9a7958a064e711301b22df9 Mon Sep 17 00:00:00 2001 From: timklge <2026103+timklge@users.noreply.github.com> Date: Sat, 9 Aug 2025 18:20:31 +0200 Subject: [PATCH] Add power balance source with center outward drawing mode (#52) * Add power balance source with center outward drawing mode * Add apk archive step --- .github/workflows/android.yml | 6 + .../karoopowerbar/CustomProgressBar.kt | 145 ++++++++++++++++-- .../kotlin/de/timklge/karoopowerbar/Window.kt | 51 ++++++ .../karoopowerbar/screens/MainScreen.kt | 1 + 4 files changed, 192 insertions(+), 11 deletions(-) diff --git a/.github/workflows/android.yml b/.github/workflows/android.yml index 3fc3c2c..0f4df54 100644 --- a/.github/workflows/android.yml +++ b/.github/workflows/android.yml @@ -45,6 +45,12 @@ jobs: - name: Build with Gradle run: ./gradlew build + - name: Archive APK + uses: actions/upload-artifact@v4 + with: + name: app-release.apk + path: app/build/outputs/apk/release/app-release.apk + - name: Create Release id: create_release uses: softprops/action-gh-release@v2 diff --git a/app/src/main/kotlin/de/timklge/karoopowerbar/CustomProgressBar.kt b/app/src/main/kotlin/de/timklge/karoopowerbar/CustomProgressBar.kt index 7cb750d..6359074 100644 --- a/app/src/main/kotlin/de/timklge/karoopowerbar/CustomProgressBar.kt +++ b/app/src/main/kotlin/de/timklge/karoopowerbar/CustomProgressBar.kt @@ -42,6 +42,7 @@ class CustomProgressBar(private val view: CustomView, var showLabel: Boolean = true var barBackground: Boolean = false @ColorInt var progressColor: Int = 0xFF2b86e6.toInt() + var drawMode: ProgressBarDrawMode = ProgressBarDrawMode.STANDARD var fontSize = CustomProgressBarFontSize.MEDIUM set(value) { @@ -152,15 +153,63 @@ class CustomProgressBar(private val view: CustomView, val fullWidth = canvas.width.toFloat() val halfWidth = fullWidth / 2f - val barLeft = when (horizontalLocation) { - HorizontalPowerbarLocation.LEFT -> 0f - HorizontalPowerbarLocation.RIGHT -> fullWidth - (halfWidth * p).toFloat() - HorizontalPowerbarLocation.FULL -> 0f - } - val barRight = when (horizontalLocation) { - HorizontalPowerbarLocation.LEFT -> (halfWidth * p).toFloat() - HorizontalPowerbarLocation.RIGHT -> fullWidth - HorizontalPowerbarLocation.FULL -> (fullWidth * p).toFloat() + // Calculate bar left and right positions based on draw mode + val (barLeft, barRight) = when (drawMode) { + ProgressBarDrawMode.STANDARD -> { + // Standard left-to-right progress bar + when (horizontalLocation) { + HorizontalPowerbarLocation.LEFT -> Pair(0f, (halfWidth * p).toFloat()) + HorizontalPowerbarLocation.RIGHT -> Pair(fullWidth - (halfWidth * p).toFloat(), fullWidth) + HorizontalPowerbarLocation.FULL -> Pair(0f, (fullWidth * p).toFloat()) + } + } + ProgressBarDrawMode.CENTER_OUT -> { + // Center-outward progress bar: 0.5 = invisible, <0.5 = extend left, >0.5 = extend right + when (horizontalLocation) { + HorizontalPowerbarLocation.LEFT -> { + val centerPoint = halfWidth / 2f // Center of left half + when { + p == 0.5 -> Pair(centerPoint, centerPoint) // Invisible at 0.5 + p < 0.5 -> { + val leftExtent = centerPoint * (0.5 - p) * 2.0 // Map 0.0-0.5 to full left extension + Pair((centerPoint - leftExtent).toFloat(), centerPoint) + } + else -> { + val rightExtent = centerPoint * (p - 0.5) * 2.0 // Map 0.5-1.0 to full right extension + Pair(centerPoint, (centerPoint + rightExtent).toFloat()) + } + } + } + HorizontalPowerbarLocation.RIGHT -> { + val centerPoint = halfWidth + (halfWidth / 2f) // Center of right half + when { + p == 0.5 -> Pair(centerPoint, centerPoint) // Invisible at 0.5 + p < 0.5 -> { + val leftExtent = (halfWidth / 2f) * (0.5 - p) * 2.0 // Map 0.0-0.5 to full left extension + Pair((centerPoint - leftExtent).toFloat(), centerPoint) + } + else -> { + val rightExtent = (halfWidth / 2f) * (p - 0.5) * 2.0 // Map 0.5-1.0 to full right extension + Pair(centerPoint, (centerPoint + rightExtent).toFloat()) + } + } + } + HorizontalPowerbarLocation.FULL -> { + val centerPoint = halfWidth // Center of full width + when { + p == 0.5 -> Pair(centerPoint, centerPoint) // Invisible at 0.5 + p < 0.5 -> { + val leftExtent = halfWidth * (0.5 - p) * 2.0 // Map 0.0-0.5 to full left extension + Pair((centerPoint - leftExtent).toFloat(), centerPoint) + } + else -> { + val rightExtent = halfWidth * (p - 0.5) * 2.0 // Map 0.5-1.0 to full right extension + Pair(centerPoint, (centerPoint + rightExtent).toFloat()) + } + } + } + } + } } val minTargetX = when (horizontalLocation) { @@ -256,7 +305,44 @@ class CustomProgressBar(private val view: CustomView, val textBounds = textPaint.measureText(label) val xOffset = (textBounds + 20).coerceAtLeast(10f) / 2f - val x = (if (horizontalLocation != HorizontalPowerbarLocation.RIGHT) rect.right - xOffset else rect.left - xOffset).coerceIn(backgroundLeft..backgroundRight-xOffset*2f) + + // Calculate label position based on draw mode + val x = when (drawMode) { + ProgressBarDrawMode.STANDARD -> { + // Original logic for standard mode + (if (horizontalLocation != HorizontalPowerbarLocation.RIGHT) rect.right - xOffset else rect.left - xOffset).coerceIn(backgroundLeft..backgroundRight-xOffset*2f) + } + ProgressBarDrawMode.CENTER_OUT -> { + // For center outward mode, position label at the edge of the bar + when { + p == 0.5 -> { + // When bar is invisible (at center), position at center + when (horizontalLocation) { + HorizontalPowerbarLocation.LEFT -> { + val centerPoint = halfWidth / 2f + (centerPoint - xOffset).coerceIn(backgroundLeft..backgroundRight-xOffset*2f) + } + HorizontalPowerbarLocation.RIGHT -> { + val centerPoint = halfWidth + (halfWidth / 2f) + (centerPoint - xOffset).coerceIn(backgroundLeft..backgroundRight-xOffset*2f) + } + HorizontalPowerbarLocation.FULL -> { + val centerPoint = halfWidth + (centerPoint - xOffset).coerceIn(backgroundLeft..backgroundRight-xOffset*2f) + } + } + } + p < 0.5 -> { + // Bar extends left from center, place label at left edge + (rect.left - xOffset).coerceIn(backgroundLeft..backgroundRight-xOffset*2f) + } + else -> { + // Bar extends right from center, place label at right edge + (rect.right - xOffset).coerceIn(backgroundLeft..backgroundRight-xOffset*2f) + } + } + } + } val r = x + xOffset * 2 val fm = textPaint.fontMetrics @@ -344,7 +430,44 @@ class CustomProgressBar(private val view: CustomView, val textBounds = textPaint.measureText(label) val xOffset = (textBounds + 20).coerceAtLeast(10f) / 2f - val x = (if (horizontalLocation != HorizontalPowerbarLocation.RIGHT) rect.right - xOffset else rect.left - xOffset).coerceIn(backgroundLeft..backgroundRight-xOffset*2f) + + // Calculate label position based on draw mode + val x = when (drawMode) { + ProgressBarDrawMode.STANDARD -> { + // Original logic for standard mode + (if (horizontalLocation != HorizontalPowerbarLocation.RIGHT) rect.right - xOffset else rect.left - xOffset).coerceIn(backgroundLeft..backgroundRight-xOffset*2f) + } + ProgressBarDrawMode.CENTER_OUT -> { + // For center outward mode, position label at the edge of the bar + when { + p == 0.5 -> { + // When bar is invisible (at center), position at center + when (horizontalLocation) { + HorizontalPowerbarLocation.LEFT -> { + val centerPoint = halfWidth / 2f + (centerPoint - xOffset).coerceIn(backgroundLeft..backgroundRight-xOffset*2f) + } + HorizontalPowerbarLocation.RIGHT -> { + val centerPoint = halfWidth + (halfWidth / 2f) + (centerPoint - xOffset).coerceIn(backgroundLeft..backgroundRight-xOffset*2f) + } + HorizontalPowerbarLocation.FULL -> { + val centerPoint = halfWidth + (centerPoint - xOffset).coerceIn(backgroundLeft..backgroundRight-xOffset*2f) + } + } + } + p < 0.5 -> { + // Bar extends left from center, place label at left edge + (rect.left - xOffset).coerceIn(backgroundLeft..backgroundRight-xOffset*2f) + } + else -> { + // Bar extends right from center, place label at right edge + (rect.right - xOffset).coerceIn(backgroundLeft..backgroundRight-xOffset*2f) + } + } + } + } val r = x + xOffset * 2 // textDrawBaselineY calculation uses rect.top and barSize.barHeight. diff --git a/app/src/main/kotlin/de/timklge/karoopowerbar/Window.kt b/app/src/main/kotlin/de/timklge/karoopowerbar/Window.kt index 5027a9e..5df9e5d 100644 --- a/app/src/main/kotlin/de/timklge/karoopowerbar/Window.kt +++ b/app/src/main/kotlin/de/timklge/karoopowerbar/Window.kt @@ -55,6 +55,11 @@ enum class HorizontalPowerbarLocation { FULL, LEFT, RIGHT } +enum class ProgressBarDrawMode { + STANDARD, // Normal left-to-right progress + CENTER_OUT // Progress extends outward from center (0.5 = invisible, <0.5 = left, >0.5 = right) +} + class Window( private val context: Context, val powerbarLocation: PowerbarLocation = PowerbarLocation.BOTTOM, @@ -183,6 +188,7 @@ class Window( SelectedSource.ROUTE_PROGRESS -> streamRouteProgress(SelectedSource.ROUTE_PROGRESS, ::getRouteProgress) SelectedSource.REMAINING_ROUTE -> streamRouteProgress(SelectedSource.REMAINING_ROUTE, ::getRemainingRouteProgress) SelectedSource.GRADE -> streamGrade() + SelectedSource.POWER_BALANCE -> streamBalance() SelectedSource.NONE -> {} } }) @@ -199,6 +205,51 @@ class Window( } } + private suspend fun streamBalance() { + data class StreamData(val powerBalanceLeft: Double?, val power: Double?) + + karooSystem.streamDataFlow(DataType.Type.PEDAL_POWER_BALANCE) + .map { + val values = (it as? StreamState.Streaming)?.dataPoint?.values + + StreamData(values?.get(DataType.Field.PEDAL_POWER_BALANCE_LEFT), values?.get(DataType.Field.POWER)) + } + .distinctUntilChanged() + .throttle(1_000).collect { streamData -> + val powerBalanceLeft = streamData.powerBalanceLeft + val powerbarsWithBalanceSource = powerbars.values.filter { it.source == SelectedSource.POWER_BALANCE } + + powerbarsWithBalanceSource.forEach { powerbar -> + powerbar.drawMode = ProgressBarDrawMode.CENTER_OUT + + if (streamData.powerBalanceLeft != null) { + val value = remap(1.0 - (powerBalanceLeft ?: 0.5).coerceIn(0.0, 1.0), 0.4, 0.6, 0.0, 1.0) + + val percentLeft = ((powerBalanceLeft ?: 0.5) * 100).roundToInt() + val percentDiffTo50 = (percentLeft - 50).absoluteValue + + @ColorRes val zoneColorRes = Zone.entries[percentDiffTo50.toInt().coerceIn(0, Zone.entries.size-1)].colorResource + + powerbar.progressColor = context.getColor(zoneColorRes) + powerbar.progress = value + + val percentRight = 100 - percentLeft + + powerbar.label = "${percentLeft}-${percentRight}" + + Log.d(TAG, "Balance: $powerBalanceLeft power: ${streamData.power}") + } else { + powerbar.progressColor = context.getColor(R.color.zone0) + powerbar.progress = null + powerbar.label = "?" + + Log.d(TAG, "Balance: Unavailable") + } + powerbar.invalidate() + } + } + } + data class BarProgress( val progress: Double?, val label: String?, 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 a5d11c5..1e70d8e 100644 --- a/app/src/main/kotlin/de/timklge/karoopowerbar/screens/MainScreen.kt +++ b/app/src/main/kotlin/de/timklge/karoopowerbar/screens/MainScreen.kt @@ -88,6 +88,7 @@ enum class SelectedSource(val id: String, val label: String) { CADENCE("cadence", "Cadence"), CADENCE_3S("cadence_3s", "Cadence (3 sec avg)"), GRADE("grade", "Grade"), + POWER_BALANCE("power_balance", "Power Balance"), ROUTE_PROGRESS("route_progress", "Route Progress"), REMAINING_ROUTE("route_progress_remaining", "Route Remaining");