Last active
December 28, 2024 18:04
-
-
Save pbk20191/0ccd84dca5c4dc2b47b2e9c0d6aa7702 to your computer and use it in GitHub Desktop.
Calendar Grid style layout for Compose
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
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()) | |
} | |
} | |
} | |
} | |
} | |
) | |
} |
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
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