Compose 日历弹框

官方日期选择效果

Compose Android 日历弹框

  • 查看年月日
  • 查看年月选择年份
  • 左右点击和滑动控制上下月
  • 选中日期后切换重置成未选中

示例代码

 // Decoupled snackbar host state from scaffold state for demo purposes.
    val snackState = remember { SnackbarHostState() }
    val snackScope = rememberCoroutineScope()
    SnackbarHost(hostState = snackState, Modifier)
    val openDialog = remember { mutableStateOf(true) }
// TODO demo how to read the selected date from the state.
    if (openDialog.value) {
        val datePickerState = rememberDatePickerState()
        val confirmEnabled = derivedStateOf { datePickerState.selectedDateMillis != null }
        DatePickerDialog(
            onDismissRequest = {
                // Dismiss the dialog when the user clicks outside the dialog or on the back
                // button. If you want to disable that functionality, simply use an empty
                // onDismissRequest.
                openDialog.value = false
            },
            confirmButton = {
                TextButton(
                    onClick = {
                        openDialog.value = false
                        snackScope.launch {
                            snackState.showSnackbar(
                                "Selected date timestamp: ${datePickerState.selectedDateMillis}"
                            )
                        }
                    },
                    enabled = confirmEnabled.value
                ) {
                    Text("OK")
                }
            },
            dismissButton = {
                TextButton(
                    onClick = {
                        openDialog.value = false
                    }
                ) {
                    Text("Cancel")
                }
            }, modifier = Modifier.padding(16.dp)
        ) {
            DatePicker(state = datePickerState, showModeToggle = false)
        }
    }

官方时间选择效果

Compose Android 日历弹框

  • 上午下午
  • 小时和分钟小时钟表选择样式

示例代码

 // Decoupled snackbar host state from scaffold state for demo purposes.
    val snackState = remember { SnackbarHostState() }
    val snackScope = rememberCoroutineScope()
    SnackbarHost(hostState = snackState, Modifier)
    val openDialog = remember { mutableStateOf(true) }
// TODO demo how to read the selected date from the state.
    if (openDialog.value) {
        val datePickerState = rememberTimePickerState()
        val confirmEnabled = derivedStateOf { datePickerState.hour != null }
        DatePickerDialog(
            onDismissRequest = {
                // Dismiss the dialog when the user clicks outside the dialog or on the back
                // button. If you want to disable that functionality, simply use an empty
                // onDismissRequest.
                openDialog.value = false
            },
            confirmButton = {
                TextButton(
                    onClick = {
                        openDialog.value = false
                        snackScope.launch {
                            snackState.showSnackbar(
                                "Selected times : ${datePickerState.hour}: ${datePickerState.minute}"
                            )
                        }
                    },
                    enabled = confirmEnabled.value
                ) {
                    Text("OK")
                }
            },
            dismissButton = {
                TextButton(
                    onClick = {
                        openDialog.value = false
                    }
                ) {
                    Text("Cancel")
                }
            }, properties = DialogProperties()
        ) {
            TimePicker(
                state = datePickerState, modifier = Modifier
                    .wrapContentSize()
                    .scale(0.8f), layoutType = TimePickerLayoutType.Vertical
            )
        }
    }

自定义时间控件,无法左右滑动切换月份默认选择每月1号,先看效果

Compose Android 日历弹框

  • 定义Dialog
  • 定义canvas绘制顶部星期样式
  • 绘制日期主体嵌套for循环绘制x,y横纵块样式
  • 计算平年闰年每个月日期数,用以填充canvas主体内,Calendar.DAY_OF_WEEK获取每月1号的起始位置
  • 计算canvas居中对其文字效果
  • 从Modifier的 pointerInput>detectTapGestures>onPress>Offset获取点击位置
  • Offset点击位置变更分为有效区域和无效区域,有效添加选中背景,无效维持上一次选中背景
  • 通过方法onSelectedDay将选中项的年月日传递出去
  • ChineseCalendar补充额外农历日期展示
  • 点击年月日更换年份选项组件,LazyVerticalGrid制作年份组件,以当前年份为中点,向前向后推100年,rememberLazyGridState将位置锁定到当前年
  • 年月日交互状态刷

