Skip to content

Instantly share code, notes, and snippets.

@pbk20191
Last active December 28, 2024 18:04
Show Gist options
  • Save pbk20191/0ccd84dca5c4dc2b47b2e9c0d6aa7702 to your computer and use it in GitHub Desktop.
Save pbk20191/0ccd84dca5c4dc2b47b2e9c0d6aa7702 to your computer and use it in GitHub Desktop.
Calendar Grid style layout for Compose
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.layout.SubcomposeLayout
import androidx.compose.ui.layout.SubcomposeLayoutState
import androidx.compose.ui.layout.SubcomposeSlotReusePolicy
import androidx.compose.ui.unit.*
import java.time.LocalDate
/**
Calendar Grid style layout
each cell will have same size, the biggest intrinsic size that can be determined.
each width is no bigger than 1/7 of container available width.
each height is no bigger than 1/6 of container available height.
It is not a lazy layout, so it's safe to be use inside scroll container
*/
@Composable
fun CalendarGrid(
month: CalendarMonthState,
modifier: Modifier = Modifier,
content: @Composable (LocalDate) -> Unit
) {
val currentState by rememberUpdatedState(month)
val layoutState = remember {
// 항상 7*6 = 42개의 slot이 필요
SubcomposeLayoutState(SubcomposeSlotReusePolicy(maxSlotsToRetainForReuse = 7 * 6))
}
val intervalList: Collection<LocalDate> by remember {
derivedStateOf(structuralEqualityPolicy()) {
currentState.gridSequence.toList()
}
}
CalendarGridLayout(intervalList, layoutState, modifier, content)
}
// This function is stateless so it can be stable
@Stable
@Composable
private fun CalendarGridLayout(
intervalList: Collection<LocalDate>,
layoutState: SubcomposeLayoutState,
modifier: Modifier = Modifier,
content: @Composable (LocalDate) -> Unit
) {
SubcomposeLayout(
state = layoutState,
modifier = modifier,
measurePolicy = { containerConstraints ->
val contentPadding = PaddingValues(0.dp)
val startPadding = contentPadding.calculateLeftPadding(layoutDirection).roundToPx()
val endPadding = contentPadding.calculateRightPadding(layoutDirection).roundToPx()
val topPadding = contentPadding.calculateTopPadding().roundToPx()
val bottomPadding = contentPadding.calculateBottomPadding().roundToPx()
val totalVerticalPadding = topPadding + bottomPadding
val totalHorizontalPadding = startPadding + endPadding
val itemMaxWidth = if (containerConstraints.hasBoundedWidth) {
// container가 width, maxWidth 등으로 이미 width가 정해진 경우, 또는 normal case
val estimatedWidth = (containerConstraints.maxWidth / 7) - totalHorizontalPadding
estimatedWidth.coerceAtLeast(0)
} else {
// explicit한 제약 조건 없음 e.g) LazyRow
Constraints.Infinity
}
val itemMaxHeight = if (containerConstraints.hasBoundedHeight) {
// container가 height, maxHeight 등으로 이미 height가 정해진 경우, 또는 Normal Case
val estimatedHeight =
containerConstraints.maxHeight / ((intervalList.size / 7) + 1) - totalVerticalPadding
estimatedHeight.coerceAtLeast(0)
} else {
// explicit한 제약 없음 e.g) LazyColumn
Constraints.Infinity
}
// 대응하는 제약조건 추가 => container 사이즈가 존재한다면 width/7, height /6 이 최대 크기가 되도록 제약
val itemConstraints = containerConstraints
.copy(minWidth = 0, minHeight = 0)
.constrain(Constraints(maxWidth = itemMaxWidth, maxHeight = itemMaxHeight))
val measureablesCollection = intervalList.mapIndexed{ index, date ->
subcompose(index) {
content(date)
}
}
val grids = measureablesCollection.map { measurables ->
measurables.map{
it.measure(itemConstraints)
}
}
val flattenPlaceable = grids.flatten()
// cell 중 가장 width 큰거
val maxWidth = flattenPlaceable.maxByOrNull { it.measuredWidth }?.width ?: return@SubcomposeLayout layout(0,0) {}
// cell 중 가장 height 큰거
val maxHeight = flattenPlaceable.maxByOrNull { it.measuredHeight }?.height ?: return@SubcomposeLayout layout(0,0) {}
// 일주일 단위로 컷
val chunkedGrid = grids.chunked(7)
// 전체 container width 계산
val occupiedWidth: Int = if (maxWidth == Constraints.Infinity) {
Constraints.Infinity
} else {
containerConstraints.constrainWidth((maxWidth + totalHorizontalPadding) * 7)
}
// 전체 container height 계산
val occupiedHeight: Int = if (maxHeight == Constraints.Infinity) {
Constraints.Infinity
} else {
containerConstraints.constrainHeight(chunkedGrid.size * (maxHeight + totalVerticalPadding))
}
layout(
width = occupiedWidth,
height = occupiedHeight,
alignmentLines = emptyMap()
) {
// 한줄당 7칸 총 6줄 배치
chunkedGrid.forEachIndexed { week, weekPlaceables ->
val height = week * maxHeight
weekPlaceables.forEachIndexed { day, placeables ->
placeables.forEachIndexed { zIndex, placeable ->
placeable.placeRelative(day * (maxWidth + startPadding) + startPadding, height + topPadding + week * topPadding, zIndex.toFloat())
}
}
}
}
}
)
}
import android.os.Parcelable
import androidx.compose.runtime.Immutable
import kotlinx.parcelize.Parcelize
import java.time.LocalDate
import java.time.YearMonth
import java.time.temporal.WeekFields
@Parcelize
@Immutable
data class CalendarMonthState(
val yearMonth: YearMonth = YearMonth.now(),
val weekFields: WeekFields = WeekFields.SUNDAY_START
): Parcelable {
// yearMonth 년월의 첫번째 주의 일요일부터 마지막 주의 토요일까지( weekFields에 따라 요일은 달라질 수 있음 )
val gridSequence:Sequence<LocalDate> get() {
val start = yearMonth.atDay(1).with(weekFields.dayOfWeek(), 1)
val end = yearMonth.atEndOfMonth().with(weekFields.dayOfWeek(), 7)
return generateSequence(start) {
if (it < end) {
it.plusDays(1)
} else {
null
}
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment