Coverage Summary for Class: SurferBudgetProgressBarKt (com.georgeci.moneysurfer.uikit.components.budget)
| Class |
Class, %
|
Method, %
|
Branch, %
|
Line, %
|
Instruction, %
|
| SurferBudgetProgressBarKt |
100%
(1/1)
|
75%
(3/4)
|
60%
(12/20)
|
100%
(30/30)
|
97.1%
(409/421)
|
package com.georgeci.moneysurfer.uikit.components.budget
import androidx.compose.foundation.Canvas
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.CornerRadius
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Size
import androidx.compose.ui.semantics.clearAndSetSemantics
import androidx.compose.ui.semantics.contentDescription
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import com.georgeci.moneysurfer.uikit.preview.SurferComponentPreview
import com.georgeci.moneysurfer.uikit.theme.AppTheme
/**
* Budget spend as a bar, with an optional reference tick the fill is read against.
*
* What the tick *means* is the caller's to decide, which is why it is not named after any one
* reading: the budget cards put it at the alert threshold, the dashboard's safe-to-spend card puts
* it at how far into the period we are so the bar reads as a pace. Anything that styles or narrates
* the tick has to keep working for both.
*
* The fill is capped at the full width — past 100 % the colour carries the overspend, not the
* geometry, so an overshoot never draws outside the track. [contentDescription] is what screen
* readers get: the bar is a picture of numbers stated in the surrounding text, so the caller
* passes the sentence rather than letting the shape speak for itself.
*/
@Composable
fun SurferBudgetProgressBar(
progress: Float,
status: SurferBudgetStatus,
modifier: Modifier = Modifier,
tickFraction: Float? = null,
height: Dp = 8.dp,
contentDescription: String? = null,
) {
val fill = status.color
val track = AppTheme.materialColors.surfaceContainerHighest
val tickColor = AppTheme.materialColors.onSurfaceVariant
val capped = progress.coerceIn(0f, 1f)
Canvas(
modifier = modifier
.fillMaxWidth()
.height(height)
.clearAndSetSemantics {
if (contentDescription != null) this.contentDescription = contentDescription
},
) {
val radius = CornerRadius(size.height / 2f)
drawRoundRect(color = track, cornerRadius = radius)
if (capped > 0f) {
drawRoundRect(
color = fill,
size = Size(size.width * capped, size.height),
cornerRadius = radius,
)
}
val tick = tickFraction?.coerceIn(0f, 1f) ?: return@Canvas
val x = (size.width * tick).coerceIn(0f, size.width - TICK_WIDTH_PX)
drawRect(
color = tickColor.copy(alpha = 0.6f),
topLeft = Offset(x, 0f),
size = Size(TICK_WIDTH_PX, size.height),
)
}
}
private const val TICK_WIDTH_PX = 2f
@Preview
@Composable
private fun SurferBudgetProgressBarPreview() {
SurferComponentPreview {
Column(
modifier = Modifier.fillMaxWidth().padding(16.dp),
verticalArrangement = Arrangement.spacedBy(12.dp),
) {
SurferBudgetProgressBar(progress = 0.35f, status = SurferBudgetStatus.Ok, tickFraction = 0.8f)
SurferBudgetProgressBar(progress = 0.86f, status = SurferBudgetStatus.Warn, tickFraction = 0.8f)
SurferBudgetProgressBar(progress = 1.4f, status = SurferBudgetStatus.Over, tickFraction = 0.8f)
SurferBudgetProgressBar(progress = 0f, status = SurferBudgetStatus.Ok)
}
}
}