Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions ComposeCollapsingTopBar/api/ComposeCollapsingTopBar.klib.api
Original file line number Diff line number Diff line change
Expand Up @@ -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 <get-exitState>(): com.flaringapp.compose.topbar.dependent/CollapsingTopBarExitState // com.flaringapp.compose.topbar.scaffold/CollapsingTopBarScaffoldState.exitState.<get-exitState>|<get-exitState>(){}[0]
final val hasMeasured // com.flaringapp.compose.topbar.scaffold/CollapsingTopBarScaffoldState.hasMeasured|{}hasMeasured[0]
final fun <get-hasMeasured>(): kotlin/Boolean // com.flaringapp.compose.topbar.scaffold/CollapsingTopBarScaffoldState.hasMeasured.<get-hasMeasured>|<get-hasMeasured>(){}[0]
final val isCollapsed // com.flaringapp.compose.topbar.scaffold/CollapsingTopBarScaffoldState.isCollapsed|{}isCollapsed[0]
final fun <get-isCollapsed>(): kotlin/Boolean // com.flaringapp.compose.topbar.scaffold/CollapsingTopBarScaffoldState.isCollapsed.<get-isCollapsed>|<get-isCollapsed>(){}[0]
final val isExpanded // com.flaringapp.compose.topbar.scaffold/CollapsingTopBarScaffoldState.isExpanded|{}isExpanded[0]
Expand Down Expand Up @@ -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 <init>(kotlin/Boolean = ...) // com.flaringapp.compose.topbar/CollapsingTopBarState.<init>|<init>(kotlin.Boolean){}[0]

final val hasMeasured // com.flaringapp.compose.topbar/CollapsingTopBarState.hasMeasured|{}hasMeasured[0]
final fun <get-hasMeasured>(): kotlin/Boolean // com.flaringapp.compose.topbar/CollapsingTopBarState.hasMeasured.<get-hasMeasured>|<get-hasMeasured>(){}[0]
final val isCollapsed // com.flaringapp.compose.topbar/CollapsingTopBarState.isCollapsed|{}isCollapsed[0]
final fun <get-isCollapsed>(): kotlin/Boolean // com.flaringapp.compose.topbar/CollapsingTopBarState.isCollapsed.<get-isCollapsed>|<get-isCollapsed>(){}[0]
final val isExpanded // com.flaringapp.compose.topbar/CollapsingTopBarState.isExpanded|{}isExpanded[0]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -204,6 +221,7 @@ public class CollapsingTopBarState @RememberInComposition internal constructor(
collapsedHeight = collapsedHeight,
expandedHeight = expandedHeight,
).also {
hasMeasuredState = true
layoutInfoState.value = it
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*/
Expand All @@ -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<Float>) {
animateHeightTo(animationSpec) {
topBarState.layoutInfo.expandedHeight.toFloat()
Expand Down
9 changes: 7 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

</details>

Expand Down
Loading