From 6a03b87d17b91271e32d1a2e19e9e1783f4c5d87 Mon Sep 17 00:00:00 2001 From: Kizito Nwose Date: Sat, 18 Jan 2025 22:44:53 +0100 Subject: [PATCH] Derive day coordinates from item root layout --- .../kizitonwose/calendar/compose/Calendar.kt | 26 +- .../calendar/compose/CalendarMonths.kt | 142 +++++++--- .../calendar/compose/ItemPlacementInfo.kt | 72 ++--- .../compose/weekcalendar/WeekCalendar.kt | 23 +- .../yearcalendar/YearCalendarMonths.kt | 246 ++++++++++-------- .../yearcalendar/YearItemPlacementInfo.kt | 68 ++--- .../kizitonwose/calendar/compose/Calendar.kt | 26 +- .../calendar/compose/CalendarMonths.kt | 142 +++++++--- .../calendar/compose/ItemPlacementInfo.kt | 72 ++--- .../compose/weekcalendar/WeekCalendar.kt | 23 +- .../yearcalendar/YearCalendarMonths.kt | 246 ++++++++++-------- .../yearcalendar/YearItemPlacementInfo.kt | 68 ++--- 12 files changed, 626 insertions(+), 528 deletions(-) diff --git a/compose-multiplatform/library/src/commonMain/kotlin/com/kizitonwose/calendar/compose/Calendar.kt b/compose-multiplatform/library/src/commonMain/kotlin/com/kizitonwose/calendar/compose/Calendar.kt index a0be32aa..e9f2d804 100644 --- a/compose-multiplatform/library/src/commonMain/kotlin/com/kizitonwose/calendar/compose/Calendar.kt +++ b/compose-multiplatform/library/src/commonMain/kotlin/com/kizitonwose/calendar/compose/Calendar.kt @@ -10,11 +10,9 @@ import androidx.compose.foundation.lazy.LazyRow import androidx.compose.runtime.Composable import androidx.compose.runtime.DisposableEffect import androidx.compose.ui.Modifier -import androidx.compose.ui.layout.onPlaced import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp -import androidx.compose.ui.util.fastRoundToInt import com.kizitonwose.calendar.compose.CalendarDefaults.flingBehavior import com.kizitonwose.calendar.compose.heatmapcalendar.HeatMapCalendarImpl import com.kizitonwose.calendar.compose.heatmapcalendar.HeatMapCalendarState @@ -180,7 +178,7 @@ private fun Calendar( ) { if (isHorizontal) { LazyRow( - modifier = modifier.onPlaced(state.placementInfo::onCalendarPlaced), + modifier = modifier, state = state.listState, flingBehavior = flingBehavior(calendarScrollPaged, state.listState), userScrollEnabled = userScrollEnabled, @@ -196,12 +194,12 @@ private fun Calendar( monthBody = monthBody, monthFooter = monthFooter, monthContainer = monthContainer, - onFirstDayPlaced = state.placementInfo::onFirstDayPlaced, + onItemPlaced = state.placementInfo::onItemPlaced, ) } } else { LazyColumn( - modifier = modifier.onPlaced(state.placementInfo::onCalendarPlaced), + modifier = modifier, state = state.listState, flingBehavior = flingBehavior(calendarScrollPaged, state.listState), userScrollEnabled = userScrollEnabled, @@ -217,7 +215,7 @@ private fun Calendar( monthBody = monthBody, monthFooter = monthFooter, monthContainer = monthContainer, - onFirstDayPlaced = state.placementInfo::onFirstDayPlaced, + onItemPlaced = state.placementInfo::onItemPlaced, ) } } @@ -257,7 +255,7 @@ public fun WeekCalendar( weekHeader: (@Composable ColumnScope.(Week) -> Unit)? = null, weekFooter: (@Composable ColumnScope.(Week) -> Unit)? = null, ): Unit = WeekCalendarImpl( - modifier = modifier.onPlaced(state.placementInfo::onCalendarPlaced), + modifier = modifier, state = state, calendarScrollPaged = calendarScrollPaged, userScrollEnabled = userScrollEnabled, @@ -266,7 +264,7 @@ public fun WeekCalendar( weekHeader = weekHeader, weekFooter = weekFooter, contentPadding = contentPadding, - onFirstDayPlaced = state.placementInfo::onFirstDayPlaced, + onItemPlaced = state.placementInfo::onItemPlaced, ) /** @@ -550,14 +548,14 @@ private fun YearCalendar( // Intentionally not creating a coroutine scope with LaunchedEffect DisposableEffect(monthVerticalSpacing, monthHorizontalSpacing) { with(density) { - state.placementInfo.monthVerticalSpacingPx = monthVerticalSpacing.toPx().fastRoundToInt() - state.placementInfo.monthHorizontalSpacingPx = monthHorizontalSpacing.toPx().fastRoundToInt() + state.placementInfo.monthVerticalSpacingPx = monthVerticalSpacing.roundToPx() + state.placementInfo.monthHorizontalSpacingPx = monthHorizontalSpacing.roundToPx() } onDispose {} } if (isHorizontal) { LazyRow( - modifier = modifier.onPlaced(state.placementInfo::onCalendarPlaced), + modifier = modifier, state = state.listState, flingBehavior = flingBehavior(calendarScrollPaged, state.listState), userScrollEnabled = userScrollEnabled, @@ -582,12 +580,12 @@ private fun YearCalendar( yearBody = yearBody, yearFooter = yearFooter, yearContainer = yearContainer, - onFirstMonthAndDayPlaced = state.placementInfo::onFirstMonthAndDayPlaced, + onItemPlaced = state.placementInfo::onItemPlaced, ) } } else { LazyColumn( - modifier = modifier.onPlaced(state.placementInfo::onCalendarPlaced), + modifier = modifier, state = state.listState, flingBehavior = flingBehavior(calendarScrollPaged, state.listState), userScrollEnabled = userScrollEnabled, @@ -612,7 +610,7 @@ private fun YearCalendar( yearBody = yearBody, yearFooter = yearFooter, yearContainer = yearContainer, - onFirstMonthAndDayPlaced = state.placementInfo::onFirstMonthAndDayPlaced, + onItemPlaced = state.placementInfo::onItemPlaced, ) } } diff --git a/compose-multiplatform/library/src/commonMain/kotlin/com/kizitonwose/calendar/compose/CalendarMonths.kt b/compose-multiplatform/library/src/commonMain/kotlin/com/kizitonwose/calendar/compose/CalendarMonths.kt index ce77e868..02a08193 100644 --- a/compose-multiplatform/library/src/commonMain/kotlin/com/kizitonwose/calendar/compose/CalendarMonths.kt +++ b/compose-multiplatform/library/src/commonMain/kotlin/com/kizitonwose/calendar/compose/CalendarMonths.kt @@ -11,6 +11,10 @@ import androidx.compose.foundation.layout.wrapContentHeight import androidx.compose.foundation.lazy.LazyItemScope import androidx.compose.foundation.lazy.LazyListScope import androidx.compose.runtime.Composable +import androidx.compose.runtime.Stable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberUpdatedState import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clipToBounds import androidx.compose.ui.layout.LayoutCoordinates @@ -29,7 +33,7 @@ internal fun LazyListScope.CalendarMonths( monthBody: (@Composable ColumnScope.(CalendarMonth, content: @Composable () -> Unit) -> Unit)?, monthFooter: (@Composable ColumnScope.(CalendarMonth) -> Unit)?, monthContainer: (@Composable LazyItemScope.(CalendarMonth, container: @Composable () -> Unit) -> Unit)?, - onFirstDayPlaced: (coordinates: LayoutCoordinates) -> Unit, + onItemPlaced: (itemCoordinates: ItemCoordinates) -> Unit, ) { items( count = monthCount, @@ -40,57 +44,117 @@ internal fun LazyListScope.CalendarMonths( ContentHeightMode.Wrap -> false ContentHeightMode.Fill -> true } - val hasContainer = monthContainer != null - monthContainer.or(defaultMonthContainer)(month) { - Column( - modifier = Modifier - .then(if (hasContainer) Modifier.fillMaxWidth() else Modifier.fillParentMaxWidth()) - .then( - if (fillHeight) { - if (hasContainer) Modifier.fillMaxHeight() else Modifier.fillParentMaxHeight() - } else { - Modifier.wrapContentHeight() - }, - ), - ) { - monthHeader?.invoke(this, month) - monthBody.or(defaultMonthBody)(month) { - Column( - modifier = Modifier - .fillMaxWidth() - .then(if (fillHeight) Modifier.weight(1f) else Modifier.wrapContentHeight()), - ) { - for (week in month.weekDays) { - Row( - modifier = Modifier - .fillMaxWidth() - .then(if (fillHeight) Modifier.weight(1f) else Modifier.wrapContentHeight()), - ) { - for (day in week) { - Box( - modifier = Modifier - .weight(1f) - .clipToBounds() - .onFirstDayPlaced(day, month, onFirstDayPlaced), - ) { - dayContent(day) + val hasMonthContainer = monthContainer != null + val currentOnItemPlaced by rememberUpdatedState(onItemPlaced) + val itemCoordinatesStore = remember(month.yearMonth) { + ItemCoordinatesStore(currentOnItemPlaced) + } + Box(Modifier.onPlaced(itemCoordinatesStore::onItemRootPlaced)) { + monthContainer.or(defaultMonthContainer)(month) { + Column( + modifier = Modifier + .then( + if (hasMonthContainer) { + Modifier.fillMaxWidth() + } else { + Modifier.fillParentMaxWidth() + }, + ) + .then( + if (fillHeight) { + if (hasMonthContainer) { + Modifier.fillMaxHeight() + } else { + Modifier.fillParentMaxHeight() + } + } else { + Modifier.wrapContentHeight() + }, + ), + ) { + monthHeader?.invoke(this, month) + monthBody.or(defaultMonthBody)(month) { + Column( + modifier = Modifier + .fillMaxWidth() + .then( + if (fillHeight) { + Modifier.weight(1f) + } else { + Modifier.wrapContentHeight() + }, + ), + ) { + for ((row, week) in month.weekDays.withIndex()) { + Row( + modifier = Modifier + .fillMaxWidth() + .then( + if (fillHeight) { + Modifier.weight(1f) + } else { + Modifier.wrapContentHeight() + }, + ), + ) { + for ((column, day) in week.withIndex()) { + Box( + modifier = Modifier + .weight(1f) + .clipToBounds() + .onFirstDayPlaced( + dateRow = row, + dateColumn = column, + onFirstDayPlaced = itemCoordinatesStore::onFirstDayPlaced, + ), + ) { + dayContent(day) + } } } } } } + monthFooter?.invoke(this, month) } - monthFooter?.invoke(this, month) } } } } +@Stable +internal class ItemCoordinatesStore( + private val onItemPlaced: (itemCoordinates: ItemCoordinates) -> Unit, +) { + private var itemRootCoordinates: LayoutCoordinates? = null + private var firstDayCoordinates: LayoutCoordinates? = null + + fun onItemRootPlaced(coordinates: LayoutCoordinates) { + itemRootCoordinates = coordinates + check() + } + + fun onFirstDayPlaced(coordinates: LayoutCoordinates) { + firstDayCoordinates = coordinates + check() + } + + private fun check() { + val itemRootCoordinates = itemRootCoordinates ?: return + val firstDayCoordinates = firstDayCoordinates ?: return + val itemCoordinates = ItemCoordinates( + itemRootCoordinates = itemRootCoordinates, + firstDayCoordinates = firstDayCoordinates, + ) + onItemPlaced(itemCoordinates) + } +} + private inline fun Modifier.onFirstDayPlaced( - day: CalendarDay, - month: CalendarMonth, + dateRow: Int, + dateColumn: Int, noinline onFirstDayPlaced: (coordinates: LayoutCoordinates) -> Unit, -) = if (day == month.weekDays.first().first()) { +) = if (dateRow == 0 && dateColumn == 0) { onPlaced(onFirstDayPlaced) } else { this diff --git a/compose-multiplatform/library/src/commonMain/kotlin/com/kizitonwose/calendar/compose/ItemPlacementInfo.kt b/compose-multiplatform/library/src/commonMain/kotlin/com/kizitonwose/calendar/compose/ItemPlacementInfo.kt index 38890cc0..41616bdd 100644 --- a/compose-multiplatform/library/src/commonMain/kotlin/com/kizitonwose/calendar/compose/ItemPlacementInfo.kt +++ b/compose-multiplatform/library/src/commonMain/kotlin/com/kizitonwose/calendar/compose/ItemPlacementInfo.kt @@ -1,58 +1,48 @@ package com.kizitonwose.calendar.compose import androidx.compose.foundation.gestures.Orientation +import androidx.compose.runtime.Immutable import androidx.compose.runtime.Stable import androidx.compose.runtime.withFrameNanos -import androidx.compose.ui.geometry.Offset import androidx.compose.ui.layout.LayoutCoordinates -import androidx.compose.ui.layout.positionInWindow import androidx.compose.ui.unit.round import kotlinx.coroutines.isActive import kotlin.coroutines.coroutineContext +@Immutable +internal data class ItemCoordinates( + val itemRootCoordinates: LayoutCoordinates, + val firstDayCoordinates: LayoutCoordinates, +) + @Stable internal class ItemPlacementInfo { - private var calendarCoordinates: LayoutCoordinates? = null - private var firstDayCoordinates: LayoutCoordinates? = null - - fun onCalendarPlaced(coordinates: LayoutCoordinates) { - calendarCoordinates = coordinates - } + private var itemCoordinates: ItemCoordinates? = null - fun onFirstDayPlaced(coordinates: LayoutCoordinates) { - firstDayCoordinates = coordinates + fun onItemPlaced(itemCoordinates: ItemCoordinates) { + this.itemCoordinates = itemCoordinates } - suspend fun awaitFistDayOffsetAndSize(orientation: Orientation): DayOffsetSize? { - var calendarCoord: LayoutCoordinates? = null - var firstDayCoord: LayoutCoordinates? = null - while (coroutineContext.isActive && - (calendarCoord == null || firstDayCoord == null) - ) { - calendarCoord = calendarCoordinates - firstDayCoord = firstDayCoordinates - if (calendarCoord == null || firstDayCoord == null) { - withFrameNanos {} - } + suspend fun awaitFistDayOffsetAndSize(orientation: Orientation): OffsetSize? { + var itemCoordinates = this.itemCoordinates + while (coroutineContext.isActive && itemCoordinates == null) { + withFrameNanos {} + itemCoordinates = this.itemCoordinates } - if (calendarCoord == null || - firstDayCoord == null || - !calendarCoord.isAttached || - !firstDayCoord.isAttached - ) { + if (itemCoordinates == null) { return null } - val itemViewCoord = findItemViewCoordinates(firstDayCoord, calendarCoord) - val daySize = firstDayCoord.size - val dayOffset = itemViewCoord.localPositionOf(firstDayCoord, Offset.Zero).round() + val (itemRootCoordinates, firstDayCoordinates) = itemCoordinates + val daySize = firstDayCoordinates.size + val dayOffset = itemRootCoordinates.localPositionOf(firstDayCoordinates).round() return when (orientation) { - Orientation.Vertical -> DayOffsetSize( + Orientation.Vertical -> OffsetSize( offset = dayOffset.y, size = daySize.height, ) Orientation.Horizontal -> { - DayOffsetSize( + OffsetSize( offset = dayOffset.x, size = daySize.width, ) @@ -60,25 +50,9 @@ internal class ItemPlacementInfo { } } - internal data class DayOffsetSize( + @Immutable + internal data class OffsetSize( val offset: Int, val size: Int, ) } - -internal fun findItemViewCoordinates( - firstDayCoord: LayoutCoordinates, - calendarCoord: LayoutCoordinates, -): LayoutCoordinates { - var itemViewCoord = firstDayCoord - var parent = itemViewCoord.parentLayoutCoordinates - // Find the coordinates that match the index item layout - while (parent != null && - parent.size != calendarCoord.size && - parent.positionInWindow() != calendarCoord.positionInWindow() - ) { - itemViewCoord = parent - parent = itemViewCoord.parentLayoutCoordinates - } - return itemViewCoord -} diff --git a/compose-multiplatform/library/src/commonMain/kotlin/com/kizitonwose/calendar/compose/weekcalendar/WeekCalendar.kt b/compose-multiplatform/library/src/commonMain/kotlin/com/kizitonwose/calendar/compose/weekcalendar/WeekCalendar.kt index 480764d6..b4469c24 100644 --- a/compose-multiplatform/library/src/commonMain/kotlin/com/kizitonwose/calendar/compose/weekcalendar/WeekCalendar.kt +++ b/compose-multiplatform/library/src/commonMain/kotlin/com/kizitonwose/calendar/compose/weekcalendar/WeekCalendar.kt @@ -10,11 +10,16 @@ import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.width import androidx.compose.foundation.lazy.LazyRow import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberUpdatedState import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clipToBounds import androidx.compose.ui.layout.LayoutCoordinates import androidx.compose.ui.layout.onPlaced import com.kizitonwose.calendar.compose.CalendarDefaults.flingBehavior +import com.kizitonwose.calendar.compose.ItemCoordinates +import com.kizitonwose.calendar.compose.ItemCoordinatesStore import com.kizitonwose.calendar.core.Week import com.kizitonwose.calendar.core.WeekDay import com.kizitonwose.calendar.core.format.toIso8601String @@ -30,7 +35,7 @@ internal fun WeekCalendarImpl( dayContent: @Composable BoxScope.(WeekDay) -> Unit, weekHeader: (@Composable ColumnScope.(Week) -> Unit)? = null, weekFooter: (@Composable ColumnScope.(Week) -> Unit)? = null, - onFirstDayPlaced: (coordinates: LayoutCoordinates) -> Unit, + onItemPlaced: (itemCoordinates: ItemCoordinates) -> Unit, ) { LazyRow( modifier = modifier, @@ -45,6 +50,10 @@ internal fun WeekCalendarImpl( key = { offset -> state.store[offset].days.first().date.toIso8601String() }, ) { offset -> val week = state.store[offset] + val currentOnItemPlaced by rememberUpdatedState(onItemPlaced) + val itemCoordinatesStore = remember(week.days.first().date) { + ItemCoordinatesStore(currentOnItemPlaced) + } Column( modifier = Modifier .then( @@ -53,16 +62,17 @@ internal fun WeekCalendarImpl( } else { Modifier.width(IntrinsicSize.Max) }, - ), + ) + .onPlaced(itemCoordinatesStore::onItemRootPlaced), ) { weekHeader?.invoke(this, week) Row { - for (day in week.days) { + for ((column, day) in week.days.withIndex()) { Box( modifier = Modifier .then(if (calendarScrollPaged) Modifier.weight(1f) else Modifier) .clipToBounds() - .onFirstDayPlaced(day, week, onFirstDayPlaced), + .onFirstDayPlaced(column, itemCoordinatesStore::onFirstDayPlaced), ) { dayContent(day) } @@ -75,10 +85,9 @@ internal fun WeekCalendarImpl( } private inline fun Modifier.onFirstDayPlaced( - day: WeekDay, - week: Week, + column: Int, noinline onFirstDayPlaced: (coordinates: LayoutCoordinates) -> Unit, -) = if (day == week.days.first()) { +) = if (column == 0) { onPlaced(onFirstDayPlaced) } else { this diff --git a/compose-multiplatform/library/src/commonMain/kotlin/com/kizitonwose/calendar/compose/yearcalendar/YearCalendarMonths.kt b/compose-multiplatform/library/src/commonMain/kotlin/com/kizitonwose/calendar/compose/yearcalendar/YearCalendarMonths.kt index a5d09274..63ecabd4 100644 --- a/compose-multiplatform/library/src/commonMain/kotlin/com/kizitonwose/calendar/compose/yearcalendar/YearCalendarMonths.kt +++ b/compose-multiplatform/library/src/commonMain/kotlin/com/kizitonwose/calendar/compose/yearcalendar/YearCalendarMonths.kt @@ -48,7 +48,7 @@ internal fun LazyListScope.YearCalendarMonths( yearBody: (@Composable ColumnScope.(CalendarYear, content: @Composable () -> Unit) -> Unit)?, yearFooter: (@Composable ColumnScope.(CalendarYear) -> Unit)?, yearContainer: (@Composable LazyItemScope.(CalendarYear, container: @Composable () -> Unit) -> Unit)?, - onFirstMonthAndDayPlaced: (month: CalendarMonth, monthCoordinates: LayoutCoordinates, dayCoordinates: LayoutCoordinates) -> Unit, + onItemPlaced: (itemCoordinates: YearItemCoordinates) -> Unit, ) { items( count = yearCount, @@ -61,101 +61,114 @@ internal fun LazyListScope.YearCalendarMonths( YearContentHeightMode.Stretch, -> true } + val months = isMonthVisible.apply(year.months) val hasYearContainer = yearContainer != null - yearContainer.or(defaultYearContainer)(year) { - Column( - modifier = Modifier - .then(if (hasYearContainer) Modifier.fillMaxWidth() else Modifier.fillParentMaxWidth()) - .then( - if (fillHeight) { - if (hasYearContainer) Modifier.fillMaxHeight() else Modifier.fillParentMaxHeight() - } else { - Modifier.wrapContentHeight() - }, - ), - ) { - val months = isMonthVisible.apply(year.months) - val currentOnFirstMonthAndDayPlaced by rememberUpdatedState(onFirstMonthAndDayPlaced) - val monthDayCoordinates = remember(months.first().yearMonth) { - MonthDayCoordinates(months.first(), currentOnFirstMonthAndDayPlaced) - } - val onFirstMonthPlaced: (LayoutCoordinates) -> Unit = remember { - { - monthDayCoordinates.monthCoordinates = it - } - } - val onFirstDayPlaced: (LayoutCoordinates) -> Unit = remember { - { - monthDayCoordinates.dayCoordinates = it - } - } - yearHeader?.invoke(this, year) - yearBody.or(defaultYearBody)(year) { - CalendarGrid( - modifier = Modifier - .fillMaxWidth() - .then(if (fillHeight) Modifier.weight(1f) else Modifier.wrapContentHeight()) - .padding(yearBodyContentPadding), - monthColumns = monthColumns, - monthCount = months.count(), - fillHeight = fillHeight, - monthVerticalSpacing = monthVerticalSpacing, - monthHorizontalSpacing = monthHorizontalSpacing, - onFirstMonthPlaced = onFirstMonthPlaced, - ) { monthOffset -> - val month = months[monthOffset] - val hasContainer = monthContainer != null - monthContainer.or(defaultMonthContainer)(month) { - Column( - modifier = Modifier - .then(if (hasContainer) Modifier.fillMaxWidth() else Modifier) - .then( - if (fillHeight) { - if (hasContainer) Modifier.fillMaxHeight() else Modifier - } else { - Modifier.wrapContentHeight() - }, - ), - ) { - monthHeader?.invoke(this, month) - monthBody.or(defaultMonthBody)(month) { - Column( - modifier = Modifier - .fillMaxWidth() - .then(if (fillHeight) Modifier.weight(1f) else Modifier.wrapContentHeight()), - ) { - for (week in month.weekDays) { - Row( - modifier = Modifier - .fillMaxWidth() - .then( - if (contentHeightMode == YearContentHeightMode.Stretch) { - Modifier.weight(1f) - } else { - Modifier.wrapContentHeight() - }, - ), - ) { - for (day in week) { - Box( - modifier = Modifier - .weight(1f) - .clipToBounds() - .onFirstDayPlaced(day, month, monthOffset, onFirstDayPlaced), - ) { - dayContent(day) + val currentOnItemPlaced by rememberUpdatedState(onItemPlaced) + val contentCoordinates = remember(year.year) { + YearItemCoordinatesStore(months.first(), currentOnItemPlaced) + } + Box(Modifier.onPlaced(contentCoordinates::onItemRootPlaced)) { + yearContainer.or(defaultYearContainer)(year) { + Column( + modifier = Modifier + .then( + if (hasYearContainer) { + Modifier.fillMaxWidth() + } else { + Modifier.fillParentMaxWidth() + }, + ) + .then( + if (fillHeight) { + if (hasYearContainer) { + Modifier.fillMaxHeight() + } else { + Modifier.fillParentMaxHeight() + } + } else { + Modifier.wrapContentHeight() + }, + ), + ) { + yearHeader?.invoke(this, year) + yearBody.or(defaultYearBody)(year) { + CalendarGrid( + modifier = Modifier + .fillMaxWidth() + .then(if (fillHeight) Modifier.weight(1f) else Modifier.wrapContentHeight()) + .padding(yearBodyContentPadding), + monthColumns = monthColumns, + monthCount = months.count(), + fillHeight = fillHeight, + monthVerticalSpacing = monthVerticalSpacing, + monthHorizontalSpacing = monthHorizontalSpacing, + onFirstMonthPlaced = contentCoordinates::onFirstMonthPlaced, + ) { monthOffset -> + val month = months[monthOffset] + val hasMonthContainer = monthContainer != null + monthContainer.or(defaultMonthContainer)(month) { + Column( + modifier = Modifier + .then(if (hasMonthContainer) Modifier.fillMaxWidth() else Modifier) + .then( + if (fillHeight) { + if (hasMonthContainer) Modifier.fillMaxHeight() else Modifier + } else { + Modifier.wrapContentHeight() + }, + ), + ) { + monthHeader?.invoke(this, month) + monthBody.or(defaultMonthBody)(month) { + Column( + modifier = Modifier + .fillMaxWidth() + .then( + if (fillHeight) { + Modifier.weight(1f) + } else { + Modifier.wrapContentHeight() + }, + ), + ) { + for ((row, week) in month.weekDays.withIndex()) { + Row( + modifier = Modifier + .fillMaxWidth() + .then( + if (contentHeightMode == YearContentHeightMode.Stretch) { + Modifier.weight(1f) + } else { + Modifier.wrapContentHeight() + }, + ), + ) { + for ((column, day) in week.withIndex()) { + Box( + modifier = Modifier + .weight(1f) + .clipToBounds() + .onFirstDayPlaced( + monthIndex = monthOffset, + dateRow = row, + dateColumn = column, + onFirstDayPlaced = contentCoordinates::onFirstDayPlaced, + ), + ) { + dayContent(day) + } } } } } } + monthFooter?.invoke(this, month) } - monthFooter?.invoke(this, month) } } } + yearFooter?.invoke(this, year) } - yearFooter?.invoke(this, year) } } } @@ -203,38 +216,49 @@ private inline fun CalendarGrid( } @Stable -private class MonthDayCoordinates( - private val month: CalendarMonth, - private val onMonthAndDayPlaced: ( - calendarMonth: CalendarMonth, - monthCoordinates: LayoutCoordinates, - dayCoordinates: LayoutCoordinates, - ) -> Unit, +internal class YearItemCoordinatesStore( + private val firstMonth: CalendarMonth, + private val onItemPlaced: (itemCoordinates: YearItemCoordinates) -> Unit, ) { - var monthCoordinates: LayoutCoordinates? = null - set(value) { - field = value - val dayCoordinates = dayCoordinates - if (value != null && dayCoordinates != null) { - onMonthAndDayPlaced(month, value, dayCoordinates) - } - } - var dayCoordinates: LayoutCoordinates? = null - set(value) { - field = value - val monthCoordinates = monthCoordinates - if (value != null && monthCoordinates != null) { - onMonthAndDayPlaced(month, monthCoordinates, value) - } - } + private var itemRootCoordinates: LayoutCoordinates? = null + private var firstDayCoordinates: LayoutCoordinates? = null + private var firstMonthCoordinates: LayoutCoordinates? = null + + fun onItemRootPlaced(coordinates: LayoutCoordinates) { + itemRootCoordinates = coordinates + check() + } + + fun onFirstMonthPlaced(coordinates: LayoutCoordinates) { + firstMonthCoordinates = coordinates + check() + } + + fun onFirstDayPlaced(coordinates: LayoutCoordinates) { + firstDayCoordinates = coordinates + check() + } + + private fun check() { + val itemRootCoordinates = itemRootCoordinates ?: return + val firstMonthCoordinates = firstMonthCoordinates ?: return + val firstDayCoordinates = firstDayCoordinates ?: return + val itemCoordinates = YearItemCoordinates( + firstMonth = firstMonth, + itemRootCoordinates = itemRootCoordinates, + firstMonthCoordinates = firstMonthCoordinates, + firstDayCoordinates = firstDayCoordinates, + ) + onItemPlaced(itemCoordinates) + } } private inline fun Modifier.onFirstDayPlaced( - day: CalendarDay, - month: CalendarMonth, monthIndex: Int, + dateRow: Int, + dateColumn: Int, noinline onFirstDayPlaced: (coordinates: LayoutCoordinates) -> Unit, -) = if (monthIndex == 0 && day == month.weekDays.first().first()) { +) = if (monthIndex == 0 && dateRow == 0 && dateColumn == 0) { onPlaced(onFirstDayPlaced) } else { this diff --git a/compose-multiplatform/library/src/commonMain/kotlin/com/kizitonwose/calendar/compose/yearcalendar/YearItemPlacementInfo.kt b/compose-multiplatform/library/src/commonMain/kotlin/com/kizitonwose/calendar/compose/yearcalendar/YearItemPlacementInfo.kt index 8c60f496..0837c00a 100644 --- a/compose-multiplatform/library/src/commonMain/kotlin/com/kizitonwose/calendar/compose/yearcalendar/YearItemPlacementInfo.kt +++ b/compose-multiplatform/library/src/commonMain/kotlin/com/kizitonwose/calendar/compose/yearcalendar/YearItemPlacementInfo.kt @@ -1,22 +1,26 @@ package com.kizitonwose.calendar.compose.yearcalendar import androidx.compose.foundation.gestures.Orientation +import androidx.compose.runtime.Immutable import androidx.compose.runtime.Stable import androidx.compose.runtime.withFrameNanos -import androidx.compose.ui.geometry.Offset import androidx.compose.ui.layout.LayoutCoordinates import androidx.compose.ui.unit.round -import com.kizitonwose.calendar.compose.findItemViewCoordinates import com.kizitonwose.calendar.core.CalendarMonth import kotlinx.coroutines.isActive import kotlin.coroutines.coroutineContext -private typealias FirstMonthDayCoordinates = Triple +@Immutable +internal data class YearItemCoordinates( + val firstMonth: CalendarMonth, + val itemRootCoordinates: LayoutCoordinates, + val firstMonthCoordinates: LayoutCoordinates, + val firstDayCoordinates: LayoutCoordinates, +) @Stable internal class YearItemPlacementInfo { - private var calendarCoordinates: LayoutCoordinates? = null - private var firstMonthDayCoordinates: FirstMonthDayCoordinates? = null + private var itemCoordinates: YearItemCoordinates? = null internal var isMonthVisible: ((month: CalendarMonth) -> Boolean)? = null internal var monthVerticalSpacingPx = 0 @@ -24,49 +28,24 @@ internal class YearItemPlacementInfo { internal var monthColumns = 0 internal var contentHeightMode = YearContentHeightMode.Wrap - fun onCalendarPlaced(coordinates: LayoutCoordinates) { - calendarCoordinates = coordinates - } - - fun onFirstMonthAndDayPlaced( - month: CalendarMonth, - monthCoordinates: LayoutCoordinates, - dayCoordinates: LayoutCoordinates, - ) { - firstMonthDayCoordinates = Triple( - first = month, - second = monthCoordinates, - third = dayCoordinates, - ) + fun onItemPlaced(itemCoordinates: YearItemCoordinates) { + this.itemCoordinates = itemCoordinates } suspend fun awaitFistMonthDayOffsetAndSize(orientation: Orientation): OffsetSize? { - var calendarCoord: LayoutCoordinates? = null - var firstMonthDayCoord: FirstMonthDayCoordinates? = null - while (coroutineContext.isActive && - (calendarCoord == null || firstMonthDayCoord == null) - ) { - calendarCoord = calendarCoordinates - firstMonthDayCoord = firstMonthDayCoordinates - // day and month coord are set at the same time but check anyway - if (calendarCoord == null || firstMonthDayCoord == null) { - withFrameNanos {} - } + var itemCoordinates = this.itemCoordinates + while (coroutineContext.isActive && itemCoordinates == null) { + withFrameNanos {} + itemCoordinates = this.itemCoordinates } - if (calendarCoord == null || - firstMonthDayCoord == null || - !calendarCoord.isAttached || - !firstMonthDayCoord.second.isAttached || - !firstMonthDayCoord.third.isAttached - ) { + if (itemCoordinates == null) { return null } - val (month, firstMonthCoord, firstDayCoord) = firstMonthDayCoord - val itemViewCoord = findItemViewCoordinates(firstDayCoord, calendarCoord) - val daySize = firstDayCoord.size - val monthOffset = itemViewCoord.localPositionOf(firstMonthCoord, Offset.Zero).round() - val dayOffsetInMonth = firstMonthCoord.localPositionOf(firstDayCoord, Offset.Zero).round() - val monthSize = firstMonthCoord.size + val (firstMonth, itemRootCoordinates, firstMonthCoordinates, firstDayCoordinates) = itemCoordinates + val daySize = firstDayCoordinates.size + val monthOffset = itemRootCoordinates.localPositionOf(firstMonthCoordinates).round() + val dayOffsetInMonth = firstMonthCoordinates.localPositionOf(firstDayCoordinates).round() + val monthSize = firstMonthCoordinates.size return when (orientation) { Orientation.Vertical -> OffsetSize( monthOffsetInContainer = monthOffset.y, @@ -74,7 +53,7 @@ internal class YearItemPlacementInfo { monthSpacing = monthVerticalSpacingPx, dayOffsetInMonth = dayOffsetInMonth.y, daySize = daySize.height, - dayBodyCount = month.weekDays.size, + dayBodyCount = firstMonth.weekDays.size, ) Orientation.Horizontal -> { @@ -84,12 +63,13 @@ internal class YearItemPlacementInfo { monthSpacing = monthHorizontalSpacingPx, dayOffsetInMonth = dayOffsetInMonth.x, daySize = daySize.width, - dayBodyCount = month.weekDays.first().size, + dayBodyCount = firstMonth.weekDays.first().size, ) } } } + @Immutable internal data class OffsetSize( val monthSize: Int, val monthOffsetInContainer: Int, diff --git a/compose/src/main/java/com/kizitonwose/calendar/compose/Calendar.kt b/compose/src/main/java/com/kizitonwose/calendar/compose/Calendar.kt index e33b52f9..868d53bd 100644 --- a/compose/src/main/java/com/kizitonwose/calendar/compose/Calendar.kt +++ b/compose/src/main/java/com/kizitonwose/calendar/compose/Calendar.kt @@ -10,11 +10,9 @@ import androidx.compose.foundation.lazy.LazyRow import androidx.compose.runtime.Composable import androidx.compose.runtime.DisposableEffect import androidx.compose.ui.Modifier -import androidx.compose.ui.layout.onPlaced import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp -import androidx.compose.ui.util.fastRoundToInt import com.kizitonwose.calendar.compose.CalendarDefaults.flingBehavior import com.kizitonwose.calendar.compose.heatmapcalendar.HeatMapCalendarImpl import com.kizitonwose.calendar.compose.heatmapcalendar.HeatMapCalendarState @@ -180,7 +178,7 @@ private fun Calendar( ) { if (isHorizontal) { LazyRow( - modifier = modifier.onPlaced(state.placementInfo::onCalendarPlaced), + modifier = modifier, state = state.listState, flingBehavior = flingBehavior(calendarScrollPaged, state.listState), userScrollEnabled = userScrollEnabled, @@ -196,12 +194,12 @@ private fun Calendar( monthBody = monthBody, monthFooter = monthFooter, monthContainer = monthContainer, - onFirstDayPlaced = state.placementInfo::onFirstDayPlaced, + onItemPlaced = state.placementInfo::onItemPlaced, ) } } else { LazyColumn( - modifier = modifier.onPlaced(state.placementInfo::onCalendarPlaced), + modifier = modifier, state = state.listState, flingBehavior = flingBehavior(calendarScrollPaged, state.listState), userScrollEnabled = userScrollEnabled, @@ -217,7 +215,7 @@ private fun Calendar( monthBody = monthBody, monthFooter = monthFooter, monthContainer = monthContainer, - onFirstDayPlaced = state.placementInfo::onFirstDayPlaced, + onItemPlaced = state.placementInfo::onItemPlaced, ) } } @@ -257,7 +255,7 @@ public fun WeekCalendar( weekHeader: (@Composable ColumnScope.(Week) -> Unit)? = null, weekFooter: (@Composable ColumnScope.(Week) -> Unit)? = null, ): Unit = WeekCalendarImpl( - modifier = modifier.onPlaced(state.placementInfo::onCalendarPlaced), + modifier = modifier, state = state, calendarScrollPaged = calendarScrollPaged, userScrollEnabled = userScrollEnabled, @@ -266,7 +264,7 @@ public fun WeekCalendar( weekHeader = weekHeader, weekFooter = weekFooter, contentPadding = contentPadding, - onFirstDayPlaced = state.placementInfo::onFirstDayPlaced, + onItemPlaced = state.placementInfo::onItemPlaced, ) /** @@ -550,14 +548,14 @@ private fun YearCalendar( // Intentionally not creating a coroutine scope with LaunchedEffect DisposableEffect(monthVerticalSpacing, monthHorizontalSpacing) { with(density) { - state.placementInfo.monthVerticalSpacingPx = monthVerticalSpacing.toPx().fastRoundToInt() - state.placementInfo.monthHorizontalSpacingPx = monthHorizontalSpacing.toPx().fastRoundToInt() + state.placementInfo.monthVerticalSpacingPx = monthVerticalSpacing.roundToPx() + state.placementInfo.monthHorizontalSpacingPx = monthHorizontalSpacing.roundToPx() } onDispose {} } if (isHorizontal) { LazyRow( - modifier = modifier.onPlaced(state.placementInfo::onCalendarPlaced), + modifier = modifier, state = state.listState, flingBehavior = flingBehavior(calendarScrollPaged, state.listState), userScrollEnabled = userScrollEnabled, @@ -582,12 +580,12 @@ private fun YearCalendar( yearBody = yearBody, yearFooter = yearFooter, yearContainer = yearContainer, - onFirstMonthAndDayPlaced = state.placementInfo::onFirstMonthAndDayPlaced, + onItemPlaced = state.placementInfo::onItemPlaced, ) } } else { LazyColumn( - modifier = modifier.onPlaced(state.placementInfo::onCalendarPlaced), + modifier = modifier, state = state.listState, flingBehavior = flingBehavior(calendarScrollPaged, state.listState), userScrollEnabled = userScrollEnabled, @@ -612,7 +610,7 @@ private fun YearCalendar( yearBody = yearBody, yearFooter = yearFooter, yearContainer = yearContainer, - onFirstMonthAndDayPlaced = state.placementInfo::onFirstMonthAndDayPlaced, + onItemPlaced = state.placementInfo::onItemPlaced, ) } } diff --git a/compose/src/main/java/com/kizitonwose/calendar/compose/CalendarMonths.kt b/compose/src/main/java/com/kizitonwose/calendar/compose/CalendarMonths.kt index 0e765e96..24fb6b22 100644 --- a/compose/src/main/java/com/kizitonwose/calendar/compose/CalendarMonths.kt +++ b/compose/src/main/java/com/kizitonwose/calendar/compose/CalendarMonths.kt @@ -11,6 +11,10 @@ import androidx.compose.foundation.layout.wrapContentHeight import androidx.compose.foundation.lazy.LazyItemScope import androidx.compose.foundation.lazy.LazyListScope import androidx.compose.runtime.Composable +import androidx.compose.runtime.Stable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberUpdatedState import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clipToBounds import androidx.compose.ui.layout.LayoutCoordinates @@ -28,7 +32,7 @@ internal fun LazyListScope.CalendarMonths( monthBody: (@Composable ColumnScope.(CalendarMonth, content: @Composable () -> Unit) -> Unit)?, monthFooter: (@Composable ColumnScope.(CalendarMonth) -> Unit)?, monthContainer: (@Composable LazyItemScope.(CalendarMonth, container: @Composable () -> Unit) -> Unit)?, - onFirstDayPlaced: (coordinates: LayoutCoordinates) -> Unit, + onItemPlaced: (itemCoordinates: ItemCoordinates) -> Unit, ) { items( count = monthCount, @@ -39,57 +43,117 @@ internal fun LazyListScope.CalendarMonths( ContentHeightMode.Wrap -> false ContentHeightMode.Fill -> true } - val hasContainer = monthContainer != null - monthContainer.or(defaultMonthContainer)(month) { - Column( - modifier = Modifier - .then(if (hasContainer) Modifier.fillMaxWidth() else Modifier.fillParentMaxWidth()) - .then( - if (fillHeight) { - if (hasContainer) Modifier.fillMaxHeight() else Modifier.fillParentMaxHeight() - } else { - Modifier.wrapContentHeight() - }, - ), - ) { - monthHeader?.invoke(this, month) - monthBody.or(defaultMonthBody)(month) { - Column( - modifier = Modifier - .fillMaxWidth() - .then(if (fillHeight) Modifier.weight(1f) else Modifier.wrapContentHeight()), - ) { - for (week in month.weekDays) { - Row( - modifier = Modifier - .fillMaxWidth() - .then(if (fillHeight) Modifier.weight(1f) else Modifier.wrapContentHeight()), - ) { - for (day in week) { - Box( - modifier = Modifier - .weight(1f) - .clipToBounds() - .onFirstDayPlaced(day, month, onFirstDayPlaced), - ) { - dayContent(day) + val hasMonthContainer = monthContainer != null + val currentOnItemPlaced by rememberUpdatedState(onItemPlaced) + val itemCoordinatesStore = remember(month.yearMonth) { + ItemCoordinatesStore(currentOnItemPlaced) + } + Box(Modifier.onPlaced(itemCoordinatesStore::onItemRootPlaced)) { + monthContainer.or(defaultMonthContainer)(month) { + Column( + modifier = Modifier + .then( + if (hasMonthContainer) { + Modifier.fillMaxWidth() + } else { + Modifier.fillParentMaxWidth() + }, + ) + .then( + if (fillHeight) { + if (hasMonthContainer) { + Modifier.fillMaxHeight() + } else { + Modifier.fillParentMaxHeight() + } + } else { + Modifier.wrapContentHeight() + }, + ), + ) { + monthHeader?.invoke(this, month) + monthBody.or(defaultMonthBody)(month) { + Column( + modifier = Modifier + .fillMaxWidth() + .then( + if (fillHeight) { + Modifier.weight(1f) + } else { + Modifier.wrapContentHeight() + }, + ), + ) { + for ((row, week) in month.weekDays.withIndex()) { + Row( + modifier = Modifier + .fillMaxWidth() + .then( + if (fillHeight) { + Modifier.weight(1f) + } else { + Modifier.wrapContentHeight() + }, + ), + ) { + for ((column, day) in week.withIndex()) { + Box( + modifier = Modifier + .weight(1f) + .clipToBounds() + .onFirstDayPlaced( + dateRow = row, + dateColumn = column, + onFirstDayPlaced = itemCoordinatesStore::onFirstDayPlaced, + ), + ) { + dayContent(day) + } } } } } } + monthFooter?.invoke(this, month) } - monthFooter?.invoke(this, month) } } } } +@Stable +internal class ItemCoordinatesStore( + private val onItemPlaced: (itemCoordinates: ItemCoordinates) -> Unit, +) { + private var itemRootCoordinates: LayoutCoordinates? = null + private var firstDayCoordinates: LayoutCoordinates? = null + + fun onItemRootPlaced(coordinates: LayoutCoordinates) { + itemRootCoordinates = coordinates + check() + } + + fun onFirstDayPlaced(coordinates: LayoutCoordinates) { + firstDayCoordinates = coordinates + check() + } + + private fun check() { + val itemRootCoordinates = itemRootCoordinates ?: return + val firstDayCoordinates = firstDayCoordinates ?: return + val itemCoordinates = ItemCoordinates( + itemRootCoordinates = itemRootCoordinates, + firstDayCoordinates = firstDayCoordinates, + ) + onItemPlaced(itemCoordinates) + } +} + private inline fun Modifier.onFirstDayPlaced( - day: CalendarDay, - month: CalendarMonth, + dateRow: Int, + dateColumn: Int, noinline onFirstDayPlaced: (coordinates: LayoutCoordinates) -> Unit, -) = if (day == month.weekDays.first().first()) { +) = if (dateRow == 0 && dateColumn == 0) { onPlaced(onFirstDayPlaced) } else { this diff --git a/compose/src/main/java/com/kizitonwose/calendar/compose/ItemPlacementInfo.kt b/compose/src/main/java/com/kizitonwose/calendar/compose/ItemPlacementInfo.kt index 38890cc0..41616bdd 100644 --- a/compose/src/main/java/com/kizitonwose/calendar/compose/ItemPlacementInfo.kt +++ b/compose/src/main/java/com/kizitonwose/calendar/compose/ItemPlacementInfo.kt @@ -1,58 +1,48 @@ package com.kizitonwose.calendar.compose import androidx.compose.foundation.gestures.Orientation +import androidx.compose.runtime.Immutable import androidx.compose.runtime.Stable import androidx.compose.runtime.withFrameNanos -import androidx.compose.ui.geometry.Offset import androidx.compose.ui.layout.LayoutCoordinates -import androidx.compose.ui.layout.positionInWindow import androidx.compose.ui.unit.round import kotlinx.coroutines.isActive import kotlin.coroutines.coroutineContext +@Immutable +internal data class ItemCoordinates( + val itemRootCoordinates: LayoutCoordinates, + val firstDayCoordinates: LayoutCoordinates, +) + @Stable internal class ItemPlacementInfo { - private var calendarCoordinates: LayoutCoordinates? = null - private var firstDayCoordinates: LayoutCoordinates? = null - - fun onCalendarPlaced(coordinates: LayoutCoordinates) { - calendarCoordinates = coordinates - } + private var itemCoordinates: ItemCoordinates? = null - fun onFirstDayPlaced(coordinates: LayoutCoordinates) { - firstDayCoordinates = coordinates + fun onItemPlaced(itemCoordinates: ItemCoordinates) { + this.itemCoordinates = itemCoordinates } - suspend fun awaitFistDayOffsetAndSize(orientation: Orientation): DayOffsetSize? { - var calendarCoord: LayoutCoordinates? = null - var firstDayCoord: LayoutCoordinates? = null - while (coroutineContext.isActive && - (calendarCoord == null || firstDayCoord == null) - ) { - calendarCoord = calendarCoordinates - firstDayCoord = firstDayCoordinates - if (calendarCoord == null || firstDayCoord == null) { - withFrameNanos {} - } + suspend fun awaitFistDayOffsetAndSize(orientation: Orientation): OffsetSize? { + var itemCoordinates = this.itemCoordinates + while (coroutineContext.isActive && itemCoordinates == null) { + withFrameNanos {} + itemCoordinates = this.itemCoordinates } - if (calendarCoord == null || - firstDayCoord == null || - !calendarCoord.isAttached || - !firstDayCoord.isAttached - ) { + if (itemCoordinates == null) { return null } - val itemViewCoord = findItemViewCoordinates(firstDayCoord, calendarCoord) - val daySize = firstDayCoord.size - val dayOffset = itemViewCoord.localPositionOf(firstDayCoord, Offset.Zero).round() + val (itemRootCoordinates, firstDayCoordinates) = itemCoordinates + val daySize = firstDayCoordinates.size + val dayOffset = itemRootCoordinates.localPositionOf(firstDayCoordinates).round() return when (orientation) { - Orientation.Vertical -> DayOffsetSize( + Orientation.Vertical -> OffsetSize( offset = dayOffset.y, size = daySize.height, ) Orientation.Horizontal -> { - DayOffsetSize( + OffsetSize( offset = dayOffset.x, size = daySize.width, ) @@ -60,25 +50,9 @@ internal class ItemPlacementInfo { } } - internal data class DayOffsetSize( + @Immutable + internal data class OffsetSize( val offset: Int, val size: Int, ) } - -internal fun findItemViewCoordinates( - firstDayCoord: LayoutCoordinates, - calendarCoord: LayoutCoordinates, -): LayoutCoordinates { - var itemViewCoord = firstDayCoord - var parent = itemViewCoord.parentLayoutCoordinates - // Find the coordinates that match the index item layout - while (parent != null && - parent.size != calendarCoord.size && - parent.positionInWindow() != calendarCoord.positionInWindow() - ) { - itemViewCoord = parent - parent = itemViewCoord.parentLayoutCoordinates - } - return itemViewCoord -} diff --git a/compose/src/main/java/com/kizitonwose/calendar/compose/weekcalendar/WeekCalendar.kt b/compose/src/main/java/com/kizitonwose/calendar/compose/weekcalendar/WeekCalendar.kt index 9a254aa4..8660b65a 100644 --- a/compose/src/main/java/com/kizitonwose/calendar/compose/weekcalendar/WeekCalendar.kt +++ b/compose/src/main/java/com/kizitonwose/calendar/compose/weekcalendar/WeekCalendar.kt @@ -10,11 +10,16 @@ import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.width import androidx.compose.foundation.lazy.LazyRow import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberUpdatedState import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clipToBounds import androidx.compose.ui.layout.LayoutCoordinates import androidx.compose.ui.layout.onPlaced import com.kizitonwose.calendar.compose.CalendarDefaults.flingBehavior +import com.kizitonwose.calendar.compose.ItemCoordinates +import com.kizitonwose.calendar.compose.ItemCoordinatesStore import com.kizitonwose.calendar.core.Week import com.kizitonwose.calendar.core.WeekDay @@ -29,7 +34,7 @@ internal fun WeekCalendarImpl( dayContent: @Composable BoxScope.(WeekDay) -> Unit, weekHeader: (@Composable ColumnScope.(Week) -> Unit)? = null, weekFooter: (@Composable ColumnScope.(Week) -> Unit)? = null, - onFirstDayPlaced: (coordinates: LayoutCoordinates) -> Unit, + onItemPlaced: (itemCoordinates: ItemCoordinates) -> Unit, ) { LazyRow( modifier = modifier, @@ -44,6 +49,10 @@ internal fun WeekCalendarImpl( key = { offset -> state.store[offset].days.first().date }, ) { offset -> val week = state.store[offset] + val currentOnItemPlaced by rememberUpdatedState(onItemPlaced) + val itemCoordinatesStore = remember(week.days.first().date) { + ItemCoordinatesStore(currentOnItemPlaced) + } Column( modifier = Modifier .then( @@ -52,16 +61,17 @@ internal fun WeekCalendarImpl( } else { Modifier.width(IntrinsicSize.Max) }, - ), + ) + .onPlaced(itemCoordinatesStore::onItemRootPlaced), ) { weekHeader?.invoke(this, week) Row { - for (day in week.days) { + for ((column, day) in week.days.withIndex()) { Box( modifier = Modifier .then(if (calendarScrollPaged) Modifier.weight(1f) else Modifier) .clipToBounds() - .onFirstDayPlaced(day, week, onFirstDayPlaced), + .onFirstDayPlaced(column, itemCoordinatesStore::onFirstDayPlaced), ) { dayContent(day) } @@ -74,10 +84,9 @@ internal fun WeekCalendarImpl( } private inline fun Modifier.onFirstDayPlaced( - day: WeekDay, - week: Week, + column: Int, noinline onFirstDayPlaced: (coordinates: LayoutCoordinates) -> Unit, -) = if (day == week.days.first()) { +) = if (column == 0) { onPlaced(onFirstDayPlaced) } else { this diff --git a/compose/src/main/java/com/kizitonwose/calendar/compose/yearcalendar/YearCalendarMonths.kt b/compose/src/main/java/com/kizitonwose/calendar/compose/yearcalendar/YearCalendarMonths.kt index 0afd670a..fe6f4dc8 100644 --- a/compose/src/main/java/com/kizitonwose/calendar/compose/yearcalendar/YearCalendarMonths.kt +++ b/compose/src/main/java/com/kizitonwose/calendar/compose/yearcalendar/YearCalendarMonths.kt @@ -48,7 +48,7 @@ internal fun LazyListScope.YearCalendarMonths( yearBody: (@Composable ColumnScope.(CalendarYear, content: @Composable () -> Unit) -> Unit)?, yearFooter: (@Composable ColumnScope.(CalendarYear) -> Unit)?, yearContainer: (@Composable LazyItemScope.(CalendarYear, container: @Composable () -> Unit) -> Unit)?, - onFirstMonthAndDayPlaced: (month: CalendarMonth, monthCoordinates: LayoutCoordinates, dayCoordinates: LayoutCoordinates) -> Unit, + onItemPlaced: (itemCoordinates: YearItemCoordinates) -> Unit, ) { items( count = yearCount, @@ -61,101 +61,114 @@ internal fun LazyListScope.YearCalendarMonths( YearContentHeightMode.Stretch, -> true } + val months = isMonthVisible.apply(year.months) val hasYearContainer = yearContainer != null - yearContainer.or(defaultYearContainer)(year) { - Column( - modifier = Modifier - .then(if (hasYearContainer) Modifier.fillMaxWidth() else Modifier.fillParentMaxWidth()) - .then( - if (fillHeight) { - if (hasYearContainer) Modifier.fillMaxHeight() else Modifier.fillParentMaxHeight() - } else { - Modifier.wrapContentHeight() - }, - ), - ) { - val months = isMonthVisible.apply(year.months) - val currentOnFirstMonthAndDayPlaced by rememberUpdatedState(onFirstMonthAndDayPlaced) - val monthDayCoordinates = remember(months.first().yearMonth) { - MonthDayCoordinates(months.first(), currentOnFirstMonthAndDayPlaced) - } - val onFirstMonthPlaced: (LayoutCoordinates) -> Unit = remember { - { - monthDayCoordinates.monthCoordinates = it - } - } - val onFirstDayPlaced: (LayoutCoordinates) -> Unit = remember { - { - monthDayCoordinates.dayCoordinates = it - } - } - yearHeader?.invoke(this, year) - yearBody.or(defaultYearBody)(year) { - CalendarGrid( - modifier = Modifier - .fillMaxWidth() - .then(if (fillHeight) Modifier.weight(1f) else Modifier.wrapContentHeight()) - .padding(yearBodyContentPadding), - monthColumns = monthColumns, - monthCount = months.count(), - fillHeight = fillHeight, - monthVerticalSpacing = monthVerticalSpacing, - monthHorizontalSpacing = monthHorizontalSpacing, - onFirstMonthPlaced = onFirstMonthPlaced, - ) { monthOffset -> - val month = months[monthOffset] - val hasContainer = monthContainer != null - monthContainer.or(defaultMonthContainer)(month) { - Column( - modifier = Modifier - .then(if (hasContainer) Modifier.fillMaxWidth() else Modifier) - .then( - if (fillHeight) { - if (hasContainer) Modifier.fillMaxHeight() else Modifier - } else { - Modifier.wrapContentHeight() - }, - ), - ) { - monthHeader?.invoke(this, month) - monthBody.or(defaultMonthBody)(month) { - Column( - modifier = Modifier - .fillMaxWidth() - .then(if (fillHeight) Modifier.weight(1f) else Modifier.wrapContentHeight()), - ) { - for (week in month.weekDays) { - Row( - modifier = Modifier - .fillMaxWidth() - .then( - if (contentHeightMode == YearContentHeightMode.Stretch) { - Modifier.weight(1f) - } else { - Modifier.wrapContentHeight() - }, - ), - ) { - for (day in week) { - Box( - modifier = Modifier - .weight(1f) - .clipToBounds() - .onFirstDayPlaced(day, month, monthOffset, onFirstDayPlaced), - ) { - dayContent(day) + val currentOnItemPlaced by rememberUpdatedState(onItemPlaced) + val contentCoordinates = remember(year.year) { + YearItemCoordinatesStore(months.first(), currentOnItemPlaced) + } + Box(Modifier.onPlaced(contentCoordinates::onItemRootPlaced)) { + yearContainer.or(defaultYearContainer)(year) { + Column( + modifier = Modifier + .then( + if (hasYearContainer) { + Modifier.fillMaxWidth() + } else { + Modifier.fillParentMaxWidth() + }, + ) + .then( + if (fillHeight) { + if (hasYearContainer) { + Modifier.fillMaxHeight() + } else { + Modifier.fillParentMaxHeight() + } + } else { + Modifier.wrapContentHeight() + }, + ), + ) { + yearHeader?.invoke(this, year) + yearBody.or(defaultYearBody)(year) { + CalendarGrid( + modifier = Modifier + .fillMaxWidth() + .then(if (fillHeight) Modifier.weight(1f) else Modifier.wrapContentHeight()) + .padding(yearBodyContentPadding), + monthColumns = monthColumns, + monthCount = months.count(), + fillHeight = fillHeight, + monthVerticalSpacing = monthVerticalSpacing, + monthHorizontalSpacing = monthHorizontalSpacing, + onFirstMonthPlaced = contentCoordinates::onFirstMonthPlaced, + ) { monthOffset -> + val month = months[monthOffset] + val hasMonthContainer = monthContainer != null + monthContainer.or(defaultMonthContainer)(month) { + Column( + modifier = Modifier + .then(if (hasMonthContainer) Modifier.fillMaxWidth() else Modifier) + .then( + if (fillHeight) { + if (hasMonthContainer) Modifier.fillMaxHeight() else Modifier + } else { + Modifier.wrapContentHeight() + }, + ), + ) { + monthHeader?.invoke(this, month) + monthBody.or(defaultMonthBody)(month) { + Column( + modifier = Modifier + .fillMaxWidth() + .then( + if (fillHeight) { + Modifier.weight(1f) + } else { + Modifier.wrapContentHeight() + }, + ), + ) { + for ((row, week) in month.weekDays.withIndex()) { + Row( + modifier = Modifier + .fillMaxWidth() + .then( + if (contentHeightMode == YearContentHeightMode.Stretch) { + Modifier.weight(1f) + } else { + Modifier.wrapContentHeight() + }, + ), + ) { + for ((column, day) in week.withIndex()) { + Box( + modifier = Modifier + .weight(1f) + .clipToBounds() + .onFirstDayPlaced( + monthIndex = monthOffset, + dateRow = row, + dateColumn = column, + onFirstDayPlaced = contentCoordinates::onFirstDayPlaced, + ), + ) { + dayContent(day) + } } } } } } + monthFooter?.invoke(this, month) } - monthFooter?.invoke(this, month) } } } + yearFooter?.invoke(this, year) } - yearFooter?.invoke(this, year) } } } @@ -203,38 +216,49 @@ private inline fun CalendarGrid( } @Stable -private class MonthDayCoordinates( - private val month: CalendarMonth, - private val onMonthAndDayPlaced: ( - calendarMonth: CalendarMonth, - monthCoordinates: LayoutCoordinates, - dayCoordinates: LayoutCoordinates, - ) -> Unit, +internal class YearItemCoordinatesStore( + private val firstMonth: CalendarMonth, + private val onItemPlaced: (itemCoordinates: YearItemCoordinates) -> Unit, ) { - var monthCoordinates: LayoutCoordinates? = null - set(value) { - field = value - val dayCoordinates = dayCoordinates - if (value != null && dayCoordinates != null) { - onMonthAndDayPlaced(month, value, dayCoordinates) - } - } - var dayCoordinates: LayoutCoordinates? = null - set(value) { - field = value - val monthCoordinates = monthCoordinates - if (value != null && monthCoordinates != null) { - onMonthAndDayPlaced(month, monthCoordinates, value) - } - } + private var itemRootCoordinates: LayoutCoordinates? = null + private var firstDayCoordinates: LayoutCoordinates? = null + private var firstMonthCoordinates: LayoutCoordinates? = null + + fun onItemRootPlaced(coordinates: LayoutCoordinates) { + itemRootCoordinates = coordinates + check() + } + + fun onFirstMonthPlaced(coordinates: LayoutCoordinates) { + firstMonthCoordinates = coordinates + check() + } + + fun onFirstDayPlaced(coordinates: LayoutCoordinates) { + firstDayCoordinates = coordinates + check() + } + + private fun check() { + val itemRootCoordinates = itemRootCoordinates ?: return + val firstMonthCoordinates = firstMonthCoordinates ?: return + val firstDayCoordinates = firstDayCoordinates ?: return + val itemCoordinates = YearItemCoordinates( + firstMonth = firstMonth, + itemRootCoordinates = itemRootCoordinates, + firstMonthCoordinates = firstMonthCoordinates, + firstDayCoordinates = firstDayCoordinates, + ) + onItemPlaced(itemCoordinates) + } } private inline fun Modifier.onFirstDayPlaced( - day: CalendarDay, - month: CalendarMonth, monthIndex: Int, + dateRow: Int, + dateColumn: Int, noinline onFirstDayPlaced: (coordinates: LayoutCoordinates) -> Unit, -) = if (monthIndex == 0 && day == month.weekDays.first().first()) { +) = if (monthIndex == 0 && dateRow == 0 && dateColumn == 0) { onPlaced(onFirstDayPlaced) } else { this diff --git a/compose/src/main/java/com/kizitonwose/calendar/compose/yearcalendar/YearItemPlacementInfo.kt b/compose/src/main/java/com/kizitonwose/calendar/compose/yearcalendar/YearItemPlacementInfo.kt index 8c60f496..0837c00a 100644 --- a/compose/src/main/java/com/kizitonwose/calendar/compose/yearcalendar/YearItemPlacementInfo.kt +++ b/compose/src/main/java/com/kizitonwose/calendar/compose/yearcalendar/YearItemPlacementInfo.kt @@ -1,22 +1,26 @@ package com.kizitonwose.calendar.compose.yearcalendar import androidx.compose.foundation.gestures.Orientation +import androidx.compose.runtime.Immutable import androidx.compose.runtime.Stable import androidx.compose.runtime.withFrameNanos -import androidx.compose.ui.geometry.Offset import androidx.compose.ui.layout.LayoutCoordinates import androidx.compose.ui.unit.round -import com.kizitonwose.calendar.compose.findItemViewCoordinates import com.kizitonwose.calendar.core.CalendarMonth import kotlinx.coroutines.isActive import kotlin.coroutines.coroutineContext -private typealias FirstMonthDayCoordinates = Triple +@Immutable +internal data class YearItemCoordinates( + val firstMonth: CalendarMonth, + val itemRootCoordinates: LayoutCoordinates, + val firstMonthCoordinates: LayoutCoordinates, + val firstDayCoordinates: LayoutCoordinates, +) @Stable internal class YearItemPlacementInfo { - private var calendarCoordinates: LayoutCoordinates? = null - private var firstMonthDayCoordinates: FirstMonthDayCoordinates? = null + private var itemCoordinates: YearItemCoordinates? = null internal var isMonthVisible: ((month: CalendarMonth) -> Boolean)? = null internal var monthVerticalSpacingPx = 0 @@ -24,49 +28,24 @@ internal class YearItemPlacementInfo { internal var monthColumns = 0 internal var contentHeightMode = YearContentHeightMode.Wrap - fun onCalendarPlaced(coordinates: LayoutCoordinates) { - calendarCoordinates = coordinates - } - - fun onFirstMonthAndDayPlaced( - month: CalendarMonth, - monthCoordinates: LayoutCoordinates, - dayCoordinates: LayoutCoordinates, - ) { - firstMonthDayCoordinates = Triple( - first = month, - second = monthCoordinates, - third = dayCoordinates, - ) + fun onItemPlaced(itemCoordinates: YearItemCoordinates) { + this.itemCoordinates = itemCoordinates } suspend fun awaitFistMonthDayOffsetAndSize(orientation: Orientation): OffsetSize? { - var calendarCoord: LayoutCoordinates? = null - var firstMonthDayCoord: FirstMonthDayCoordinates? = null - while (coroutineContext.isActive && - (calendarCoord == null || firstMonthDayCoord == null) - ) { - calendarCoord = calendarCoordinates - firstMonthDayCoord = firstMonthDayCoordinates - // day and month coord are set at the same time but check anyway - if (calendarCoord == null || firstMonthDayCoord == null) { - withFrameNanos {} - } + var itemCoordinates = this.itemCoordinates + while (coroutineContext.isActive && itemCoordinates == null) { + withFrameNanos {} + itemCoordinates = this.itemCoordinates } - if (calendarCoord == null || - firstMonthDayCoord == null || - !calendarCoord.isAttached || - !firstMonthDayCoord.second.isAttached || - !firstMonthDayCoord.third.isAttached - ) { + if (itemCoordinates == null) { return null } - val (month, firstMonthCoord, firstDayCoord) = firstMonthDayCoord - val itemViewCoord = findItemViewCoordinates(firstDayCoord, calendarCoord) - val daySize = firstDayCoord.size - val monthOffset = itemViewCoord.localPositionOf(firstMonthCoord, Offset.Zero).round() - val dayOffsetInMonth = firstMonthCoord.localPositionOf(firstDayCoord, Offset.Zero).round() - val monthSize = firstMonthCoord.size + val (firstMonth, itemRootCoordinates, firstMonthCoordinates, firstDayCoordinates) = itemCoordinates + val daySize = firstDayCoordinates.size + val monthOffset = itemRootCoordinates.localPositionOf(firstMonthCoordinates).round() + val dayOffsetInMonth = firstMonthCoordinates.localPositionOf(firstDayCoordinates).round() + val monthSize = firstMonthCoordinates.size return when (orientation) { Orientation.Vertical -> OffsetSize( monthOffsetInContainer = monthOffset.y, @@ -74,7 +53,7 @@ internal class YearItemPlacementInfo { monthSpacing = monthVerticalSpacingPx, dayOffsetInMonth = dayOffsetInMonth.y, daySize = daySize.height, - dayBodyCount = month.weekDays.size, + dayBodyCount = firstMonth.weekDays.size, ) Orientation.Horizontal -> { @@ -84,12 +63,13 @@ internal class YearItemPlacementInfo { monthSpacing = monthHorizontalSpacingPx, dayOffsetInMonth = dayOffsetInMonth.x, daySize = daySize.width, - dayBodyCount = month.weekDays.first().size, + dayBodyCount = firstMonth.weekDays.first().size, ) } } } + @Immutable internal data class OffsetSize( val monthSize: Int, val monthOffsetInContainer: Int,