Compose Android 日历弹框

Compose 日历弹框

官方日期选择效果

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

示例代码

ini 复制代码
 // 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)
        }
    }

官方时间选择效果

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

示例代码

ini 复制代码
 // 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号,先看效果

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

示例代码

ini 复制代码
@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]
}

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

相关推荐
万能的小裴同学32 分钟前
Android M3U8视频播放器
android·音视频
q***57741 小时前
MySql的慢查询(慢日志)
android·mysql·adb
JavaNoober1 小时前
Android 前台服务 "Bad Notification" 崩溃机制分析文档
android
城东米粉儿2 小时前
关于ObjectAnimator
android
zhangphil3 小时前
Android渲染线程Render Thread的RenderNode与DisplayList,引用Bitmap及Open GL纹理上传GPU
android
火柴就是我4 小时前
从头写一个自己的app
android·前端·flutter
lichong9515 小时前
XLog debug 开启打印日志,release 关闭打印日志
android·java·前端
用户69371750013846 小时前
14.Kotlin 类:类的形态(一):抽象类 (Abstract Class)
android·后端·kotlin
火柴就是我6 小时前
NekoBoxForAndroid 编译libcore.aar
android
Kaede67 小时前
MySQL中如何使用命令行修改root密码
android·mysql·adb