Update target range drawing

This commit is contained in:
Tim Kluge 2025-05-27 22:29:42 +02:00
parent 72b9a0f57e
commit 3e1fa2169b
3 changed files with 269 additions and 71 deletions

View File

@ -36,13 +36,30 @@ class CustomProgressBar @JvmOverloads constructor(
set(value) { set(value) {
field = value field = value
textPaint.textSize = value.fontSize textPaint.textSize = value.fontSize
targetZoneStrokePaint.strokeWidth = when(value){
CustomProgressBarSize.SMALL -> 3f
CustomProgressBarSize.MEDIUM -> 6f
CustomProgressBarSize.LARGE -> 8f
}
targetIndicatorPaint.strokeWidth = when(value){
CustomProgressBarSize.SMALL -> 6f
CustomProgressBarSize.MEDIUM -> 8f
CustomProgressBarSize.LARGE -> 10f
}
} }
private val targetColor = 0xFF9933FF.toInt() private val targetColor = 0xFF9933FF.toInt()
private val targetZonePaint = Paint().apply { private val targetZoneFillPaint = Paint().apply {
isAntiAlias = true isAntiAlias = true
strokeWidth = 10f style = Paint.Style.FILL
color = targetColor
alpha = 100 // Semi-transparent fill
}
private val targetZoneStrokePaint = Paint().apply {
isAntiAlias = true
strokeWidth = 6f
style = Paint.Style.STROKE style = Paint.Style.STROKE
color = targetColor color = targetColor
} }
@ -97,130 +114,311 @@ class CustomProgressBar @JvmOverloads constructor(
textAlign = Paint.Align.CENTER textAlign = Paint.Align.CENTER
} }
private val targetIndicatorPaint = Paint().apply {
isAntiAlias = true
strokeWidth = 8f
style = Paint.Style.STROKE
}
override fun onDrawForeground(canvas: Canvas) { override fun onDrawForeground(canvas: Canvas) {
super.onDrawForeground(canvas) super.onDrawForeground(canvas)
// Determine if the current progress is within the target range // Determine if the current progress is within the target range
val isTargetMet = progress != null && minTarget != null && maxTarget != null && progress!! >= minTarget!! && progress!! <= maxTarget!! val isTargetMet =
progress != null && minTarget != null && maxTarget != null && progress!! >= minTarget!! && progress!! <= maxTarget!!
val color = if (isTargetMet) { linePaint.color = progressColor
targetColor // Use Zone 8 color if target is met lineStrokePaint.color = progressColor
} else { blurPaint.color = progressColor
progressColor // Use default progress color otherwise blurPaintHighlight.color = ColorUtils.blendARGB(progressColor, 0xFFFFFF, 0.5f)
}
linePaint.color = color when (location) {
lineStrokePaint.color = color
blurPaint.color = color
blurPaintHighlight.color = ColorUtils.blendARGB(color, 0xFFFFFF, 0.5f)
when(location){
PowerbarLocation.TOP -> { PowerbarLocation.TOP -> {
val barTop = 15f val barTop = 15f
val barBottom = barTop + size.barHeight val barBottom = barTop + size.barHeight
val rect = RectF( val rect = RectF(
1f, 1f,
barTop, barTop,
((canvas.width.toDouble() - 1f) * (progress ?: 0.0).coerceIn(0.0, 1.0)).toFloat(), ((canvas.width.toDouble() - 1f) * (progress ?: 0.0).coerceIn(
0.0,
1.0
)).toFloat(),
barBottom barBottom
) )
canvas.drawRect(0f, barTop, canvas.width.toFloat(), barBottom, backgroundPaint) canvas.drawRect(0f, barTop, canvas.width.toFloat(), barBottom, backgroundPaint)
// Draw target zone fill behind the progress bar
if (minTarget != null && maxTarget != null) {
val minTargetX = (canvas.width * minTarget!!).toFloat()
val maxTargetX = (canvas.width * maxTarget!!).toFloat()
canvas.drawRoundRect(
minTargetX,
barTop,
maxTargetX,
barBottom,
2f,
2f,
targetZoneFillPaint
)
}
if (progress != null) { if (progress != null) {
canvas.drawRoundRect(rect, 2f, 2f, blurPaint) canvas.drawRoundRect(rect, 2f, 2f, blurPaint)
canvas.drawRoundRect(rect, 2f, 2f, linePaint) canvas.drawRoundRect(rect, 2f, 2f, linePaint)
canvas.drawRoundRect(rect.right-4, rect.top, rect.right+4, rect.bottom, 2f, 2f, blurPaintHighlight) canvas.drawRoundRect(
rect.right - 4,
rect.top,
rect.right + 4,
rect.bottom,
2f,
2f,
blurPaintHighlight
)
// Draw target indicator lines first to be "behind" the label // Draw target zone stroke after progress bar, before label
if (minTarget != null && maxTarget != null) { if (minTarget != null && maxTarget != null) {
val minTargetX = (canvas.width * minTarget!!).toFloat() val minTargetX = (canvas.width * minTarget!!).toFloat()
val maxTargetX = (canvas.width * maxTarget!!).toFloat() val maxTargetX = (canvas.width * maxTarget!!).toFloat()
val edgeOffset = targetZonePaint.strokeWidth / 2f // Draw stroked rounded rectangle for the target zone
canvas.drawRoundRect(
// Horizontal line at the top edge of the canvas minTargetX,
canvas.drawLine(minTargetX, edgeOffset, maxTargetX, edgeOffset, targetZonePaint) barTop,
// Vertical lines spanning most of the canvas height maxTargetX,
canvas.drawLine(minTargetX, edgeOffset, minTargetX, edgeOffset + 40, targetZonePaint) barBottom,
canvas.drawLine(maxTargetX, edgeOffset, maxTargetX, edgeOffset + 40, targetZonePaint) 2f,
2f,
targetZoneStrokePaint
)
} }
if (showLabel){ // Draw vertical target indicator line if target is present
val textBounds = textPaint.measureText(label) if (target != null) {
val xOffset = (textBounds + 20).coerceAtLeast(10f) / 2f val targetX = (canvas.width * target!!).toFloat()
val yOffset = when(size){ targetIndicatorPaint.color = if (isTargetMet) Color.GREEN else Color.RED
canvas.drawLine(targetX, barTop, targetX, barBottom, targetIndicatorPaint)
}
if (showLabel) {
val textContent =
label // Store original label, as textPaint.measureText can be slow
val measuredTextWidth = textPaint.measureText(textContent)
val labelBoxWidth = (measuredTextWidth + 20).coerceAtLeast(10f)
val labelBoxHeight = size.fontSize + 10f // Consistent height with padding
// Calculate horizontal position for the label box (centered around progress end, clamped)
val labelBoxLeft = (rect.right - labelBoxWidth / 2f).coerceIn(
0f,
canvas.width - labelBoxWidth
)
var labelBoxTop: Float
var labelBoxBottom: Float
var textYPosition: Float
if (target != null) { // If workout target is present, move label BELOW the bar
val labelPadding = 5f // Padding between bar and label box
labelBoxTop = barBottom + labelPadding
labelBoxBottom = labelBoxTop + labelBoxHeight
// Vertically center text in the new box
val labelBoxCenterY = labelBoxTop + labelBoxHeight / 2f
textYPosition =
labelBoxCenterY - (textPaint.ascent() + textPaint.descent()) / 2f
} else { // Original position for TOP
val yOffsetOriginal = when (size) {
CustomProgressBarSize.SMALL -> (size.fontSize - size.barHeight) / 2 + 2f CustomProgressBarSize.SMALL -> (size.fontSize - size.barHeight) / 2 + 2f
CustomProgressBarSize.MEDIUM, CustomProgressBarSize.LARGE -> (size.fontSize - size.barHeight) / 2 CustomProgressBarSize.MEDIUM, CustomProgressBarSize.LARGE -> (size.fontSize - size.barHeight) / 2
} }
val x = (rect.right - xOffset).coerceIn(0f..canvas.width-xOffset*2f) labelBoxTop = barTop - yOffsetOriginal
val y = rect.top - yOffset labelBoxBottom =
val r = x + xOffset * 2 barBottom + yOffsetOriginal // Original calculation was based on rect.bottom which is barBottom
val b = rect.bottom + yOffset textYPosition = barBottom + 6f // Original text Y
}
// Use targetZonePaint for outline if target is met lineStrokePaint.color = if (target != null){
val currentOutlinePaint = if (isTargetMet) targetZonePaint else lineStrokePaint if (isTargetMet) Color.GREEN else Color.RED
} else progressColor
canvas.drawRoundRect(x, y, r, b, 2f, 2f, textBackgroundPaint) canvas.drawRoundRect(
canvas.drawRoundRect(x, y, r, b, 2f, 2f, blurPaint) labelBoxLeft,
canvas.drawRoundRect(x, y, r, b, 2f, 2f, currentOutlinePaint) labelBoxTop,
labelBoxLeft + labelBoxWidth,
labelBoxBottom,
2f,
2f,
textBackgroundPaint
)
canvas.drawRoundRect(
labelBoxLeft,
labelBoxTop,
labelBoxLeft + labelBoxWidth,
labelBoxBottom,
2f,
2f,
blurPaint
)
canvas.drawRoundRect(
labelBoxLeft,
labelBoxTop,
labelBoxLeft + labelBoxWidth,
labelBoxBottom,
2f,
2f,
lineStrokePaint
)
canvas.drawText(label, x + xOffset, rect.top + size.barHeight + 6, textPaint) canvas.drawText(
textContent,
labelBoxLeft + labelBoxWidth / 2f,
textYPosition,
textPaint
)
} }
} }
} }
PowerbarLocation.BOTTOM -> { PowerbarLocation.BOTTOM -> {
val barTop = canvas.height.toFloat() - 1f - size.barHeight val barTop = canvas.height.toFloat() - 1f - size.barHeight
val barBottom = canvas.height.toFloat() val barBottom = canvas.height.toFloat()
val rect = RectF( val rect = RectF(
1f, 1f,
barTop, barTop,
((canvas.width.toDouble() - 1f) * (progress ?: 0.0).coerceIn(0.0, 1.0)).toFloat(), ((canvas.width.toDouble() - 1f) * (progress ?: 0.0).coerceIn(
0.0,
1.0
)).toFloat(),
barBottom barBottom
) )
canvas.drawRect(0f, barTop, canvas.width.toFloat(), barBottom, backgroundPaint) canvas.drawRect(0f, barTop, canvas.width.toFloat(), barBottom, backgroundPaint)
// Draw target zone fill behind the progress bar
if (minTarget != null && maxTarget != null) {
val minTargetX = (canvas.width * minTarget!!).toFloat()
val maxTargetX = (canvas.width * maxTarget!!).toFloat()
canvas.drawRoundRect(
minTargetX,
barTop,
maxTargetX,
barBottom,
2f,
2f,
targetZoneFillPaint
)
}
if (progress != null) { if (progress != null) {
canvas.drawRoundRect(rect, 2f, 2f, blurPaint) canvas.drawRoundRect(rect, 2f, 2f, blurPaint)
canvas.drawRoundRect(rect, 2f, 2f, linePaint) canvas.drawRoundRect(rect, 2f, 2f, linePaint)
canvas.drawRoundRect(rect.right-4, rect.top, rect.right+4, rect.bottom, 2f, 2f, blurPaintHighlight) canvas.drawRoundRect(
rect.right - 4,
rect.top,
rect.right + 4,
rect.bottom,
2f,
2f,
blurPaintHighlight
)
// Draw target indicator lines first to be "behind" the label // Draw target zone stroke after progress bar, before label
if (minTarget != null && maxTarget != null) { if (minTarget != null && maxTarget != null) {
val minTargetX = (canvas.width * minTarget!!).toFloat() val minTargetX = (canvas.width * minTarget!!).toFloat()
val maxTargetX = (canvas.width * maxTarget!!).toFloat() val maxTargetX = (canvas.width * maxTarget!!).toFloat()
val edgeOffset = targetZonePaint.strokeWidth / 2f // Draw stroked rounded rectangle for the target zone
canvas.drawRoundRect(
// Horizontal line at the bottom edge of the canvas minTargetX,
canvas.drawLine(minTargetX, canvas.height - edgeOffset, maxTargetX, canvas.height - edgeOffset, targetZonePaint) barTop,
// Vertical lines spanning most of the canvas height maxTargetX,
canvas.drawLine(minTargetX, canvas.height - edgeOffset - 40, minTargetX, canvas.height - edgeOffset, targetZonePaint) barBottom,
canvas.drawLine(maxTargetX, canvas.height - edgeOffset - 40, maxTargetX, canvas.height - edgeOffset, targetZonePaint) 2f,
2f,
targetZoneStrokePaint
)
} }
if (showLabel){ // Draw vertical target indicator line if target is present
val textBounds = textPaint.measureText(label) if (target != null) {
val xOffset = (textBounds + 20).coerceAtLeast(10f) / 2f val targetX = (canvas.width * target!!).toFloat()
val yOffset = when(size){ targetIndicatorPaint.color = if (isTargetMet) Color.GREEN else Color.RED
canvas.drawLine(targetX, barTop, targetX, barBottom, targetIndicatorPaint)
}
if (showLabel) {
val textContent = label // Store original label
val measuredTextWidth = textPaint.measureText(textContent)
val labelBoxWidth = (measuredTextWidth + 20).coerceAtLeast(10f)
val labelBoxHeight = size.fontSize + 10f // Consistent height with padding
// Calculate horizontal position for the label box (centered around progress end, clamped)
val labelBoxLeft = (rect.right - labelBoxWidth / 2f).coerceIn(
0f,
canvas.width - labelBoxWidth
)
var labelBoxTop: Float
var labelBoxBottom: Float
var textYPosition: Float
if (target != null) { // If workout target is present, move label ABOVE the bar
val labelPadding = 5f // Padding between bar and label box
labelBoxBottom = barTop - labelPadding
labelBoxTop = labelBoxBottom - labelBoxHeight
// Vertically center text in the new box
val labelBoxCenterY = labelBoxTop + labelBoxHeight / 2f
textYPosition =
labelBoxCenterY - (textPaint.ascent() + textPaint.descent()) / 2f
} else { // Original position for BOTTOM
val yOffsetOriginal = when (size) {
CustomProgressBarSize.SMALL -> size.fontSize / 2 + 2f CustomProgressBarSize.SMALL -> size.fontSize / 2 + 2f
CustomProgressBarSize.MEDIUM -> size.fontSize / 2 CustomProgressBarSize.MEDIUM -> size.fontSize / 2
CustomProgressBarSize.LARGE -> size.fontSize / 2 - 5f CustomProgressBarSize.LARGE -> size.fontSize / 2 - 5f
} }
val x = (rect.right - xOffset).coerceIn(0f..canvas.width-xOffset*2f) labelBoxTop = barTop - yOffsetOriginal
val y = (rect.top - yOffset) labelBoxBottom = barBottom + 5f // Original 'b' calculation
val r = x + xOffset * 2 textYPosition =
val b = rect.bottom + 5 barBottom - 1f // Original text Y (rect.top + size.barHeight -1)
}
// Use targetZonePaint for outline if target is met lineStrokePaint.color = if (target != null){
val currentOutlinePaint = if (isTargetMet) targetZonePaint else lineStrokePaint if (isTargetMet) Color.GREEN else Color.RED
} else progressColor
canvas.drawRoundRect(x, y, r, b, 2f, 2f, textBackgroundPaint) canvas.drawRoundRect(
canvas.drawRoundRect(x, y, r, b, 2f, 2f, blurPaint) labelBoxLeft,
canvas.drawRoundRect(x, y, r, b, 2f, 2f, currentOutlinePaint) labelBoxTop,
labelBoxLeft + labelBoxWidth,
labelBoxBottom,
2f,
2f,
textBackgroundPaint
)
canvas.drawRoundRect(
labelBoxLeft,
labelBoxTop,
labelBoxLeft + labelBoxWidth,
labelBoxBottom,
2f,
2f,
blurPaint
)
canvas.drawRoundRect(
labelBoxLeft,
labelBoxTop,
labelBoxLeft + labelBoxWidth,
labelBoxBottom,
2f,
2f,
lineStrokePaint
)
canvas.drawText(label, x + xOffset, rect.top + size.barHeight - 1, textPaint) canvas.drawText(
textContent,
labelBoxLeft + labelBoxWidth / 2f,
textYPosition,
textPaint
)
} }
} }
} }

View File

@ -175,7 +175,7 @@ class Window(
var lastKnownRouteLength: Double? = null var lastKnownRouteLength: Double? = null
combine(karooSystem.streamUserProfile(), karooSystem.streamDataFlow(DataType.Type.DISTANCE_TO_DESTINATION), karooSystem.streamNavigationState()) { userProfile, distanceToDestination, navigationState -> combine(karooSystem.streamUserProfile(), karooSystem.streamDataFlow(DataType.Type.DISTANCE_TO_DESTINATION), karooSystem.streamNavigationState()) { userProfile, distanceToDestination, navigationState ->
StreamData(userProfile, (distanceToDestination as? StreamState.Streaming)?.dataPoint?.singleValue, navigationState) StreamData(userProfile, (distanceToDestination as? StreamState.Streaming)?.dataPoint?.values[DataType.Field.DISTANCE_TO_DESTINATION], navigationState)
}.distinctUntilChanged().throttle(5_000).collect { (userProfile, distanceToDestination, navigationState) -> }.distinctUntilChanged().throttle(5_000).collect { (userProfile, distanceToDestination, navigationState) ->
val state = navigationState.state val state = navigationState.state
val routePolyline = when (state) { val routePolyline = when (state) {

View File

@ -2,12 +2,12 @@
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="40dp"> android:layout_height="80dp">
<de.timklge.karoopowerbar.CustomProgressBar <de.timklge.karoopowerbar.CustomProgressBar
android:id="@+id/progressBar" android:id="@+id/progressBar"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="40dp" android:layout_height="80dp"
android:layout_gravity="center" /> android:layout_gravity="center" />
</FrameLayout> </FrameLayout>