Created
October 3, 2021 08:20
-
-
Save XanderZhu/dffbd8daff649b79fba3a4f8a6457160 to your computer and use it in GitHub Desktop.
Jetpack compose calendar.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
@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() | |
) | |
} | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | |
} | |
} | |
) | |
} | |
} | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
@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 | |
} | |
} | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
@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
where TileStyle and WeekDay in your code?