Created
February 22, 2024 10:58
-
-
Save Kodmia/0e5b31b1014a0131ffea048f79120b2f to your computer and use it in GitHub Desktop.
Gauge
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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