Skip to content

Instantly share code, notes, and snippets.

@Kodmia
Created February 22, 2024 10:58
Show Gist options
  • Save Kodmia/0e5b31b1014a0131ffea048f79120b2f to your computer and use it in GitHub Desktop.
Save Kodmia/0e5b31b1014a0131ffea048f79120b2f to your computer and use it in GitHub Desktop.
Gauge
private const val indicatorStartAngle = 135
private const val indicatorArcDegrees = 270
@OptIn(ExperimentalTextApi::class)
@Composable
fun Gauge(
canvasSize: Dp = 300.dp,
indicatorValue: Float = 0f,
presetValue: Float = 0f,
showPresetValue: Boolean = false,
maxIndicatorValue: Float,
minWarningValue: Float,
maxWarningValue: Float,
pressureUnits: PressureUnits = PressureUnits.BAR,
scaleFactor: Float = 1.5f,
backgroundIndicatorColor: Color = MaterialTheme.colors.onBackground.copy(alpha = 0.2f),
foregroundIndicatorColor: Color = MaterialTheme.colors.primary,
errorForegroundIndicatorColor: Color = MaterialTheme.colors.error,
warningForegroundIndicatorColor: Color = MaterialTheme.colors.error,
indicatorStrokeWidth: Float = 20f,
markerTextStyle: TextStyle = MaterialTheme.typography.subtitle1.copy(fontSize = 8.sp),
pressureTextStyle: TextStyle = MaterialTheme.typography.subtitle1.copy(fontSize = 32.sp, fontWeight = FontWeight.Bold),
pressureUnitsTextStyle: TextStyle = MaterialTheme.typography.subtitle1.copy(fontSize = 12.sp),
pressurePresetTextStyle: TextStyle = MaterialTheme.typography.subtitle1.copy(fontSize = 12.sp),
borderColor: Color = MaterialTheme.colors.primary,
borderWidth: Dp = 1.dp,
){
val numOfMarkers: Int = if (maxIndicatorValue.toInt() % 10 == 0){
10
}
else {
5
}
val numOfDigits: Int = when(pressureUnits){
PressureUnits.BAR -> {
1
}
PressureUnits.PSI -> {
0
}
PressureUnits.VOLTS -> {
2
}
}
val textMeasurer = rememberTextMeasurer()
val textTransition = updateTransition(
targetState = showPresetValue,
label = "preset text transition"
)
var allowedIndicatorValue by remember {mutableStateOf(maxIndicatorValue)}
allowedIndicatorValue = if (indicatorValue <= maxIndicatorValue) {
indicatorValue
}
else {
maxIndicatorValue
}
var animatedIndicatorValue by remember {mutableStateOf(0f)}
LaunchedEffect(key1 = allowedIndicatorValue){
animatedIndicatorValue = allowedIndicatorValue
}
val percentage = (animatedIndicatorValue / maxIndicatorValue) * 100
val sweepAngle by animateFloatAsState(
targetValue = (indicatorArcDegrees.toFloat() / 100f * percentage),
animationSpec = tween(1000)
)
val receivedValue by animateFloatAsState(
targetValue = allowedIndicatorValue,
animationSpec = tween(1000)
)
val animatedPressureTextColor by animateColorAsState(
targetValue = if (allowedIndicatorValue <= 0f){
MaterialTheme.colors.onSurface.copy(alpha = 0.3f)
}
else {
MaterialTheme.colors.onSurface
}
)
val animatedIndicatorColor by animateColorAsState(
targetValue = if (allowedIndicatorValue <= minWarningValue){
errorForegroundIndicatorColor
}
else if (allowedIndicatorValue > maxWarningValue){
warningForegroundIndicatorColor
}
else {
foregroundIndicatorColor
},
animationSpec = tween(1000)
)
Column(
modifier = Modifier
.size(canvasSize)
.drawBehind {
val componentSize = size / scaleFactor //to provide some padding
backgroundIndicator(
componentSize = componentSize,
indicatorColor = backgroundIndicatorColor,
indicatorStrokeWidth = indicatorStrokeWidth,
)
foregroundIndicator(
sweepAngle = sweepAngle,
componentSize = componentSize,
indicatorColor = animatedIndicatorColor,
indicatorStrokeWidth = indicatorStrokeWidth
)
indicatorMarkers(
componentSize = componentSize,
indicatorStrokeWidth = indicatorStrokeWidth,
textMeasurer = textMeasurer,
markersTextStyle = markerTextStyle,
minMarkerValue = 0f,
maxMarkerValue = maxIndicatorValue,
numOfMarkers = numOfMarkers,
numOfDigits = numOfDigits
)
}
.border(
width = borderWidth,
color = borderColor,
shape = CircleShape
)
.clip(
CircleShape
),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
){
TextBlock(
transition = textTransition,
pressureUnits = stringResource(id = pressureUnits.aliasStringId),
pressureValue = receivedValue.toFormattedPressureValue(pressureUnits),
presetValue = presetValue.toFormattedPressureValue(pressureUnits),
pressureTextColor = animatedPressureTextColor,
pressureUnitsTextStyle = pressureUnitsTextStyle,
pressureTextStyle = pressureTextStyle,
presetTextStyle = pressurePresetTextStyle
)
}
}
fun Float.toFormattedPressureValue(pressureUnits: PressureUnits): String {
return when(pressureUnits){
PressureUnits.BAR -> {
"%.1f".format(this)
}
PressureUnits.PSI -> {
"%.0f".format(this)
}
PressureUnits.VOLTS -> {
"%.2f".format(this)
}
}
}
@OptIn(ExperimentalAnimationApi::class)
@Composable
private fun TextBlock(
transition: Transition<Boolean>,
pressureUnits: String,
pressureValue: String,
presetValue: String,
pressureTextColor: Color,
pressureUnitsTextStyle: TextStyle,
pressureTextStyle: TextStyle,
presetTextStyle: TextStyle
){
//Pressure Units
Text(
text = pressureUnits,
textAlign = TextAlign.Center,
style = pressureUnitsTextStyle,
)
//Pressure
Text(
text = pressureValue,
textAlign = TextAlign.Center,
style = pressureTextStyle,
color = pressureTextColor
)
//preset values
transition.AnimatedVisibility(
visible = {targetSelected -> targetSelected},
enter = expandVertically(),
exit = shrinkVertically()
) {
Text(
text = presetValue,
textAlign = TextAlign.Center,
style = presetTextStyle
)
}
}
private fun DrawScope.backgroundIndicator(
componentSize: Size,
indicatorColor: Color,
indicatorStrokeWidth: Float,
){
drawArc(
size = componentSize,
color = indicatorColor,
startAngle = indicatorStartAngle.toFloat(),
sweepAngle = indicatorArcDegrees.toFloat(),
useCenter = false,
style = Stroke(
width = indicatorStrokeWidth,
cap = StrokeCap.Butt
),
topLeft = Offset(
x = (size.width - componentSize.width) / 2f,
y = (size.height - componentSize.height) / 2f
)
)
}
private fun DrawScope.foregroundIndicator(
sweepAngle: Float,
componentSize: Size,
indicatorColor: Color,
indicatorStrokeWidth: Float,
){
drawArc(
size = componentSize,
color = indicatorColor,
startAngle = indicatorStartAngle.toFloat(),
sweepAngle = sweepAngle,
useCenter = false,
style = Stroke(
width = indicatorStrokeWidth,
cap = StrokeCap.Butt
),
topLeft = Offset(
x = (size.width - componentSize.width) / 2f,
y = (size.height - componentSize.height) / 2f
)
)
}
@OptIn(ExperimentalTextApi::class)
private fun DrawScope.indicatorMarkers(
componentSize: Size,
indicatorStrokeWidth: Float,
textMeasurer: TextMeasurer,
markersTextStyle: TextStyle,
minMarkerValue: Float,
maxMarkerValue: Float,
numOfMarkers: Int,
numOfDigits: Int
){
val w = size.width
val h = size.height
val degreesMarkerStep = indicatorArcDegrees / numOfMarkers
val startStepAngle = -(180 - indicatorStartAngle)
val textMarkerStep = (maxMarkerValue - minMarkerValue) / numOfMarkers.toFloat()
val maxMeasuredText = textMeasurer.measure(
text = AnnotatedString("000"),
style = markersTextStyle
)
for ((counter, degrees) in (startStepAngle .. (startStepAngle + indicatorArcDegrees) step degreesMarkerStep)
.withIndex()
){
val lineEndX = (size.width - componentSize.width) / 2f + indicatorStrokeWidth / 2f
val lineStartX = (size.width - componentSize.width) / 2f - indicatorStrokeWidth / 2f
val xx = componentSize.width / 2f + indicatorStrokeWidth / 2f + maxMeasuredText.size.width / 2f + 10f
val posX = xx * cos(-Math.toRadians(degrees.toDouble()).toFloat())
val posY = xx * sin(-Math.toRadians(degrees.toDouble()).toFloat())
val text = textMeasurer.measure(
text = AnnotatedString("%.${numOfDigits}f".format(minMarkerValue + textMarkerStep*counter)),
style = markersTextStyle
)
val tw = text.size.width / 2
val th = text.size.height / 2
rotate(
degrees.toFloat(),
){
drawLine(
color = Color.Black,
start = Offset(lineStartX, h/2f),
end = Offset(lineEndX, h/2f),
strokeWidth = 3f
)
}
drawText(
text,
color = Color.LightGray,
topLeft = Offset(
x = w / 2f - posX - tw,
y = h / 2f + posY - th,
)
)
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment