Jetpack Compose Material3 组件之 DatePicker(日期选择)

前言

在之前我使用 Comose 写 APP 的时候,官方还没有给出关于 DatePicker 的解决方案。

当时为了在 Compose 中实现 DatePicker ,大致有两种方案:

一是使用原生 VIew 的 DatePicker,但是因为觉得我即然都用 Compose 了,再去用 VIew ,总觉得怪怪的,所以就没有用这个方案。

二是使用别人写的第三方 DatePicker,我当时采用的就是这个方案。

但是找了一圈,只找到一个相对好用的库,然而这个库是个法国人写,所以对中文的支持不是太好,至于这个不是太好,是什么意思呢?你们看图就知道了:

1

哈哈哈,星期的缩写都是 “星”。

关于这个问题,我也提了 ISSUE,并且详细解释了问题来源以及解决方法,但是作者并没有理我,直至今日都没有修复这个问题。

至于我为什么不自己修复之后提 PR,看其中一个回复:

I’m thinking about ability to inject the functionality from outside if necessary. Default function would be getDisplayName() but it can be overriden by the code similar to the one here. It’s obviously a bug in the Android implementation, so it shouldn’t be fixed by this library.

所以这个问题就这么搁置了。

直到最近,我翻阅 Compose 更新日志时,发现从 Compose Material3 1.1.0 版本开始,新增了 DatePicker DateRangePicker DatePickerDialog 三个组件。

终于,官方出日期选择了,这不得来学学。

基本用法

首先,是最基本的 DatePicker 的使用。

DatePicker 只有一个必须参数 state,用于设置一些配置信息以及获取当前选中的日期。

我们可以通过 rememberDatePickerState 生成 DatePicker 需要的 state

Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
    val datePickerState = rememberDatePickerState()
    DatePicker(state = datePickerState, modifier = Modifier.padding(16.dp))

    Text("当前选中日期的时间戳 ${datePickerState.selectedDateMillis ?: "没有选择"}")
}

效果如下:

2

在这个选择页面中,支持通过点击日期旁边的编辑图标切换至手动输入模式:

3

当然,我们也可以通过设置 rememberDatePickerState 的参数来指定初始化显示日期选择界面还是输入框界面:

val datePickerState = rememberDatePickerState(
    initialDisplayMode = DisplayMode.Picker // 默认显示选择框
    // initialDisplayMode = DisplayMode.Input // 默认显示输入框
)

另外,我们也可以设置默认展示的月份和限制只能选择的年份:

val datePickerState = rememberDatePickerState(
    yearRange = 2023..2024,
    initialDisplayedMonthMillis = 1685577600000 // 注意这里是时间戳
)

如果想要更加自由的限制可以选择的日期,则需要使用 Compose Material3 1.2.0-alpha02 及其以上版本。

在这个版本中提供了一个叫 selectableDates 的参数,可以在其中完全自定义可以选择的日期,这里以官方的 sample 举例,如果我们想限制禁止选择周末,且只能选择2023年以后的日期,那么可以这样写:

val datePickerState = rememberDatePickerState(
        selectableDates = object : SelectableDates {
            // 禁止选择周末(周六和周日)
            override fun isSelectableDate(utcTimeMillis: Long): Boolean {
                return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
                    val dayOfWeek = Instant.ofEpochMilli(utcTimeMillis).atZone(ZoneId.of("UTC"))
                        .toLocalDate().dayOfWeek
                    dayOfWeek != DayOfWeek.SUNDAY && dayOfWeek != DayOfWeek.SATURDAY
                } else {
                    val calendar = Calendar.getInstance(TimeZone.getTimeZone("UTC"))
                    calendar.timeInMillis = utcTimeMillis
                    calendar[Calendar.DAY_OF_WEEK] != Calendar.SUNDAY &&
                            calendar[Calendar.DAY_OF_WEEK] != Calendar.SATURDAY
                }
            }

            // 只允许选择2023年以前
            override fun isSelectableYear(year: Int): Boolean {
                return year > 2022
            }
        }
    )

运行效果如下:

4

可以看到周末是灰色的,不可选中。

点开选择年份时,2023 年以前不可选择:

5

在对话框中使用

上面一节讲的只是基本的使用,但是实际开发过程中,或许还是在 Dialog 中选择日期的场景更多。

所以官方也提供了一个 DatePickerDialog 组件。

其实看 DatePickerDialog 的源码就能看出,它也只是简单封装了一下 AlertDialog:

6

所以实际上使用和 DatePicker 基本没有区别,只是需要额外处理 dialog 的状态,这里依旧以官方 sample 为例:

val openDialog = remember { mutableStateOf(true) }
if (openDialog.value) {
    val datePickerState = rememberDatePickerState()
    val confirmEnabled = derivedStateOf { datePickerState.selectedDateMillis != null }
    DatePickerDialog(
        onDismissRequest = {
            openDialog.value = false
        },
        confirmButton = {
            TextButton(
                onClick = {
                    openDialog.value = false
                    println("选中时间戳为: ${datePickerState.selectedDateMillis}")
                },
                enabled = confirmEnabled.value
            ) {
                Text("确定")
            }
        },
        dismissButton = {
            TextButton(
                onClick = {
                    openDialog.value = false
                }
            ) {
                Text("取消")
            }
        }
    ) {
        DatePicker(state = datePickerState)
    }
}

运行效果如下:

7

日期范围选择

除此之外,在 MD3 新的 API 中还提供了一个可以选择日期范围的函数 DateRangePicker

它的参数与 DatePicker 类似,只是 state 变为了 DateRangePickerState

我们可以通过 rememberDateRangePickerState 生成一个 state

state 中,我们可以设置时间选择器的初始化展示模式(initialDisplayMode)、默认起始日期(initialSelectedStartDateMillis)、默认结束日期(initialSelectedEndDateMillis)、默认展示日期(initialDisplayedMonthMillis)、允许选择的年份(yearRange)。

并且,同样的,在 Compose Material3 1.2.0-alpha02 及其以上版本还支持完全自定义可以选择的日期 selectableDates

该函数的显示效果如下:

val state = rememberDateRangePickerState()
DateRangePicker(state = state, modifier = Modifier.fillMaxSize())

8

获取选中的值依旧是通过 sate:

println("选择的时间戳范围: ${state.selectedStartDateMillis}..${state.selectedEndDateMillis}")

总结

本文只是简要介绍了关于 Compsoe Material3 中关于日期选择的基本使用方法,更多的使用方法还需要读者自行探索。

可以看到,Compose 的官方组件已经越来越多,越来越趋向于成熟。

相较于正式版刚发布没多久时的什么东西都没有,什么都需要自己造轮子的状态,现在几乎已经涵盖了我们开发中常用到的各种控件和需求了。