Skip to content

Instantly share code, notes, and snippets.

@XanderZhu
Created October 3, 2021 08:20
Show Gist options
  • Star 5 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save XanderZhu/dffbd8daff649b79fba3a4f8a6457160 to your computer and use it in GitHub Desktop.
Save XanderZhu/dffbd8daff649b79fba3a4f8a6457160 to your computer and use it in GitHub Desktop.
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
)
}
}
@ndhblue
Copy link

ndhblue commented Jul 29, 2023

where TileStyle and WeekDay in your code?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment