sindrenm
10/23/2024, 4:27 PMhero
and content
. If there is space enough in the component to render both slots without needing to scroll, then both slots are placed. If not, only content
is placed, and hero
is not. I got this to work quite easily with a custom Layout
and just matching the full height of all the content against the max height of the layout. (See attached image, and code in thread). However, I run into trouble when I also want to make this vertically scrollable, since that makes the max height “infinite”. Any help would be appreciated!sindrenm
10/23/2024, 4:27 PM@Composable
fun OptionalHeroColumn(
hero: @Composable () -> Unit,
content: @Composable () -> Unit,
modifier: Modifier = Modifier,
) {
Box(modifier) {
Layout(contents = listOf(hero, content)) { measurables, constraints ->
val heroMeasurable = measurables[0]
val contentMeasurable = measurables[1]
val heroPlaceables = heroMeasurable.map { it.measure(constraints) }
val heroHeight = heroPlaceables.sumOf { it.height }
val contentPlaceables = contentMeasurable.map { it.measure(constraints) }
val contentHeight = contentPlaceables.sumOf { it.height }
val totalHeight = heroHeight + contentHeight
layout(constraints.maxWidth, constraints.maxHeight) {
var y = 0
if (totalHeight <= constraints.maxHeight) {
heroPlaceables.forEach { heroPlaceable ->
heroPlaceable.place(x = 0, y = y)
y += heroPlaceable.height
}
}
contentPlaceables.forEach { contentPlaceable ->
contentPlaceable.place(x = 0, y = y)
y += contentPlaceable.height
}
}
}
}
}
@Preview("Non-scrolling – height: 300 dp", heightDp = 300)
@Preview("Non-scrolling – height: 500 dp", heightDp = 500)
@Composable
private fun SamplePreview() {
OptionalHeroColumn(
hero = { HeroContent() },
content = { BodyContent() },
modifier = Modifier.fillMaxSize(),
)
}
Stylianos Gakis
10/23/2024, 4:33 PMStylianos Gakis
10/23/2024, 4:35 PMsindrenm
10/23/2024, 5:10 PMcontent
is a couple of buttons that we would prefer (if possible, given the space) to show, and we're willing to remove the hero
for that to happen. But if it's still not possible, it's okay to have it placed outside the boundaries of the component.Stylianos Gakis
10/23/2024, 5:18 PMAlex Vanyo
10/23/2024, 5:37 PMModifier.onSizeChanged
will probably result in the available size being retrieved too late, so you may want to use a Modifier.layout
outside of the scrolling areasindrenm
10/23/2024, 6:48 PMonSizeChanged { fullScreenSize = it }
or similar, and achieve first-frame correctness and avoid extra recompositions.Stylianos Gakis
10/23/2024, 6:53 PMAlex Vanyo
10/23/2024, 7:13 PMvar outerConstraints: Constraints? by remember { mutableStateOf(null) }
Box(
modifier
.layout { measurable, constraints ->
outerConstraints = constraints
val placeable = measurable.measure(constraints)
layout(placeable.width, placeable.height) {
placeable.placeRelative(0, 0)
}
}
// instead of
//.onSizeChanged { }
.verticalScroll()
If you retrieve the constraints before measuring into a state holder, then the children can have access to the correct value as they’re being measured if they retrieve from the state holder
I think this gets a bit more complicated once lookahead and related things start happening, but not having a correct first frame already messes with that anywayStylianos Gakis
10/23/2024, 7:22 PMsindrenm
10/23/2024, 7:23 PMAlex Vanyo
10/23/2024, 7:33 PMsindrenm
10/23/2024, 7:46 PMsindrenm
10/23/2024, 8:04 PM@Preview("Scrolling – height: 300 dp", heightDp = 300)
@Preview("Scrolling – height: 500 dp", heightDp = 500)
@Composable
private fun ScrollableOptionalHeroColumnPreview() {
ScrollableOptionalHeroColumn(
hero = { HeroContent() },
content = { BodyContent() },
modifier = Modifier.fillMaxSize(),
)
}
However, if I comment out the first @Preview
annotation, I get to see the larger preview working (second picture).sindrenm
10/23/2024, 8:06 PMStylianos Gakis
11/15/2024, 11:51 PMStylianos Gakis
11/16/2024, 1:04 AMvar layoutSize: IntSize? by remember { mutableStateOf(null) }
Box(
Modifier
.onGloballyPositioned { outer = it.size }
.fillMaxSize(),
) {
Box(
Modifier
.layout { measurable, constraints ->
logcat { "#1 outer:$layoutSize" }
val placeable = measurable.measure(constraints)
logcat { "#2 outer:$layoutSize" }
layout(placeable.width, placeable.height) {
logcat { "#3 outer:$layoutSize" }
placeable.placeRelative(0, 0)
logcat { "#4 outer:$layoutSize" }
}
},
)
}
Case 2 - onPlaced:
var layoutSize: IntSize? by remember { mutableStateOf(null) }
Box(
Modifier
.onPlaced { outer = it.size }
.fillMaxSize(),
) {
Box(... same as above)
}
Case 3 - layout:
var layoutSize: IntSize? by remember { mutableStateOf(null) }
Box(
Modifier
.layout { measurable, constraints ->
layoutSize = IntSize(constraints.maxWidth, constraints.maxHeight)
val placeable = measurable.measure(constraints)
layout(placeable.width, placeable.height) {
placeable.placeRelative(0, 0)
}
}
.fillMaxSize(),
) {
Box(... same as above)
}
I got these logs:
Case 1 - onGloballyPositioned:
#1 outer:null
#2 outer:null
#3 outer:null
#4 outer:null
#1 outer:1440 x 3120
#2 outer:1440 x 3120
#3 outer:1440 x 3120
#4 outer:1440 x 3120
Case 2 - onPlaced:
#1 outer:null
#2 outer:null
#3 outer:1440 x 3120
#4 outer:1440 x 3120
#1 outer:1440 x 3120
#2 outer:1440 x 3120
#3 outer:1440 x 3120
#4 outer:1440 x 3120
Case 3 - layout:
#1 outer:1440 x 3120
#2 outer:1440 x 3120
#3 outer:1440 x 3120
#4 outer:1440 x 3120
#1 outer:1440 x 3120
#2 outer:1440 x 3120
#3 outer:1440 x 3120
#4 outer:1440 x 3120
Stylianos Gakis
11/16/2024, 1:04 AMsindrenm
11/19/2024, 11:31 AM