Jonathan
07/18/2025, 7:37 PMLayout(
content = {
MainContent()
Column {
VideoPlayer(Modifier.fillMaxWidth())
VideoDescription(Modifeir.fillMaxWidth().weight(1F))
}
},
) { children, constraints ->
layout(width = ..., height = ...) {
}
}
In my example above, I’d like to fetch the size of the VideoPlayer
child composable so that I can use it in the measurePolicy
block to set internal state.Zach Klippenstein (he/him) [MOD]
07/19/2025, 2:10 PMModifier.onSizeChanged
on the child and store the size somewhere where you measure policy can access it.bk9735732777
07/22/2025, 7:42 AMLayout(
content = {
MainContent()
VideoPlayer(Modifier.layoutId("player"))
Column(Modifier.layoutId("description_column")) {
VideoDescription(Modifier.fillMaxWidth())
}
}
) { measurables, constraints ->
val videoPlayerMeasurables = measurables.first { it.layoutId == "player" }
val playerPlaceable = videoPlayerMeasurables.measure(constraints)
}
Zach Klippenstein (he/him) [MOD]
07/22/2025, 11:32 AMJonathan
07/22/2025, 12:13 PMbk9735732777
07/22/2025, 12:57 PMJonathan
07/22/2025, 1:31 PMZach Klippenstein (he/him) [MOD]
07/22/2025, 4:00 PMJonathan
07/22/2025, 4:01 PMModifier.onSizeChanged {}
modifier.Jonathan
07/30/2025, 2:35 PMModifier.onSizeChanged {}
. I’m now running into recomposition issues.
My custom layout is similar to the Youtube player layout. When the player sheet is minimized I’d like to shrink the video player.
I’ve attached a Modifier.layout {}
to my video player to change the size as the item is shrinking. The problem is I need to calculate new anchors for the AnchoredDraggableState
so it knows where to stop the vertical drag. Originally I was saving the video player size in a IntState but the parent layout wouldn’t detect the new size and wouldn’t calculate new drag anchors. I changed the video player size property to be a mutableStateOf
and now the layout detects with the size is changed but when I animate the change between anchors the layout becomes choppy and taps are intercepted.
Any suggestions?Zach Klippenstein (he/him) [MOD]
07/30/2025, 3:17 PMMutableIntState
is broken? I would be very surprised, lots of things would be broken then.Zach Klippenstein (he/him) [MOD]
07/30/2025, 3:17 PMJonathan
07/30/2025, 3:18 PMJonathan
07/30/2025, 3:19 PM@Composable
fun rememberPlayerSheetScaffoldState(
dragInitialEnabled: Boolean = true,
initialValue: PlayerSheetValue = Hidden,
): PlayerSheetScaffoldState = rememberSaveable(
dragInitialEnabled,
initialValue,
saver = Saver()
) {
PlayerSheetScaffoldState(
dragInitialEnabled = dragInitialEnabled,
initialValue = initialValue,
)
}
class PlayerSheetScaffoldState(
dragInitialEnabled: Boolean,
initialValue: PlayerSheetValue = Hidden,
) {
internal var anchoredDraggableState =
AnchoredDraggableState(initialValue = initialValue)
var dragEnabled by mutableStateOf(dragInitialEnabled)
internal var sheetSize = IntSize.Zero
internal var videoPlayerSize by mutableStateOf(IntSize.Zero)
internal var didLookAhead: Boolean = false
internal fun requireVideoPlayerSize(): IntSize {
checkPrecondition(!videoPlayerSize.noSize) {
"The videoPlayerSize was read before being initialized. Did you access the videoPlayerSize in a phase " +
"before layout, like effects or composition?"
}
return videoPlayerSize
}
internal fun requireSheetSize(): IntSize {
checkPrecondition(!sheetSize.noSize) {
"The sheetSize was read before being initialized. Did you access the sheetSize in a phase " +
"before layout, like effects or composition?"
}
return sheetSize
}
val isDragging: Boolean
get() {
if (anchoredDraggableState.anchors.size < 1) return false
val offset = anchoredDraggableState.requireOffset()
anchoredDraggableState.anchors.positionOf(Expanded)
.let { if (it == offset) return false }
anchoredDraggableState.anchors.positionOf(Minimized)
.let { if (it == offset) return false }
anchoredDraggableState.anchors.positionOf(Hidden)
.let { if (it == offset) return false }
return true
}
val currentValue: PlayerSheetValue get() = anchoredDraggableState.currentValue
val isMinimized: Boolean by derivedStateOf { !anchoredDraggableState.isAnimationRunning && !isDragging && currentValue == Minimized }
val isExpanded: Boolean by derivedStateOf { !anchoredDraggableState.isAnimationRunning && !isDragging && currentValue == Expanded }
val expandedProgress: Float get() = anchoredDraggableState.progress(Minimized, Expanded)
val minimizedProgress: Float get() = anchoredDraggableState.progress(Expanded, Minimized)
val scaledExpansionProgress: Float
get() = expandedProgress
.mapSpace(
targetMinValue = 0.5F,
targetMaxValue = 1F,
sourceMinValue = 0F,
sourceMaxValue = 1F
)
internal val minimizedAnchor: Float
get() = anchoredDraggableState.anchors.positionOf(Minimized)
suspend fun toggle() {
if (isExpanded) {
minimize()
} else expand()
}
suspend fun expand() = anchoredDraggableState.animateTo(Expanded)
suspend fun minimize() = anchoredDraggableState.animateTo(Minimized)
suspend fun hide() = anchoredDraggableState.animateTo(Hidden)
companion object {
fun Saver() = Saver<PlayerSheetScaffoldState, Pair<Boolean, PlayerSheetValue>>(
save = { it.dragEnabled to it.currentValue },
restore = { (dragEnabled, currentValue) ->
PlayerSheetScaffoldState(
dragInitialEnabled = dragEnabled,
initialValue = currentValue,
)
}
)
}
}
@Composable
private fun PlayerSheet(
playerSheetState: PlayerSheetScaffoldState,
player: @Composable () -> Unit,
playerContent: @Composable ColumnScope.() -> Unit,
modifier: Modifier = Modifier,
) {
Column(modifier = modifier) {
Box(
modifier = Modifier
.anchoredDraggable(
state = playerSheetState.anchoredDraggableState,
orientation = Orientation.Vertical,
enabled = true,
)
.onSizeChanged {
playerSheetState.videoPlayerSize = it
},
content = { player() },
)
playerContent()
}
}
@Composable
fun PlayerSheetScaffoldAlt(
sheetState: PlayerSheetScaffoldState,
modifier: Modifier = Modifier,
player: @Composable () -> Unit,
playerContent: @Composable ColumnScope.() -> Unit,
bottomBar: @Composable () -> Unit,
mainContent: @Composable () -> Unit,
) {
Layout(
content = {
Box(
modifier = Modifier
.fillMaxSize()
.then(modifier),
content = { mainContent() },
)
PlayerSheet(
playerSheetState = sheetState,
player = player,
playerContent = playerContent,
modifier = Modifier,
)
Box(
modifier = Modifier
.wrapContentSize()
.then(modifier),
content = { bottomBar() },
)
}
) { (mainContentMeasurables, sheetContentMeasurables, bottomBarMeasurables), constraints ->
val layoutWidth = constraints.maxWidth
val layoutHeight = constraints.maxHeight
val looseConstraints = constraints.copy(minWidth = 0, minHeight = 0)
val mainContentPlaceables = mainContentMeasurables.measure(looseConstraints)
val bottomBarPlaceables = bottomBarMeasurables.measure(looseConstraints)
val bottomBarHeight = bottomBarPlaceables.height
val sheetContentPlaceables = sheetContentMeasurables.measure(looseConstraints)
if (!isLookingAhead || !sheetState.didLookAhead) {
val sheetSize = IntSize(layoutWidth, layoutHeight)
sheetState.sheetSize = sheetSize
val newAnchorResult = calculateAnchors(
state = sheetState
.anchoredDraggableState,
bottomBarHeight = bottomBarHeight,
videoContentHeight = sheetState.requireVideoPlayerSize().height,
sheetSize = sheetSize,
constraints = constraints,
)
sheetState
.anchoredDraggableState
.updateAnchors(
newAnchors = newAnchorResult.first,
newTarget = newAnchorResult.second
)
}
sheetState.didLookAhead = isLookingAhead || sheetState.didLookAhead
layout(layoutWidth, layoutHeight) {
mainContentPlaceables.place(x = 0, y = 0)
val sheetY = (if (isLookingAhead) {
sheetState
.anchoredDraggableState
.anchors
.positionOf(anchor = sheetState.anchoredDraggableState.targetValue)
} else sheetState.anchoredDraggableState.requireOffset()).roundToInt()
// Horizontally center sheet
val sheetX = (layoutWidth - sheetContentPlaceables.width) / 2
sheetContentPlaceables.place(x = sheetX, y = sheetY)
val bottomBarYOffset =
layoutHeight - (bottomBarHeight * sheetState.minimizedProgress).toInt()
bottomBarPlaceables.place(x = 0, y = bottomBarYOffset)
}
}
}
private fun calculateAnchors(
state: AnchoredDraggableState<PlayerSheetValue>,
bottomBarHeight: Int,
videoContentHeight: Int,
sheetSize: IntSize,
constraints: Constraints,
): Pair<DraggableAnchors<PlayerSheetValue>, PlayerSheetValue> {
val layoutHeight = constraints.maxHeight.toFloat()
val sheetHeight = sheetSize.height.toFloat()
val newAnchors = DraggableAnchors {
Minimized at maxOf((layoutHeight - (bottomBarHeight + videoContentHeight)), 0F)
Expanded at maxOf((layoutHeight - sheetHeight), 0F)
Hidden at layoutHeight
}
val newTarget =
when (val oldTarget = state.targetValue) {
Hidden -> if (newAnchors.hasPositionFor(Hidden)) Hidden else oldTarget
Minimized ->
when {
newAnchors.hasPositionFor(Minimized) -> Minimized
newAnchors.hasPositionFor(Expanded) -> Expanded
newAnchors.hasPositionFor(Hidden) -> Hidden
else -> oldTarget
}
Expanded ->
when {
newAnchors.hasPositionFor(Expanded) -> Expanded
newAnchors.hasPositionFor(Minimized) -> Minimized
newAnchors.hasPositionFor(Hidden) -> Hidden
else -> oldTarget
}
}
return newAnchors to newTarget
}
Jonathan
07/30/2025, 3:21 PMval sheetState = rememberPlayerSheetScaffoldState(
initialValue = PlayerSheetValue.Expanded
)
PlayerSheetScaffoldAlt(
sheetState = sheetState,
player = {
Box(
modifier = Modifier
.background(Color.LightGray)
.aspectRatio(ratio = 16F/9F)
.clip(RectangleShape),
)
},
playerContent = {
Box(
modifier = Modifier
.background(Color.DarkGray)
.aspectRatio(ratio = 16F/9F)
.clip(RectangleShape),
)
},
bottomBar = {
Box(
modifier = Modifier
.fillMaxWidth()
.height(64.dp)
.background(Color.Yellow)
)
},
) {
Column(
Modifier
.statusBarsPadding()
.background(Color.White)
.fillMaxSize()
) {
Text(
text = "Example Test",
fontSize = 25.sp,
)
}
}
Zach Klippenstein (he/him) [MOD]
07/30/2025, 3:27 PMPlayerSheetScaffoldAlt
is doing some bad stuff with modifier: it’s passing the external modifier not to its outermost node but some nested ones, and it’s passing it to multiple composables. That’s gonna give you some really weird behavior, although not necessarily the issue you’re seeing.Jonathan
07/30/2025, 3:31 PMBottomSheetScaffold
where they pass the modifier to the “main content”… It was an accident on my part to pass the modifier to the bottomBar
composable…. I assume the bottom sheet scaffold passed the modifier to the “main content” because that would be the most intuitive to users so I mirrored it…Jonathan
07/30/2025, 3:35 PMZach Klippenstein (he/him) [MOD]
07/30/2025, 5:11 PMZach Klippenstein (he/him) [MOD]
07/30/2025, 5:13 PMBottomSheetScaffold
correctly passes the modifier
to the outermost layout: https://cs.android.com/androidx/platform/frameworks/support/+/androidx-main:compose/[…]caffold.kt;l=137;drc=b69e22dd2a026e4fe0e215ea477a12c449295bedJonathan
07/30/2025, 5:16 PMZach Klippenstein (he/him) [MOD]
07/30/2025, 5:16 PMJonathan
07/30/2025, 5:16 PMZach Klippenstein (he/him) [MOD]
07/30/2025, 5:17 PMJonathan
07/30/2025, 5:17 PMJonathan
07/30/2025, 5:19 PMSubcomposeLayout.
Then I could measure the video and the video content separately then wrap them in a custom sheet composable that I could manually style. Not sure if I’m overthinking the solution and there is a more optimal solution.Zach Klippenstein (he/him) [MOD]
07/30/2025, 7:18 PManchoredDraggable
seems to work fine here. I'm not sure where in the diff between this and your thing you get the jank, but it seems like the basic idea should work.Jonathan
07/30/2025, 7:35 PMModifier.anchoredDraggable(state, orientation = Vertical)
be placed on it instead?
2. If I wanted to have a bottom navbar peek as the sheet is expanded and when collapsed the sheet docks to the top of the bottom navbar. I think this could be achieved with a custom layout like I had before but instead of allowing the sheet to fill the entire screen it would be layoutHeight - bottomBarHeight. Do you think this would work?
I’ve attached a video of what I had minus the scaling.Zach Klippenstein (he/him) [MOD]
07/30/2025, 7:53 PMZach Klippenstein (he/him) [MOD]
07/30/2025, 7:54 PMJonathan
07/30/2025, 7:55 PMZach Klippenstein (he/him) [MOD]
07/30/2025, 7:56 PMJonathan
07/30/2025, 7:56 PMZach Klippenstein (he/him) [MOD]
07/30/2025, 7:57 PM