展示如何使用 Jetpack Compose 构建 24 小时时间选择器对话框。
让我们首先定义一个定时器选择器的组件,而不是仅仅对话框。这样我们就可以在应用程序的其他部分,甚至在对话框之外,都能够重复使用这个时间选择器。
@Composable
fun TimerPicker(
onCancel: () -> Unit,
onOk: (Time) -> Unit,
modifier: Modifier = Modifier
)
onCancel
按下取消按钮时调用。onOk
当按下 ok 按钮时调用。
Time
只是一个包含小时和分钟的数据类。
data class Time(val hour: Int, val minute: Int)
在TimerPicker
组件前面,我们需要定义一些变量。
var selectedPart by remember { mutableStateOf(TimePart.Hour) }
var selectedHour by remember { mutableStateOf(0) }
var selectedMinute by remember { mutableStateOf(0) }
val selectedIndex by remember {
derivedStateOf { if (selectedPart == TimePart.Hour) selectedHour else selectedMinute / 5 }
}
val onTime: (Int) -> Unit = remember {
{ if (selectedPart == TimePart.Hour) selectedHour = it else selectedMinute = it * 5 }
}
selectedPart
指示我们是否应该显示小时或分钟。selectedHour
存储用户选择的时间。selectedMinute
存储用户选择的分钟。
selectedIndex
代表时钟中的位置,对于小时来说,它是小时(0-23),对于分钟来说,它是同一位置的小时。例如,15 分钟为 3,40 分钟为 8。我们需要用selectedMinute
除以5,因为这是相应的小时。
enum class TimePart { Hour, Minute }
TimePart
只是一个代表所选内容(小时或分钟)的枚举。
时钟上方有两张卡片,一张是小时,一张是分钟,我把它们称为TimeCard
@Composable
fun TimeCard(
time: Int,
isSelected: Boolean,
onClick: () -> Unit
) {
Card(
shape = RoundedCornerShape(6.dp),
backgroundColor = if (isSelected) selectedColor else secondaryColor,
modifier = Modifier.clickable { onClick() }
) {
Text(
text = if (time == 0) "00" else time.toString(),
fontSize = 32.sp,
color = if (isSelected) secondaryColor else Color.White,
modifier = Modifier.padding(horizontal = 12.dp, vertical = 4.dp)
)
}
}
它只是一张显示所选小时和分钟的卡片。这就是它们在TimerPicker组件中放置的方式。
Row(
modifier = Modifier.align(Alignment.CenterHorizontally)
) {
TimeCard(
time = selectedHour,
isSelected = selectedPart == TimePart.Hour,
onClick = { selectedPart = TimePart.Hour }
)
Text(
text = ":",
fontSize = 32.sp,
color = Color.White,
modifier = Modifier.padding(horizontal = 2.dp)
)
TimeCard(
time = selectedMinute,
isSelected = selectedPart == TimePart.Minute,
onClick = { selectedPart = TimePart.Minute }
)
}
每当它们被按下时,我们根据按下的按钮更新 selectedPart
。
现在让我们来谈谈实际的时钟部分。
@Composable
fun Clock(
index: Int,
modifier: Modifier = Modifier,
content: @Composable () -> Unit
) {
val localDensity = LocalDensity.current
var radiusPx by remember { mutableStateOf(0) }
val radiusInsidePx by remember { derivedStateOf { (radiusPx * 0.67).toInt() } }
var indexCirclePx by remember { mutableStateOf(36f) }
val padding by remember {
derivedStateOf { with(localDensity) { (indexCirclePx * 0.5).toInt().toDp() } }
}
...
}
index
表示从0到23的数字,表示当前选择的索引。
radiusPx
是外部圆的半径(0-11),而radiusInsidePx
是内部圆的半径(12-23)。
在下面,我声明了两个函数来计算时钟上索引的位置。
fun posX(index: Int) =
((if (index < 12) radiusPx else radiusInsidePx) * cos(angleForIndex(index))).toInt()
fun posY(index: Int) =
((if (index < 12) radiusPx else radiusInsidePx) * sin(angleForIndex(index))).toInt()
如果索引小于12,表示时间在外部圆圈上。如果索引大于或等于12,则在内部圆圈上。
private const val step = PI * 2 / 12
private fun angleForIndex(index: Int) = -PI / 2 + step * index
我们在绘制时不使用度数,而是使用弧度。1π弧度等于180°,因此π * 2 / 12
是每个索引的步长(360° / 12)。
angleForIndex
函数根据索引计算角度。0°不在顶部,而是在3点钟位置。我们需要减去90°,以确保索引0在我们认为的0°位置。angleForIndex
从-π / 2
开始,因为π / 2
是90°,所以-π / 2
是-90°,那是索引0开始的地方。
现在我们终于来到了绘制时钟的代码部分,这仍然位于Clock组件内部。
Box(modifier = modifier) {
Surface(
color = primaryColor,
shape = CircleShape,
modifier = Modifier.fillMaxSize()
) {}
Layout(
content = content,
modifier = Modifier
.padding(padding)
.drawBehind {
val end = Offset(
x = size.width / 2 + posX(time),
y = size.height / 2 + posY(time)
)
drawCircle( // #1
radius = 9f,
color = selectedColor,
)
drawLine( // #2
start = center,
end = end,
color = selectedColor,
strokeWidth = 4f
)
drawCircle( // #3
radius = indexCirclePx,
center = end,
color = selectedColor,
)
}
) { measurables, constraints ->
val placeables = measurables.map { it.measure(constraints) }
assert(placeables.count() == 12 || placeables.count() == 24) { "Invalid items: should be 12 or 24, is ${placeables.count()}" }
indexCirclePx = (constraints.maxWidth * 0.07).toFloat() // #4
layout(constraints.maxWidth, constraints.maxHeight) {
val size = constraints.maxWidth
val maxElementSize = maxOf(placeables.maxOf { it.width }, placeables.maxOf { it.height })
radiusPx = (constraints.maxWidth - maxElementSize) / 2 // #5
placeables.forEachIndexed { index, placeable ->
placeable.place( // #6
size / 2 - placeable.width / 2 + posX(index),
size / 2 - placeable.height / 2 + posY(index),
)
}
}
}
}
我使用了一个布局(layout),这样我们可以选择元素的位置。
#1 在时钟中心绘制一个小圆。
#2 在时钟中心和选定的索引之间绘制一条线。
#3 在选定的索引处绘制一个圆。
#4 基于时钟的尺寸计算选定索引圆的半径。
#5 外部圆的半径就是可用空间除以2。
#6 将元素居中放置在它们的位置。
回到TimerPicker
,我们插入Clock元素。aspectRatio
设置为1,因此它始终是一个正方形。
Clock(
time = selectedTime,
modifier = Modifier
.aspectRatio(1f)
.align(Alignment.CenterHorizontally)
) {
ClockMarks24h(selectedPart, selectedTime, onTime)
}
ClockMarks24h
包含了一个24小时制时钟的元素。
@Composable
fun ClockMarks24h(selectedPart: TimePart, selectedTime: Int, onTime: (Int) -> Unit) {
if (selectedPart == TimePart.Hour) {
Mark(text = "00", index = 0, isSelected = selectedTime == 0, onIndex = onTime)
(1..23).map {
Mark(text = it.toString(), index = it, isSelected = selectedTime == it, onIndex = onTime)
}
} else {
Mark(text = "00", index = 0, isSelected = selectedTime == 0, onIndex = onTime)
Mark(text = "05", index = 1, isSelected = selectedTime == 1, onIndex = onTime)
(2..11).map {
Mark(text = (it * 5).toString(), index = it, isSelected = selectedTime == it, onIndex = onTime)
}
}
}
Mark
组件用于表示小时和分钟。它是一个普通的组件,因此可以按照您的喜好进行样式设置。
@Composable
fun Mark(
text: String,
index: Int, // 0..23
onIndex: (Int) -> Unit,
isSelected: Boolean
) {
Text(
text = text,
color = if (isSelected) secondaryColor else Color.White,
modifier = Modifier.clickable(
interactionSource = remember { MutableInteractionSource() },
indication = null,
onClick = { onIndex(index) }
)
)
}
最后,我们可以创建一个对话框组件,并在其中放置TimerPicker
组件。
@Composable
fun TimerPickerDialog() {
val localContext = LocalContext.current
Dialog(onDismissRequest = { /*TODO*/ }) {
Box(modifier = Modifier.fillMaxWidth()) {
TimerPicker(
onOk = { Toast.makeText(localContext, it.toString(), Toast.LENGTH_SHORT).show() },
onCancel = {},
modifier = Modifier
.fillMaxWidth(0.8f)
.align(Alignment.Center)
)
}
}
}
最终效果如下:
GitHub
https://github.com/victorbrndls/BlogProjects/blob/time-picker/app/src/main/java/com/victorbrandalise/timepicker/TimerPicker.kt