Skip to content

Commit

Permalink
Derive day coordinates from item root layout
Browse files Browse the repository at this point in the history
  • Loading branch information
kizitonwose committed Jan 18, 2025
1 parent ca4d959 commit 6a03b87
Show file tree
Hide file tree
Showing 12 changed files with 626 additions and 528 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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,
Expand All @@ -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,
Expand All @@ -217,7 +215,7 @@ private fun Calendar(
monthBody = monthBody,
monthFooter = monthFooter,
monthContainer = monthContainer,
onFirstDayPlaced = state.placementInfo::onFirstDayPlaced,
onItemPlaced = state.placementInfo::onItemPlaced,
)
}
}
Expand Down Expand Up @@ -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,
Expand All @@ -266,7 +264,7 @@ public fun WeekCalendar(
weekHeader = weekHeader,
weekFooter = weekFooter,
contentPadding = contentPadding,
onFirstDayPlaced = state.placementInfo::onFirstDayPlaced,
onItemPlaced = state.placementInfo::onItemPlaced,
)

/**
Expand Down Expand Up @@ -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,
Expand All @@ -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,
Expand All @@ -612,7 +610,7 @@ private fun YearCalendar(
yearBody = yearBody,
yearFooter = yearFooter,
yearContainer = yearContainer,
onFirstMonthAndDayPlaced = state.placementInfo::onFirstMonthAndDayPlaced,
onItemPlaced = state.placementInfo::onItemPlaced,
)
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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,
Expand All @@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -1,84 +1,58 @@
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,
)
}
}
}

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
}
Loading

0 comments on commit 6a03b87

Please sign in to comment.