示例代码

@Composable
@OptIn(ExperimentalMaterial3Api::class)
private fun customerDatePicker() {
    var isShow by remember {
        mutableStateOf(true)
    }
    var text by remember {
        mutableStateOf("")
    }
    if (isShow)
        Dialog(
            onDismissRequest = { /*TODO*/ },
        ) {
            Column(
                modifier = Modifier
                    .wrapContentSize()
                    .background(Color.White, RoundedCornerShape(8.dp))
            ) {
                var selDay by remember {
                    mutableStateOf("")
                }
                CalendarItem { year, month, day ->
                    selDay = "$year-${month + 1}-$day"
                }
                Row(
                    Modifier
                        .height(60.dp)
                        .align(Alignment.End)
                ) {
                    TextButton(onClick = {
                        isShow = false
                    }) {
                        Text(text = "取消", color = Color.LightGray)
                    }
                    TextButton(onClick = {
                        isShow = false
                        text = selDay
                    }) {
                        Text(text = "确认")
                    }
                }
            }
        } else
        Text(text = text)
}
@Composable
private fun CalendarItem(onSelectedDay: (Int, Int, Int) -> Unit) {
    Column(Modifier.fillMaxWidth(), horizontalAlignment = Alignment.CenterHorizontally) {
        val currentC = Calendar.getInstance()
        val yearC = currentC.get(Calendar.YEAR)
        val monthC = currentC.get(Calendar.MONTH)
        val dayC = currentC.get(Calendar.DAY_OF_MONTH)
        val scope = rememberCoroutineScope()
        var c by remember {
            mutableStateOf(Calendar.getInstance())
        }
        var day by remember {
            mutableIntStateOf(c.get(Calendar.DAY_OF_MONTH))
        }
        var year by remember {
            mutableIntStateOf(c.get(Calendar.YEAR))
        }
        var month by remember {
            mutableIntStateOf(c.get(Calendar.MONTH))
        }
        var left by remember {
            mutableStateOf(false)
        }
        var right by remember {
            mutableStateOf(false)
        }
        var selectPosition by remember {
            mutableStateOf(Offset(0f, 0f))
        }
        var vail by remember {
            mutableStateOf(true)
        }
        val spaceWidth = 40.dp
        var isYear by remember {
            mutableStateOf(false)
        }
        if (left || right) {
            if (left) {
                c.add(Calendar.MONTH, -1)
            } else {
                c.add(Calendar.MONTH, 1)
            }
            c.set(Calendar.DAY_OF_MONTH, 1)
            selectPosition = Offset(0f, 0f)
            day = c.get(Calendar.DAY_OF_MONTH)
            month = c.get(Calendar.MONTH)
            year = c.get(Calendar.YEAR)
            left = false
            right = false
        }
        val themeColor=MaterialTheme.colorScheme.primary//Color(0xFF2983bb)
        Row(Modifier.height(60.dp), verticalAlignment = Alignment.CenterVertically) {
            Spacer(modifier = Modifier.padding(12.dp))
            TextButton(onClick = {
                isYear = true
            }) {
                Text(
                    text = "$year-${month.plus(1)}-$day",
                    color = themeColor,
                    fontWeight = FontWeight.Bold,
                    textAlign = TextAlign.Left
                )
                Icon(
                    imageVector = Icons.Default.KeyboardArrowDown,
                    contentDescription = "year"
                )
            }
            if (!isYear) {
                Spacer(
                    modifier = Modifier
                        .weight(1f, true)
                )
                Icon(
                    imageVector = Icons.Default.KeyboardArrowLeft,
                    contentDescription = "上月",
                    modifier = Modifier.clickable {
                        left = true
                    })
                Spacer(modifier = Modifier.padding(8.dp))
                Icon(
                    imageVector = Icons.Default.KeyboardArrowRight,
                    contentDescription = "下月",
                    modifier = Modifier.clickable {
                        right = true
                    })
                Spacer(modifier = Modifier.padding(12.dp))
            }
        }
        if (!isYear) {
            val textMeasurer = rememberTextMeasurer()
            Canvas(modifier = Modifier
                .width(spaceWidth.times(7))
                .height(spaceWidth.times(7))
                .pointerInput("fig") {
                    detectTapGestures(
                        onPress = { /* Called when the gesture starts */
                            selectPosition = it
                        }
                    )
                }, onDraw = {
                val padding = 1.dp
                val recSize = Size(width = size.width / 7, height = size.width / 7)
                for (x in 0 until 7) {
                    val text = getWeekStr(x + 2)
                    var result = textMeasurer.measure(
                        AnnotatedString(text), style = TextStyle(
                            color = Color.Gray,
                            fontSize = TextUnit(
                                16f,
                                TextUnitType.Sp
                            ),
                            fontWeight = FontWeight.Bold
                        )
                    )
                    val tw = recSize.width / 2 - result.size.width / 2
                    val th = recSize.height / 2 - result.size.height / 2
                    drawText(
                        result, topLeft = Offset(
                            (recSize.width + padding.value) * x + tw,
                            th
                        )
                    )
                }
                val luC = Calendar.getInstance()
                luC.set(year, month, 1)
                val d = luC.get(Calendar.DAY_OF_WEEK)
                var currentIndexDay = 1
                for (y in 0 until 6) {
                    for (x in 0 until 7) {
                        if (currentIndexDay <= getDays(month + 1, year)) {
                            if ((y == 0 && x >= getWeekSort(d)) || y > 0) {
                                val topLeftRec = Offset(
                                    (recSize.width + padding.value) * x,
                                    recSize.height * y + recSize.height
                                )
                                vail =
                                    selectPosition.x < topLeftRec.x + recSize.width && selectPosition.x > topLeftRec.x
                                            && selectPosition.y < topLeftRec.y + recSize.height && selectPosition.y > topLeftRec.y
                                if (!vail) {
                                    vail = currentIndexDay == day
                                }
                                if (vail) {//选中背景
                                    drawRoundRect(
                                        themeColor,
                                        topLeft = topLeftRec,
                                        size = recSize,
                                        style = Fill,
                                        cornerRadius = CornerRadius(recSize.div(2f).height)
                                    )
                                    day = currentIndexDay
                                    onSelectedDay(year, month, day)
                                }
                                if (currentIndexDay == dayC && month == monthC && year == yearC) {
                                    drawRoundRect(
                                        themeColor,
                                        topLeft = topLeftRec,
                                        size = recSize,
                                        style = Stroke(width = 1.dp.value),
                                        cornerRadius = CornerRadius(recSize.div(2f).height)
                                    )
                                }
                                var result = textMeasurer.measure(
                                    AnnotatedString(currentIndexDay.toString()),
                                    style = TextStyle(
                                        color = if (vail) Color.White else Color.Black,
                                        fontSize = TextUnit(
                                            16f,
                                            TextUnitType.Sp
                                        ),
                                        fontWeight = FontWeight.Medium
                                    )
                                )
                                luC.set(year, month, currentIndexDay)
                                val lunarC = ChineseCalendar(luC.time)
                                val lunarMonth = lunarC.get(ChineseCalendar.MONTH)
                                val lunarDay = lunarC.get(ChineseCalendar.DAY_OF_MONTH)
                                var lunarDayResult = textMeasurer.measure(
                                    AnnotatedString(
                                        if (lunarDay == 1) getLunarMonth(lunarMonth) else getLunarDay(
                                            lunarDay - 1
                                        )
                                    ),
                                    style = TextStyle(
                                        color = if (vail) Color.White else Color.LightGray,
                                        fontSize = TextUnit(
                                            8f,
                                            TextUnitType.Sp
                                        ),
                                        fontWeight = FontWeight.Normal
                                    )
                                )
                                val tw = recSize.width / 2 - result.size.width / 2
                                val th = recSize.height / 2 - result.size.height / 2
                                val tl = Offset(
                                    (recSize.width + padding.value) * x + tw,
                                    recSize.height * y + recSize.height + th
                                )
                                val tll = Offset(
                                    (recSize.width + padding.value) * x + recSize.width / 2 - lunarDayResult.size.width / 2,
                                    tl.y + recSize.height / 2 - lunarDayResult.size.height / 2 + 2.dp.value
                                )
                                drawText(
                                    result, topLeft = tl
                                )
                                drawText(
                                    lunarDayResult, topLeft = tll
                                )
                                currentIndexDay++
                            }
                        }
                    }
                }
            })
        } else {
            val gridState = rememberLazyGridState()
            var currentIt by remember {
                mutableIntStateOf(0)
            }
            LazyVerticalGrid(
                columns = GridCells.Fixed(3),
                state = gridState, modifier = Modifier.height(spaceWidth * 7)
            ) {
                items(200) {
                    var text = if (it < 100) {
                        yearC - 100 + it
                    } else {
                        yearC + it - 100
                    }
                    if (text == yearC) {
                        currentIt = it
                    }
                    TextButton(
                        onClick = {
                            isYear = false
                            scope.launch {
                                year = text
                                c.set(Calendar.YEAR,year)
                            }
                        },
                        border = BorderStroke(
                            1.dp,
                            if (text == yearC) themeColor else Color.Transparent
                        )
                    ) {
                        Text(
                            text = "$text",
                            color = Color.DarkGray,
                            fontWeight = FontWeight.Medium,
                            textAlign = TextAlign.Center
                        )
                    }
                }
            }
            LaunchedEffect(key1 = "HH", block = {
                scope.launch {
                    gridState.scrollToItem(100, 0)
                }
            })
        }
    }
}
private fun getWeekStr(week: Int): String {
    return when (week) {
        2 -> "一"
        3 -> "二"
        4 -> "三"
        5 -> "四"
        6 -> "五"
        7 -> "六"
        else ->
            "日"
    }
}
private fun getWeekSort(week: Int): Int {
    return when (week) {
        1 -> 6
        7 -> 0
        else ->
            week - 2
    }
}
private fun getDays(month: Int, year: Int): Int {
    return when (month) {
        1, 3, 5, 7, 8, 10, 12 -> 31
        4, 6, 9, 11 -> 30
        else -> {//2
            if (isRunYear(year)) 29 else 28
        }
    }
}
private fun isRunYear(year: Int): Boolean {
    return if (year % 400 == 0) {
        true
    } else {
        year % 100 != 0 && year % 4 == 0
    }
}
private fun getLunarDay(index: Int): String {
    val s = "初一、初二、初三、初四、初五、初六、初七、初八、初九、初十"
    val s1 = "十一、十二、十三、十四、十五、十六、十七、十八、十九、二十"
    val s2 = "廿一、廿二、廿三、廿四、廿五、廿六、廿七、廿八、廿九、三十"
    val list = mutableListOf<String>()
    s.split("、").forEach {
        list.add(it)
    }
    s1.split("、").forEach {
        list.add(it)
    }
    s2.split("、").forEach {
        list.add(it)
    }
    if (index == 30) {
        return list[0]
    }
    return list[index]
}
private fun getLunarMonth(index: Int): String {
    return arrayOf(
        "正月",
        "二月",
        "三月",
        "四月",
        "五月",
        "六月",
        "七月",
        "八月",
        "九月",
        "十月",
        "冬月",
        "腊月"
    )[index]
}

功能满足初始需求,具体要根据业务改动,状态场景刷新要多自测