Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?
Jetpack compose calendar.
@OptIn(ExperimentalPagerApi::class)
@Composable
fun Calendar(
startDate: LocalDate,
months: List<YearMonth>,
selectedDates: Set<LocalDate>,
focusedDate: LocalDate?,
isNewDateSelectionEnabled: Boolean,
onDateClick: (LocalDate) -> Unit,
horizontalPadding: Dp,
modifier: Modifier = Modifier,
monthsNamesStyle: TextStyle = remember { TextStyle.FULL_STANDALONE },
locale: Locale = remember { Locale.getDefault() }
) {
val monthNames = remember {
months.map { yearMonth ->
Month.of(yearMonth.monthValue)
.getDisplayName(monthsNamesStyle, locale)
}
}
val pagerState = rememberPagerState(pageCount = months.count(), initialOffscreenLimit = 3)
val coroutinesScope = rememberCoroutineScope()
Column(modifier = modifier) {
ScrollableTabRow(
selectedTabIndex = pagerState.currentPage,
edgePadding = horizontalPadding
) {
monthNames.forEachIndexed { index, month ->
Tab(
selected = pagerState.currentPage == index,
onClick = {
coroutinesScope.launch {
pagerState.animateScrollToPage(index)
}
},
text = {
Text(text = month)
}
)
}
}
HorizontalPager(
state = pagerState,
verticalAlignment = Alignment.Top,
modifier = Modifier.fillMaxWidth()
) { index ->
CalendarMonth(
state = CalendarMonthState(
month = months[index],
firstEnabledDate = startDate,
selectedDates = selectedDates,
isNewDateSelectionEnabled = isNewDateSelectionEnabled,
focusedDate = focusedDate,
),
onDateClick = onDateClick,
locale = locale,
horizontalPadding = horizontalPadding,
modifier = Modifier.fillMaxWidth()
)
}
}
}
data class CalendarMonthState(
val month: YearMonth,
val firstEnabledDate: LocalDate,
val isNewDateSelectionEnabled: Boolean,
val selectedDates: Set<LocalDate>,
val focusedDate: LocalDate?
)
@Composable
fun CalendarMonth(
state: CalendarMonthState,
onDateClick: (LocalDate) -> Unit,
locale: Locale,
modifier : Modifier = Modifier,
horizontalPadding: Dp = remember { 16.dp },
) {
val weekDayNames = remember {
DayOfWeek.values().map { it.getDisplayName(TextStyle.SHORT, locale) }
}
val currentMonthDays: Array<LocalDate> = remember {
Array(state.month.lengthOfMonth()) { index ->
LocalDate.of(state.month.year, state.month.month, index + 1)
}
}
val previousMonthDays = remember {
val currentMonthFirstDay: LocalDate = currentMonthDays.first()
val dayNumber = currentMonthFirstDay.dayOfWeek.value - 1
Array(dayNumber) { i ->
currentMonthFirstDay.minusDays(
(dayNumber - i).toLong()
)
}
}
val bottomPadding = remember { 16.dp }
Box(modifier = modifier) {
Grid(
columnsCount = 7,
modifier = Modifier
.padding(bottom = bottomPadding, start = horizontalPadding, end = horizontalPadding)
.align(Alignment.Center)
) {
weekDayNames.forEach { weekDay ->
WeekDay(text = weekDay, Modifier.padding(4.dp))
}
previousMonthDays.forEach { date ->
Tile(
text = date.dayOfMonth.toString(),
style = TileStyle.GREY_OUT,
modifier = Modifier.padding(4.dp)
)
}
currentMonthDays.forEach { date ->
val style = when {
date.isBefore(state.firstEnabledDate) -> {
TileStyle.GREY_OUT
}
date == state.focusedDate -> TileStyle.BLUE_FILLED
date in state.selectedDates -> TileStyle.BLUE_STROKED
state.isNewDateSelectionEnabled -> TileStyle.NORMAL
else -> TileStyle.GREY_OUT
}
Tile(
text = date.dayOfMonth.toString(),
style = style,
modifier = Modifier
.padding(4.dp)
.let { m ->
if (style != TileStyle.GREY_OUT) {
m.clickable {
onDateClick(date)
}
} else {
m
}
}
)
}
}
}
}
@Composable
fun Grid(
columnsCount: Int,
modifier: Modifier = Modifier,
content: @Composable () -> Unit
) {
require(columnsCount > 0) {
"Columns count should be a positive number!"
}
Layout(
content = content,
modifier = modifier
) { measurables, constraints ->
val rowsCount = (measurables.count() / columnsCount) + 1
// Keep track of height of each row
val rowHeights = IntArray(rowsCount) { 0 }
// Keep track of the width of each row
val rowWidths = IntArray(rowsCount) { 0 }
// Keep track of the max width of each column
val columnWidths = IntArray(columnsCount) { 0 }
val placeables = measurables.mapIndexed { index, measurable ->
val placeable = measurable.measure(constraints)
val rowIndex = index / columnsCount
rowHeights[rowIndex] = maxOf(placeable.height, rowHeights[rowIndex])
rowWidths[rowIndex] += placeable.width
val columnIndex = index % columnsCount
columnWidths[columnIndex] = maxOf(placeable.width, columnWidths[columnIndex])
placeable
}
// Grid's height is the sum of the tallest element of each row
// coerced to the height constraints
val height = rowHeights.sum()
.coerceIn(constraints.minHeight..constraints.maxHeight)
// Grid's width is the widest row
val width = rowWidths.maxOrNull()
?.coerceIn(constraints.minWidth.rangeTo(constraints.maxWidth)) ?: constraints.minWidth
// Y of each row, based on the height accumulation of previous rows
val rowY = IntArray(rowsCount) { 0 }
for (i in 1 until rowsCount) {
rowY[i] = rowY[i-1] + rowHeights[i-1]
}
layout(width = width, height = height) {
// x cord we have placed up to, per row
val rowX = IntArray(rowsCount) { 0 }
placeables.forEachIndexed { index, placeable ->
val rowIndex = index / columnsCount
val columnIndex = index % columnsCount
val maxColumnWidth = columnWidths[columnIndex]
//Compute x to place item center horizontally in the column
val x = if(placeable.width < maxColumnWidth) {
val widthDelta = maxColumnWidth - placeable.width
val sideOffset = widthDelta / 2
rowX[rowIndex] + sideOffset
} else {
rowX[rowIndex]
}
placeable.placeRelative(
x = x,
y = rowY[rowIndex]
)
rowX[rowIndex] += maxColumnWidth
}
}
}
}
@Composable
fun Tile(
text: String,
backgroundColor: Color,
textColor: Color,
borderStroke: BorderStroke?,
elevation: Dp,
modifier: Modifier = Modifier
) {
Card(
shape = RoundedCornerShape(8.dp),
elevation = elevation,
border = borderStroke,
backgroundColor = backgroundColor,
modifier = modifier.size(40.dp)
) {
Text(
text = text,
modifier = Modifier.wrapContentSize(),
fontWeight = FontWeight.Bold,
fontSize = 13.sp,
color = textColor
)
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment