372 lines
16 KiB
Kotlin

package de.timklge.karoopowerbar
import android.content.Context
import android.graphics.BlurMaskFilter
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.util.Log
import android.view.View
import androidx.annotation.ColorInt
import androidx.core.graphics.ColorUtils
import de.timklge.karoopowerbar.screens.SelectedSource
class CustomView @JvmOverloads constructor(
context: Context, attrs: AttributeSet? = null
) : View(context, attrs) {
var progressBars: Map<HorizontalPowerbarLocation, CustomProgressBar>? = null
override fun onDrawForeground(canvas: Canvas) {
super.onDrawForeground(canvas)
// Draw all progress bars
progressBars?.values?.forEach { progressBar ->
Log.d(KarooPowerbarExtension.TAG, "Drawing progress bar for source: ${progressBar.source} - location: ${progressBar.location} - horizontalLocation: ${progressBar.horizontalLocation}")
progressBar.onDrawForeground(canvas)
}
}
}
class CustomProgressBar(private val view: CustomView,
val source: SelectedSource,
val location: PowerbarLocation,
val horizontalLocation: HorizontalPowerbarLocation) {
var progress: Double? = 0.5
var label: String = ""
var minTarget: Double? = null
var maxTarget: Double? = null
var target: Double? = null
var showLabel: Boolean = true
var barBackground: Boolean = false
@ColorInt var progressColor: Int = 0xFF2b86e6.toInt()
var fontSize = CustomProgressBarFontSize.MEDIUM
set(value) {
field = value
textPaint.textSize = value.fontSize
view.invalidate() // Redraw to apply new font size
}
var barSize = CustomProgressBarBarSize.MEDIUM
set(value) {
field = value
targetZoneStrokePaint.strokeWidth = when(value){
CustomProgressBarBarSize.NONE, CustomProgressBarBarSize.SMALL -> 3f
CustomProgressBarBarSize.MEDIUM -> 6f
CustomProgressBarBarSize.LARGE -> 8f
}
targetIndicatorPaint.strokeWidth = when(value){
CustomProgressBarBarSize.NONE, CustomProgressBarBarSize.SMALL -> 6f
CustomProgressBarBarSize.MEDIUM -> 8f
CustomProgressBarBarSize.LARGE -> 10f
}
view.invalidate() // Redraw to apply new bar size
}
private val targetColor = 0xFF9933FF.toInt()
private val targetZoneFillPaint = Paint().apply {
isAntiAlias = true
style = Paint.Style.FILL
color = targetColor
alpha = 100 // Semi-transparent fill
}
private val targetZoneStrokePaint = Paint().apply {
isAntiAlias = true
strokeWidth = 6f
style = Paint.Style.STROKE
color = targetColor
}
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 = 6f
style = Paint.Style.STROKE
color = progressColor
maskFilter = BlurMaskFilter(3f, BlurMaskFilter.Blur.NORMAL)
}
private val blurPaintHighlight = Paint().apply {
isAntiAlias = true
strokeWidth = 10f
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.fontSize
typeface = Typeface.create(Typeface.MONOSPACE, Typeface.BOLD)
textAlign = Paint.Align.CENTER
}
private val targetIndicatorPaint = Paint().apply {
isAntiAlias = true
strokeWidth = 8f
style = Paint.Style.STROKE
}
fun onDrawForeground(canvas: Canvas) {
// Determine if the current progress is within the target range
val isTargetMet =
progress != null && minTarget != null && maxTarget != null && progress!! >= minTarget!! && progress!! <= maxTarget!!
linePaint.color = progressColor
lineStrokePaint.color = progressColor
blurPaint.color = progressColor
blurPaintHighlight.color = ColorUtils.blendARGB(progressColor, 0xFFFFFF, 0.5f)
val p = (progress ?: 0.0).coerceIn(0.0, 1.0)
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()
}
val minTargetX = when (horizontalLocation) {
HorizontalPowerbarLocation.LEFT -> if (minTarget != null) (halfWidth * minTarget!!).toFloat() else 0f
HorizontalPowerbarLocation.RIGHT -> if (minTarget != null) halfWidth + (halfWidth * minTarget!!).toFloat() else 0f
HorizontalPowerbarLocation.FULL -> if (minTarget != null) (fullWidth * minTarget!!).toFloat() else 0f
}
val maxTargetX = when (horizontalLocation) {
HorizontalPowerbarLocation.LEFT -> if (maxTarget != null) (halfWidth * maxTarget!!).toFloat() else 0f
HorizontalPowerbarLocation.RIGHT -> if (maxTarget != null) halfWidth + (halfWidth * maxTarget!!).toFloat() else 0f
HorizontalPowerbarLocation.FULL -> if (maxTarget != null) (fullWidth * maxTarget!!).toFloat() else 0f
}
val targetX = when (horizontalLocation) {
HorizontalPowerbarLocation.LEFT -> if (target != null) (halfWidth * target!!).toFloat() else 0f
HorizontalPowerbarLocation.RIGHT -> if (target != null) halfWidth + (halfWidth * target!!).toFloat() else 0f
HorizontalPowerbarLocation.FULL -> if (target != null) (fullWidth * target!!).toFloat() else 0f
}
val backgroundLeft = when (horizontalLocation) {
HorizontalPowerbarLocation.LEFT -> 0f
HorizontalPowerbarLocation.RIGHT -> halfWidth
HorizontalPowerbarLocation.FULL -> 0f
}
val backgroundRight = when (horizontalLocation) {
HorizontalPowerbarLocation.LEFT -> halfWidth
HorizontalPowerbarLocation.RIGHT -> fullWidth
HorizontalPowerbarLocation.FULL -> fullWidth
}
when (location) {
PowerbarLocation.TOP -> {
val rect = RectF(
barLeft,
15f,
barRight,
15f + barSize.barHeight // barSize.barHeight will be 0f if NONE
)
// Draw bar components only if barSize is not NONE
if (barSize != CustomProgressBarBarSize.NONE) {
if (barBackground){
canvas.drawRect(backgroundLeft, 15f, backgroundRight, 15f + barSize.barHeight, backgroundPaint)
}
// Draw target zone fill behind the progress bar
if (minTarget != null && maxTarget != null) {
canvas.drawRoundRect(
minTargetX,
15f,
maxTargetX,
15f + barSize.barHeight,
2f,
2f,
targetZoneFillPaint
)
}
if (progress != null) {
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)
}
}
// Draw label (if progress is not null and showLabel is true)
if (progress != null) {
// Draw target zone stroke after progress bar, before label
if (minTarget != null && maxTarget != null) {
// Draw stroked rounded rectangle for the target zone
canvas.drawRoundRect(
minTargetX,
15f,
maxTargetX,
15f + barSize.barHeight,
2f,
2f,
targetZoneStrokePaint
)
}
// Draw vertical target indicator line if target is present
if (target != null) {
targetIndicatorPaint.color = if (isTargetMet) Color.GREEN else Color.RED
canvas.drawLine(targetX, 15f, targetX, 15f + barSize.barHeight, targetIndicatorPaint)
}
if (showLabel){
lineStrokePaint.color = if (target != null){
if (isTargetMet) Color.GREEN else Color.RED
} else progressColor
blurPaint.color = lineStrokePaint.color
blurPaintHighlight.color = ColorUtils.blendARGB(lineStrokePaint.color, 0xFFFFFF, 0.5f)
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)
val r = x + xOffset * 2
val fm = textPaint.fontMetrics
// barCenterY calculation uses barSize.barHeight, which is 0f for NONE,
// correctly centering the label on the 15f line.
val barCenterY = rect.top + barSize.barHeight / 2f
val centeredTextBaselineY = barCenterY - (fm.ascent + fm.descent) / 2f
val calculatedTextBoxTop = centeredTextBaselineY + fm.ascent
val finalTextBoxTop = calculatedTextBoxTop.coerceAtLeast(0f)
val finalTextBaselineY = finalTextBoxTop - fm.ascent
val finalTextBoxBottom = finalTextBaselineY + fm.descent
canvas.drawRoundRect(x, finalTextBoxTop, r, finalTextBoxBottom, 2f, 2f, textBackgroundPaint)
canvas.drawRoundRect(x, finalTextBoxTop, r, finalTextBoxBottom, 2f, 2f, blurPaint)
canvas.drawRoundRect(x, finalTextBoxTop, r, finalTextBoxBottom, 2f, 2f, lineStrokePaint)
canvas.drawText(label, x + xOffset, finalTextBaselineY, textPaint)
}
}
}
PowerbarLocation.BOTTOM -> {
val rect = RectF(
barLeft,
canvas.height.toFloat() - 1f - barSize.barHeight, // barSize.barHeight will be 0f if NONE
barRight,
canvas.height.toFloat()
)
// Draw bar components only if barSize is not NONE
if (barSize != CustomProgressBarBarSize.NONE) {
if (barBackground){
// Use barSize.barHeight for background top calculation
canvas.drawRect(backgroundLeft, canvas.height.toFloat() - barSize.barHeight, backgroundRight, canvas.height.toFloat(), backgroundPaint)
}
// Draw target zone fill behind the progress bar
if (minTarget != null && maxTarget != null) {
canvas.drawRoundRect(
minTargetX,
canvas.height.toFloat() - barSize.barHeight,
maxTargetX,
canvas.height.toFloat(),
2f,
2f,
targetZoneFillPaint
)
}
if (progress != null) {
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)
}
}
// Draw label (if progress is not null and showLabel is true)
if (progress != null) {
// Draw target zone stroke after progress bar, before label
if (minTarget != null && maxTarget != null) {
// Draw stroked rounded rectangle for the target zone
canvas.drawRoundRect(
minTargetX,
canvas.height.toFloat() - barSize.barHeight,
maxTargetX,
canvas.height.toFloat(),
2f,
2f,
targetZoneStrokePaint
)
}
// Draw vertical target indicator line if target is present
if (target != null) {
targetIndicatorPaint.color = if (isTargetMet) Color.GREEN else Color.RED
canvas.drawLine(targetX, canvas.height.toFloat() - barSize.barHeight, targetX, canvas.height.toFloat(), targetIndicatorPaint)
}
if (showLabel){
lineStrokePaint.color = if (target != null){
if (isTargetMet) Color.GREEN else Color.RED
} else progressColor
blurPaint.color = lineStrokePaint.color
blurPaintHighlight.color = ColorUtils.blendARGB(lineStrokePaint.color, 0xFFFFFF, 0.5f)
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)
val r = x + xOffset * 2
// textDrawBaselineY calculation uses rect.top and barSize.barHeight.
// If NONE, barSize.barHeight is 0f. rect.top becomes canvas.height - 1f.
// So, baseline is (canvas.height - 1f) + 0f - 1f = canvas.height - 2f.
val textDrawBaselineY = rect.top + barSize.barHeight - 1f
val yBox = textDrawBaselineY + textPaint.ascent()
val bBox = textDrawBaselineY + textPaint.descent()
canvas.drawRoundRect(x, yBox, r, bBox, 2f, 2f, textBackgroundPaint)
canvas.drawRoundRect(x, yBox, r, bBox, 2f, 2f, blurPaint)
canvas.drawRoundRect(x, yBox, r, bBox, 2f, 2f, lineStrokePaint)
canvas.drawText(label, x + xOffset, textDrawBaselineY, textPaint)
}
}
}
}
}
fun invalidate() {
// Invalidate the view to trigger a redraw
view.invalidate()
}
}