Jetpack Compose Desktop
依赖部分就不说了,可以参考前文用Jetpack Compose Desktop极简配置做一个Windows桌面时间显示器和用Jetpack Compose Desktop做一个推箱子小游戏,还是默认的依赖,其实也就是一个compose.desktop.currentOs
,然后首先还是修订下基本窗口,做个竖着的矩形:
fun main() = application {
Window(
onCloseRequest = ::exitApplication,
state = rememberWindowState(
size = DpSize(300.dp, 500.dp),
position = WindowPosition.Aligned(Alignment.Center)
),
title = "BMI指数"
) {
app()
}
}
然后在app()
里画上主体部分,也就是一个居中的Box
,里面用Colunm
来排列两个TextField
分别接受身高和体重:
@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("体重") })
}
}
}
}
然后就能看到这个效果了:
记录输入
然后就是要保存身高体重的数据,并且做验证,所以先定义两个状态变量:
var weight = remember { mutableStateOf("") }
var height = remember { mutableStateOf("") }
这里可以做个通用的方法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) })
}
然后把刚才的两个TextField
换成用generateInput()
重新生成文本框:
Column {
generateInput("身高", height)
generateInput("体重", weight)
}
这样就能把数据保存起来并且可以验证是数字了。
计算BMI
这里要专门做一个方法计算,方法名可以就叫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)
}
这里可以重新定义weight
和height
是kotlin
的语法特性名称遮蔽(Name shadowed),可以了解一下,后面的计算看上去很清爽也要得益于kotlin
为BigDecimal
提供了直接/
的操作。
然后再在Column
下面做一个Text()
进行显示:
Text(
modifier = Modifier.padding(10.dp),
text = calcBMI(height.value, weight.value).toString(),
style = TextStyle(fontSize = 21.sp)
)
这个文本其实才是整个窗口的核心,所以大小我特意调大一点点,然后就能看到效果了:
但仅仅是这样肯定不行,总不能让人背下BMI标准或者每次算了还去找个表对照吧,所以下面再做个图提供说明参考。
标准值参考图像制作
下面要放一个Canvas
用来显示BMI指数以及一个列图,众所周知BMI分4个级别(偏瘦、正常、超重、肥胖),所以我们先把标签画上。然后还要再画个框套着,4个级别就是4个框,框之间应该留些空白,4个框就应该有3道空白,每个空白定位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)
)
}
}
然后就能看到一个比较简单的效果:
然后需要再给每个区域填充不同的颜色,比喻说健康就是绿色,肥胖应该是橙色。但是框线还是需要保留的,不过drawRect
没办法同时Fill
和Stroke
,所以只能画两个rect
,所以上面的代码可以进行如下修改:
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),
)
}
}
为什么这里我把drawText
挪下来了呢?其实就是因为被Fill
的rect
遮住了,如果在rect
画好之后再画字就没问题了,所以要把drawText
挪下来,文字颜色也稍微设置了一下,效果就是这样:
为什么这里我把drawText
挪下来了呢?其实就是因为被Fill
的rect
遮住了,如果在rect
画好之后再画字就没问题了,所以要把drawText
挪下来,文字颜色也稍微设置了一下,效果就是这样:
接下来要做的就是让rect
默认都透明一些,如果上面的BMI数值属于它的区间,那么就颜色加深,边框也加厚,这就跟上面整个联动起来了,中间那一行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),
)
}
}
然后输入数字,可以看到计算出19.3
,是符合BMI正常水平的:
下面进行一些动画演示:
最终效果演示
输入不同的数值:
调节窗口大小:
还是挺好玩的,写起来也不怎么费事,可以动手来试一试!
本文于2023年7月20日写作并同时发布在lyrieek的稀土掘金社区与阿里云开发者社区。