Is there a way to get the size of a nested child c...
# compose-android
j
Is there a way to get the size of a nested child composable in a custom layout ?
Copy code
Layout(
    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.
z
You can use
Modifier.onSizeChanged
on the child and store the size somewhere where you measure policy can access it.
b
@Zach Klippenstein (he/him) [MOD] not sure but can we do it this way?
Copy code
Layout(
    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)
    
}
z
Oh if it’s a direct child, then yea. If you want to get the actual size of the VideoPlayer node that’s inside the Column, then you’d have to do it less directly.
j
The video player wouldn't be a direct child. It would be wrapped in a column so that I can apply a custom background & shape behind the video and video description composables.
b
Ok then i think best is to save using onSizeChanged modifier in mutablestate and remember that ans use it right @Zach Klippenstein (he/him) [MOD]
j
Would it be more performant to use an Modifier.onSizeChanged {} instead of a SubCompose layout?
z
Subcomposition is extremely expensive, onSizeChanged is almost free.
j
Thanks for the info guys. I’m currently working on rewriting my custom Layout to use the
Modifier.onSizeChanged {}
modifier.
🙌 1
I’ve tried implementing my layout with
Modifier.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?
z
You’re saying
MutableIntState
is broken? I would be very surprised, lots of things would be broken then.
Hard to say anything without seeing your code
j
I can share my layout
Copy code
@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
}
Here is a hopefully runnable example
Copy code
val 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,
        )
    }
}
z
Will take a look when I am by a bigger screen than my phone, but first thing I noticed is
PlayerSheetScaffoldAlt
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.
j
I based my design on
BottomSheetScaffold
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…
I would like to add I have the basic of the layout working. The issue is only with shrinking the video player… This assumes there aren’t hiding performance issues.
z
I'm not sure what's wrong, my best guess is that resizing the parent of the draggable while dragging is messing up the calculations. If that's the case, it would probably be easier to implement if the draggable were on a node that stayed fixed while dragging. But i'm just guessing, not sure if you'd see the same problem even if you did that.
j
The version I see when I cmd + left click shows a different file. I’m using compose BOM “2025.05.01”
z
material 2 or 3?
j
Material 3
z
i'm not sure what code you're seeing, but it sounds like maybe it was doing the wrong thing but they fixed it
j
I will correct my code.
Out of curiosity, what solution would you explore if you wanted to implement a similar “video player sheet” to the YouTube app? I’m going back to considering trying w/ a
SubcomposeLayout.
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.
z
This is a simplified version of what you're trying to do,
anchoredDraggable
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.
j
This looks good… I do have a few questions the video players parent box is locked into place. 1. It lays on top of content below it. Should the
Modifier.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.
z
i think the anchorDraggable needs to be placed on whatever you want to allow to start the drag gesture
idk if you need a custom layout for the navbar, there might be a simpler way
j
What about the parent box overlaying the main content?
z
What about it?
j
I know that the Box won’t intercept taps by default but I’m unsure if this would cause issues with screen readers.
z
it should be fine