You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何使用Jetpack Compose实现自定义日历组件展示功能

Jetpack Compose 自定义日历组件实现方案

目前Jetpack Compose官方暂未提供类似XML体系下的原生日历组件,我们可以通过自定义Composable实现符合需求的日历功能,预期实现效果如下:
示例效果

核心需求

  • 默认展示单月完整日期列表,点击左右箭头可切换前后月份
  • 服务端返回的指定日期使用特殊颜色标记,当前系统日期使用独立样式高亮

实现代码

首先需要引入kotlin datetime库用于日期计算,在模块级build.gradle中添加依赖:

implementation "org.jetbrains.kotlinx:kotlinx-datetime:0.4.0"

核心日历组件代码示例:

import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.aspectRatio
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.grid.GridCells
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.KeyboardArrowLeft
import androidx.compose.material.icons.filled.KeyboardArrowRight
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import kotlinx.datetime.Clock
import kotlinx.datetime.DateTimeUnit
import kotlinx.datetime.DayOfWeek
import kotlinx.datetime.LocalDate
import kotlinx.datetime.plus
import kotlinx.datetime.minus
import kotlinx.datetime.dayOfMonth
import kotlinx.datetime.dayOfWeek
import kotlinx.datetime.month
import kotlinx.datetime.year

// 日历状态类
data class CalendarState(
    val currentDisplayMonth: LocalDate,
    val markedDates: Set<LocalDate>,
    val today: LocalDate = Clock.System.now().let { LocalDate(it, kotlinx.datetime.TimeZone.currentSystemDefault()) }
)

@Composable
fun CustomCalendar(
    markedDates: Set<LocalDate>,
    onDateClick: (LocalDate) -> Unit
) {
    var currentMonth by remember { mutableStateOf(LocalDate(Clock.System.now(), kotlinx.datetime.TimeZone.currentSystemDefault()).let { LocalDate(it.year, it.month, 1) }) }
    val calendarState = remember(currentMonth, markedDates) {
        CalendarState(currentMonth, markedDates)
    }

    Column(modifier = Modifier.fillMaxWidth().padding(16.dp)) {
        // 月份切换栏
        Row(
            modifier = Modifier.fillMaxWidth(),
            horizontalArrangement = Arrangement.SpaceBetween,
            verticalAlignment = Alignment.CenterVertically
        ) {
            IconButton(onClick = { currentMonth = currentMonth.minus(1, DateTimeUnit.MONTH) }) {
                Icon(Icons.Default.KeyboardArrowLeft, contentDescription = "上个月")
            }
            Text(text = "${currentMonth.year}年${currentMonth.month.value}月", fontWeight = FontWeight.Bold)
            IconButton(onClick = { currentMonth = currentMonth.plus(1, DateTimeUnit.MONTH) }) {
                Icon(Icons.Default.KeyboardArrowRight, contentDescription = "下个月")
            }
        }

        // 星期表头
        LazyVerticalGrid(columns = GridCells.Fixed(7), modifier = Modifier.padding(vertical = 8.dp)) {
            items(7) { index ->
                val weekDay = when(index) {
                    0 -> "日"
                    1 -> "一"
                    2 -> "二"
                    3 -> "三"
                    4 -> "四"
                    5 -> "五"
                    else -> "六"
                }
                Box(modifier = Modifier.aspectRatio(1f), contentAlignment = Alignment.Center) {
                    Text(text = weekDay, color = Color.Gray, fontWeight = FontWeight.Medium)
                }
            }
        }

        // 日期网格
        val daysInMonth = currentMonth.lengthOfMonth()
        val firstDayOfMonth = currentMonth.dayOfWeek
        val startOffset = if (firstDayOfMonth == DayOfWeek.SUNDAY) 0 else firstDayOfMonth.value
        LazyVerticalGrid(columns = GridCells.Fixed(7)) {
            // 前补空白
            items(startOffset) {
                Box(modifier = Modifier.aspectRatio(1f))
            }
            // 当月日期
            items(daysInMonth) { dayIndex ->
                val date = LocalDate(currentMonth.year, currentMonth.month, dayIndex + 1)
                val isToday = date == calendarState.today
                val isMarked = date in calendarState.markedDates
                Box(
                    modifier = Modifier
                        .aspectRatio(1f)
                        .padding(4.dp)
                        .background(
                            color = when {
                                isToday -> Color.Blue.copy(alpha = 0.7f)
                                isMarked -> Color.Green.copy(alpha = 0.3f)
                                else -> Color.Transparent
                            },
                            shape = CircleShape
                        )
                        .clickable { onDateClick(date) },
                    contentAlignment = Alignment.Center
                ) {
                    Text(
                        text = (dayIndex + 1).toString(),
                        color = if (isToday) Color.White else Color.Black
                    )
                }
            }
        }
    }
}

// 扩展方法:获取当月天数
fun LocalDate.lengthOfMonth(): Int {
    return when (month.value) {
        1, 3, 5, 7, 8, 10, 12 -> 31
        4, 6, 9, 11 -> 30
        2 -> if (year % 4 == 0 && (year % 100 != 0 || year % 400 == 0)) 29 else 28
        else -> throw IllegalArgumentException("Invalid month")
    }
}

使用方式

直接在需要展示日历的位置调用CustomCalendar组件,传入服务端返回的标记日期集合和日期点击回调即可:

// 示例:模拟服务端返回的标记日期
val markedDates = remember {
    setOf(
        LocalDate(2024, 5, 10),
        LocalDate(2024, 5, 15),
        LocalDate(2024, 5, 20)
    )
}
CustomCalendar(
    markedDates = markedDates,
    onDateClick = { clickedDate ->
        // 处理日期点击逻辑
    }
)

关键逻辑说明

  • 月份切换:通过remember维护当前展示的年月状态,点击左右箭头时增减月份并重新计算当月日期列表
  • 日期布局:先计算当月第一天对应的星期偏移量,在日期网格前补对应数量的空白占位,保证日期和星期表头对应
  • 特殊样式匹配:渲染每个日期时分别和当前系统日期、服务端返回的标记日期集合比对,匹配对应规则则应用指定的背景和文字样式

内容的提问来源于stack exchange,提问作者Jéluchu

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.09.30 00:24:01