In the morning, I found some problems with a BMI display panel made a few years ago. I thought that Jetpack Compose Desktop can do this well. Although it is impossible to use this to replace the system on the webpage, the effect is definitely good, and it is worth trying.
Jetpack Compose Desktop
I won’t talk about the dependency part. You can refer to the previous article to use the minimal configuration of Jetpack Compose Desktop to make a Windows desktop time display and use Jetpack Compose Desktop to make a small game of Sokoban . It is still the default dependency, which is actually one compose.desktop.currentOs
.
basic layout
Then firstly revise the basic window and make a vertical rectangle:
fun main() = application {
Window(
onCloseRequest = ::exitApplication,
state = rememberWindowState(
size = DpSize(300.dp, 500.dp),
position = WindowPosition.Aligned(Alignment.Center)
),
title = "BMI指数"
) {
app()
}
}
Then app()
draw the main part in it, that is, a centered one Box
, which Colunm
is used to arrange two TextField
to accept height and weight respectively:
@Composable
@Preview
fun app() {
MaterialTheme {
Box(
modifier = Modifier.padding(20.dp).fillMaxSize(),
contentAlignment = Alignment.Center
) {
Column {
TextField(value = "", onValueChange = {}, label = { Text("身高") })
TextField(value = "", onValueChange = {}, label = { Text("体重") })
}
}
}
}
Then you can see this effect:
record input
Then it is necessary to save the data of height and weight and do verification, so first define two state variables:
var weight = remember { mutableStateOf("") }
var height = remember { mutableStateOf("") }
Here is a general method generateInput()
:
@Composable
fun generateInput(label: String, model: MutableState<String>) {
return TextField(
value = model.value,
//手机上要输入数字用这个keyboardOptions就行了,但桌面应用不行
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
onValueChange = {
//因为有可能输入法错误,把小数点输入成句号,所以做下替换,将“。”替换成“.”
val number = it.replace('。', '.')
//这个判断我是经过一定考虑的,可以清空文本框,但不应该输入0或者以0开头的数
if (number.isNotBlank() && !number.matches(Regex("[1-9](\\d+)?(\\.(\\d+)?)?"))) {
return@TextField
}
model.value = number
},
label = { Text(label) })
}
Then replace the two just now TextField
with generateInput()
regenerated text boxes:
Column {
generateInput("身高", height)
generateInput("体重", weight)
}
In this way, the data can be saved and can be verified as a number.
Calculating BMI
Here we need to do a special method calculation, the method name can be called calcBMI()
:
fun calcBMI(height: String, weight: String): BigDecimal {
if (weight.isEmpty() || height.isEmpty())
return BigDecimal.ZERO
val weight = BigDecimal(weight).setScale(2, RoundingMode.HALF_UP)
val height = BigDecimal(height).setScale(2, RoundingMode.HALF_UP)
if (weight < BigDecimal.ONE || height < BigDecimal.ONE)
return BigDecimal.ZERO
return (weight / (height / BigDecimal(100)).pow(2)).setScale(1, RoundingMode.HALF_UP)
}
Here you can redefine weight
and the grammatical feature name shadowed ( Name shadowed ), you can understand that the subsequent calculations look very refreshing and also benefit height
from the direct operation provided for . Then make another one below for display:kotlin
kotlin
BigDecimal
/
Column
Text()
Text(
modifier = Modifier.padding(10.dp),
text = calcBMI(height.value, weight.value).toString(),
style = TextStyle(fontSize = 21.sp)
)
This text is actually the core of the entire window, so I deliberately increased the size a little bit, and then I can see the effect:
But this alone is definitely not enough. You can’t let people memorize the BMI standard or find a table for comparison every time you forget it, so let’s make a picture below to provide a reference.
Standard value reference image production
Next, we will put one Canvas
to display the BMI index and a column chart. We all know that BMI is divided into 4 levels (thin, normal, overweight, and obese), so let's draw the label first. Then draw a frame to enclose it. The 4 levels are 4 frames. There should be some blank space between the frames. There should be 3 blank spaces in the 4 frames. Each blank should be positioned as follows 10
:
//这个TextMeasurer给字体用,每个字体都要有
val measurer = rememberTextMeasurer()
val standardArray = listOf("偏瘦 <=18.4", "正常 18.5 ~ 23.9", "超重 24.0 ~ 27.9", "肥胖 >= 28.0")
Canvas(
modifier = Modifier.fillMaxSize()
) {
//总体高度应该减去中间(3*10)的空白间隙
val calcHeight = size.height - 30
for (index in 0..3) {
//计算好每块的Y轴定位,给文字和框定位用
val offsetY = calcHeight / 4 * index + index * 10
drawText(
measurer,
standardArray[index],
style = TextStyle(fontSize = 16.sp, fontWeight = FontWeight.Bold),
topLeft = Offset(5f, offsetY + 5),
)
drawRect(
color = Color.Blue,
topLeft = Offset(0f, offsetY),
size = Size(size.width, calcHeight / 4),
style = Stroke(2f)
)
}
}
Then you can see a relatively simple effect:
Then you need to fill each area with a different color. For example, health is green, and obesity should be orange. But the frame line still needs to be preserved, but drawRect
there is no way to add it at the same Fill
time Stroke
, so only two can be drawn rect
, so the above code can be modified as follows:
val colorArray = listOf(Color(0xff93b5cf), Color(0xff20a162), Color(0xfffcc515), Color(0xfff86b1d))
Canvas(
modifier = Modifier.fillMaxSize()
) {
val calcHeight = size.height - 30
for (index in 0..3) {
val offsetY = calcHeight / 4 * index + index * 10
drawRect(
color = colorArray[index],
topLeft = Offset(0f, offsetY),
size = Size(size.width, calcHeight / 4),
style = Fill
)
drawRect(
color = Color.DarkGray,
topLeft = Offset(0f, offsetY),
size = Size(size.width, calcHeight / 4),
style = Stroke(1f)
)
drawText(
measurer,
standardArray[index],
style = TextStyle(fontSize = 16.sp, fontWeight = FontWeight.Bold, color = Color.DarkGray),
topLeft = Offset(5f, offsetY + 5),
)
}
}
Why did I remove it here drawText
? In fact, it is because it is covered Fill
by rect
. If rect
you draw the characters after drawing, it will be no problem, so you have to drawText
move them down and set the color of the characters a little bit. The effect is like this:
The next thing to do is to make rect
the default transparent. If the above BMI value belongs to its range, then the color will be darkened and the border will be thickened. This will be linked with the above. The assignment of the middle row should also be taken out for later judgments Text()
:
val bmi = calcBMI(height.value, weight.value)
Text(
modifier = Modifier.padding(10.dp),
text = bmi.toString(),
style = TextStyle(fontSize = 23.sp)
)
var selectIndex = if (bmi == BigDecimal.ZERO) -1 else 0
if (bmi > BigDecimal("18.5")) selectIndex++
if (bmi > BigDecimal("23.9")) selectIndex++
if (bmi > BigDecimal("27.9")) selectIndex++
val measurer = rememberTextMeasurer()
val standardArray = listOf("偏瘦 <=18.4", "正常 18.5 ~ 23.9", "超重 24.0 ~ 27.9", "肥胖 >= 28.0")
val colorArray = listOf(Color(0xff93b5cf), Color(0xff20a162), Color(0xfffcc515), Color(0xfff86b1d))
Canvas(
modifier = Modifier.fillMaxSize()
) {
val calcHeight = size.height - 30
for (index in 0..3) {
val offsetY = calcHeight / 4 * index + index * 10
drawRect(
color = colorArray[index],
topLeft = Offset(0f, offsetY),
size = Size(size.width, calcHeight / 4),
style = Fill,
alpha = if (index == selectIndex) 1f else .5f
)
drawRect(
color = Color.DarkGray,
topLeft = Offset(0f, offsetY),
size = Size(size.width, calcHeight / 4),
style = Stroke(if (index == selectIndex) 3f else 1f)
)
drawText(
measurer,
standardArray[index],
style = TextStyle(fontSize = 16.sp, fontWeight = FontWeight.Bold, color = Color.DarkGray),
topLeft = Offset(5f, offsetY + 5),
)
}
}
Then enter the number, you can see that the calculation 19.3
is in line with the normal level of BMI:
Here are some animations:
Demonstration of the final effect
Enter a different value:
Adjust window size:
It's still quite fun, and it doesn't take too much trouble to write, so you can try it yourself!
This article was written on July 20, 2023 and published simultaneously in lyrieek's rare earth nuggets community and Alibaba Cloud developer community.