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]
}
功能满足初始需求,具体要根据业务改动,状态场景刷新要多自测