diff --git a/ComposeCollapsingTopBar/api/ComposeCollapsingTopBar.klib.api b/ComposeCollapsingTopBar/api/ComposeCollapsingTopBar.klib.api index ddcb179..7e8d9ec 100644 --- a/ComposeCollapsingTopBar/api/ComposeCollapsingTopBar.klib.api +++ b/ComposeCollapsingTopBar/api/ComposeCollapsingTopBar.klib.api @@ -194,6 +194,8 @@ final class com.flaringapp.compose.topbar.scaffold/CollapsingTopBarScaffoldScrol final class com.flaringapp.compose.topbar.scaffold/CollapsingTopBarScaffoldState : com.flaringapp.compose.topbar.snap/CollapsingTopBarSnapScope, com.flaringapp.compose.topbar/CollapsingTopBarControls { // com.flaringapp.compose.topbar.scaffold/CollapsingTopBarScaffoldState|null[0] final val exitState // com.flaringapp.compose.topbar.scaffold/CollapsingTopBarScaffoldState.exitState|{}exitState[0] final fun (): com.flaringapp.compose.topbar.dependent/CollapsingTopBarExitState // com.flaringapp.compose.topbar.scaffold/CollapsingTopBarScaffoldState.exitState.|(){}[0] + final val hasMeasured // com.flaringapp.compose.topbar.scaffold/CollapsingTopBarScaffoldState.hasMeasured|{}hasMeasured[0] + final fun (): kotlin/Boolean // com.flaringapp.compose.topbar.scaffold/CollapsingTopBarScaffoldState.hasMeasured.|(){}[0] final val isCollapsed // com.flaringapp.compose.topbar.scaffold/CollapsingTopBarScaffoldState.isCollapsed|{}isCollapsed[0] final fun (): kotlin/Boolean // com.flaringapp.compose.topbar.scaffold/CollapsingTopBarScaffoldState.isCollapsed.|(){}[0] final val isExpanded // com.flaringapp.compose.topbar.scaffold/CollapsingTopBarScaffoldState.isExpanded|{}isExpanded[0] @@ -253,6 +255,8 @@ final class com.flaringapp.compose.topbar/CollapsingTopBarLayoutInfo { // com.fl final class com.flaringapp.compose.topbar/CollapsingTopBarState : androidx.compose.foundation.gestures/ScrollableState, com.flaringapp.compose.topbar.snap/CollapsingTopBarSnapScope, com.flaringapp.compose.topbar/CollapsingTopBarControls { // com.flaringapp.compose.topbar/CollapsingTopBarState|null[0] constructor (kotlin/Boolean = ...) // com.flaringapp.compose.topbar/CollapsingTopBarState.|(kotlin.Boolean){}[0] + final val hasMeasured // com.flaringapp.compose.topbar/CollapsingTopBarState.hasMeasured|{}hasMeasured[0] + final fun (): kotlin/Boolean // com.flaringapp.compose.topbar/CollapsingTopBarState.hasMeasured.|(){}[0] final val isCollapsed // com.flaringapp.compose.topbar/CollapsingTopBarState.isCollapsed|{}isCollapsed[0] final fun (): kotlin/Boolean // com.flaringapp.compose.topbar/CollapsingTopBarState.isCollapsed.|(){}[0] final val isExpanded // com.flaringapp.compose.topbar/CollapsingTopBarState.isExpanded|{}isExpanded[0] diff --git a/ComposeCollapsingTopBar/src/commonMain/kotlin/com/flaringapp/compose/topbar/CollapsingTopBar.kt b/ComposeCollapsingTopBar/src/commonMain/kotlin/com/flaringapp/compose/topbar/CollapsingTopBar.kt index 6d2b5a8..056564f 100644 --- a/ComposeCollapsingTopBar/src/commonMain/kotlin/com/flaringapp/compose/topbar/CollapsingTopBar.kt +++ b/ComposeCollapsingTopBar/src/commonMain/kotlin/com/flaringapp/compose/topbar/CollapsingTopBar.kt @@ -54,7 +54,8 @@ import kotlin.math.roundToInt * Each child may actively participate in collapsing process with [CollapsingTopBarScope] modifiers. * * Advanced collapsing techniques can be achieved using: - * - transformations based on [CollapsingTopBarState.layoutInfo] + * - transformations based on [CollapsingTopBarState.layoutInfo]. When exact bounds matter, use + * [CollapsingTopBarState.hasMeasured] to distinguish placeholder values from actual measurements. * - custom layout logic with [CollapsingTopBarNestedCollapseElement] * * @param modifier the [Modifier] to be applied to this top bar. diff --git a/ComposeCollapsingTopBar/src/commonMain/kotlin/com/flaringapp/compose/topbar/CollapsingTopBarState.kt b/ComposeCollapsingTopBar/src/commonMain/kotlin/com/flaringapp/compose/topbar/CollapsingTopBarState.kt index a023413..daff19b 100644 --- a/ComposeCollapsingTopBar/src/commonMain/kotlin/com/flaringapp/compose/topbar/CollapsingTopBarState.kt +++ b/ComposeCollapsingTopBar/src/commonMain/kotlin/com/flaringapp/compose/topbar/CollapsingTopBarState.kt @@ -29,6 +29,7 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.saveable.Saver import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue import androidx.compose.runtime.snapshots.Snapshot import com.flaringapp.compose.topbar.snap.CollapsingTopBarSnapScope import kotlin.math.max @@ -91,9 +92,25 @@ public class CollapsingTopBarState @RememberInComposition internal constructor( layoutInfo.isExpanded } + /** + * Whether this state has received actual layout measurements from [CollapsingTopBar]. + * + * Before the first measurement pass, [layoutInfo] contains placeholder values that are safe + * for arithmetic but do not yet represent real measured bounds. + */ + public val hasMeasured: Boolean + get() = hasMeasuredState + + private var hasMeasuredState by mutableStateOf(false) + /** * The layout info object calculated during the last layout pass. * + * Before [hasMeasured] becomes true, this property contains placeholder values rather than + * actual measured bounds. These placeholders are intentionally non-zero to remain safe for + * arithmetic such as division, but consumers should check [hasMeasured] when exact + * measurements are required. + * * Note that this property is observable and is updated after every scroll or remeasure. * If you use it in the composable function it will be recomposed on every change causing * potential performance issues including infinity recomposition loop. @@ -204,6 +221,7 @@ public class CollapsingTopBarState @RememberInComposition internal constructor( collapsedHeight = collapsedHeight, expandedHeight = expandedHeight, ).also { + hasMeasuredState = true layoutInfoState.value = it } } diff --git a/ComposeCollapsingTopBar/src/commonMain/kotlin/com/flaringapp/compose/topbar/scaffold/CollapsingTopBarScaffoldState.kt b/ComposeCollapsingTopBar/src/commonMain/kotlin/com/flaringapp/compose/topbar/scaffold/CollapsingTopBarScaffoldState.kt index 50bd676..6cab6e0 100644 --- a/ComposeCollapsingTopBar/src/commonMain/kotlin/com/flaringapp/compose/topbar/scaffold/CollapsingTopBarScaffoldState.kt +++ b/ComposeCollapsingTopBar/src/commonMain/kotlin/com/flaringapp/compose/topbar/scaffold/CollapsingTopBarScaffoldState.kt @@ -69,13 +69,6 @@ public class CollapsingTopBarScaffoldState @RememberInComposition internal const ) : CollapsingTopBarControls, CollapsingTopBarSnapScope { - /** - * The current visual top bar height, either during collapse or exit. - */ - @get:FrequentlyChangingValue - public val totalTopBarHeight: Float - get() = topBarState.layoutInfo.height - exitState.exitHeight - /** * Whether top bar is fully expanded and entered. */ @@ -91,6 +84,22 @@ public class CollapsingTopBarScaffoldState @RememberInComposition internal const (!exitState.isEnabled || exitState.isFullyExited) } + /** + * Whether [topBarState] has received actual layout measurements. + */ + public val hasMeasured: Boolean + get() = topBarState.hasMeasured + + /** + * The current visual top bar height, either during collapse or exit. + * + * Before [hasMeasured] becomes true, this property is based on placeholder layout values + * rather than actual measured bounds. + */ + @get:FrequentlyChangingValue + public val totalTopBarHeight: Float + get() = topBarState.layoutInfo.height - exitState.exitHeight + override suspend fun expand(animationSpec: AnimationSpec) { animateHeightTo(animationSpec) { topBarState.layoutInfo.expandedHeight.toFloat() diff --git a/README.md b/README.md index d14e732..281f2d3 100644 --- a/README.md +++ b/README.md @@ -585,8 +585,13 @@ Contains information about the collapsing state up until top bar starts exiting mode supports that). Offers state data similar to scaffold: `state.isExpanded` and `state.isCollapsed`, as well as manual controls: `state.expand()` and `state.collapse()`. -This state exposes comprehensive measurement data via `layoutInfo`, such as collapse progress -`layoutInfo.collapseProgress`, collapsed height `layoutInfo.collapsedHeight` etc. +This state also exposes `hasMeasured` to indicate whether the top bar has already received +actual layout measurements. + +Comprehensive measurement data is available via `layoutInfo`, such as collapse progress +`layoutInfo.collapseProgress`, collapsed height `layoutInfo.collapsedHeight` etc. Before +`hasMeasured` becomes true, `layoutInfo` contains safe placeholder values rather than actual +measured bounds.