使用 Android Jetpack Compose构建time picker

文章展示了如何使用JetpackCompose构建一个可复用的时间选择器组件,包括小时和分钟选择,并详细解释了组件的各个部分,如TimePicker、TimeCard和Clock组件的实现。此外,还提到了如何在Dialog中使用这个时间选择器。
摘要由CSDN通过智能技术生成

展示如何使用 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

转自:你知道如何使用 Android Jetpack Compose构建time picker吗

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值