Reputation: 66899
As can be seen in official documents there is layout named SubcomposeLayout defined as
Analogue of Layout which allows to subcompose the actual content during the measuring stage for example to use the values calculated during the measurement as params for the composition of the children.
Possible use cases:
You need to know the constraints passed by the parent during the composition and can't solve your use case with just custom Layout or LayoutModifier. See androidx.compose.foundation.layout.BoxWithConstraints.
You want to use the size of one child during the composition of the second child.
You want to compose your items lazily based on the available size. For example you have a list of 100 items and instead of composing all of them you only compose the ones which are currently visible(say 5 of them) and compose next items when the component is scrolled.
I searched Stackoverflow with SubcomposeLayout
keyword but couldn't find anything about it, created this sample code, copied most of it from official document, to test and learn how it works
@Composable
private fun SampleContent() {
Column(
modifier = Modifier
.fillMaxSize()
.verticalScroll(rememberScrollState())
) {
SubComponent(
mainContent = {
Text(
"MainContent",
modifier = Modifier
.background(Color(0xffF44336))
.height(60.dp),
color = Color.White
)
},
dependentContent = {
val size = it
println("🤔 Dependent size: $size")
Column() {
Text(
"Dependent Content",
modifier = Modifier
.background(Color(0xff9C27B0)),
color = Color.White
)
}
}
)
}
}
@Composable
private fun SubComponent(
mainContent: @Composable () -> Unit,
dependentContent: @Composable (IntSize) -> Unit
) {
SubcomposeLayout { constraints ->
val mainPlaceables = subcompose(SlotsEnum.Main, mainContent).map {
it.measure(constraints)
}
val maxSize = mainPlaceables.fold(IntSize.Zero) { currentMax, placeable ->
IntSize(
width = maxOf(currentMax.width, placeable.width),
height = maxOf(currentMax.height, placeable.height)
)
}
layout(maxSize.width, maxSize.height) {
mainPlaceables.forEach { it.placeRelative(0, 0) }
subcompose(SlotsEnum.Dependent) {
dependentContent(maxSize)
}.forEach {
it.measure(constraints).placeRelative(0, 0)
}
}
}
}
enum class SlotsEnum { Main, Dependent }
It's supposed to re-measure a component based on another component size but what this code actually does is a mystery to me.
subcompose
function work?slotId
and can we get slotId in a way?The description for subCompose
function
Performs subcomposition of the provided content with given slotId. Params: slotId - unique id which represents the slot we are composing into. If you have fixed amount or slots you can use enums as slot ids, or if you have a list of items maybe an index in the list or some other unique key can work. To be able to correctly match the content between remeasures you should provide the object which is equals to the one you used during the previous measuring. content - the composable content which defines the slot. It could emit multiple layouts, in this case the returned list of Measurables will have multiple elements.
Can someone explain what it means or/and provide a working sample for SubcomposeLayout
?
Upvotes: 16
Views: 20033
Reputation: 66899
Recently i needed to use almost the same SubcomposeLayout in question. I needed a Slider with a Composable thumb that i needed to get its width so i can set start and end of track and full width of Slider i was getting from BoxWithConstraints.
enum class SlotsEnum {
Slider, Thumb
}
/**
* [SubcomposeLayout] that measure [thumb] size to set Slider's track start and track width.
* @param thumb thumb Composable
* @param slider Slider composable that contains **thumb** and **track** of this Slider.
*/
@Composable
private fun SliderComposeLayout(
modifier: Modifier = Modifier,
thumb: @Composable () -> Unit,
slider: @Composable (IntSize, Constraints) -> Unit
) {
SubcomposeLayout(modifier = modifier) { constraints: Constraints ->
// Subcompose(compose only a section) main content and get Placeable
val thumbPlaceable: Placeable = subcompose(SlotsEnum.Thumb, thumb).map {
it.measure(constraints)
}.first()
// Width and height of the thumb Composable
val thumbSize = IntSize(thumbPlaceable.width, thumbPlaceable.height)
// Whole Slider Composable
val sliderPlaceable: Placeable = subcompose(SlotsEnum.Slider) {
slider(thumbSize, constraints)
}.map {
it.measure(constraints)
}.first()
val sliderWidth = sliderPlaceable.width
val sliderHeight = sliderPlaceable.height
layout(sliderWidth, sliderHeight) {
sliderPlaceable.placeRelative(0, 0)
}
}
}
Measured thumb and send its dimensions as IntSize and Constraints to Slider, and only placed Slider since thumb is already placed insider Slider, placing here creates two thumbs.
And used it as
SliderComposeLayout(
modifier = modifier
.minimumTouchTargetSize()
.requiredSizeIn(
minWidth = ThumbRadius * 2,
minHeight = ThumbRadius * 2,
),
thumb = { thumb() }
) { thumbSize: IntSize, constraints: Constraints ->
val isRtl = LocalLayoutDirection.current == LayoutDirection.Rtl
val width = constraints.maxWidth.toFloat()
val thumbRadiusInPx = (thumbSize.width / 2).toFloat()
// Start of the track used for measuring progress,
// it's line + radius of cap which is half of height of track
// to draw this on canvas starting point of line
// should be at trackStart + trackHeightInPx / 2 while drawing
val trackStart: Float
// End of the track that is used for measuring progress
val trackEnd: Float
val strokeRadius: Float
with(LocalDensity.current) {
strokeRadius = trackHeight.toPx() / 2
trackStart = thumbRadiusInPx.coerceAtLeast(strokeRadius)
trackEnd = width - trackStart
}
// Rest of the code
}
Result
Github link for the code
Upvotes: 5
Reputation: 66899
I made a sample based on the sample provided by official documents and @chuckj's answer but still not sure if this efficient or right way to implement it.
It basically measures longest component sets parent width and remeasures shorter one with minimumWidth
of Constraint
and resizes short one as can be seen in this gif. This is how whatsapp scales quote and message length basically.
Orange and pink containers are Columns, which direct children of DynamicWidthLayout
, that uses SubcomposeLayout
to remeasure.
@Composable
private fun DynamicWidthLayout(
modifier: Modifier = Modifier,
mainContent: @Composable () -> Unit,
dependentContent: @Composable (IntSize) -> Unit
) {
SubcomposeLayout(modifier = modifier) { constraints ->
var mainPlaceables: List<Placeable> = subcompose(SlotsEnum.Main, mainContent).map {
it.measure(constraints)
}
var maxSize =
mainPlaceables.fold(IntSize.Zero) { currentMax: IntSize, placeable: Placeable ->
IntSize(
width = maxOf(currentMax.width, placeable.width),
height = maxOf(currentMax.height, placeable.height)
)
}
val dependentMeasurables: List<Measurable> = subcompose(SlotsEnum.Dependent) {
// 🔥🔥 Send maxSize of mainComponent to
// dependent composable in case it might be used
dependentContent(maxSize)
}
val dependentPlaceables: List<Placeable> = dependentMeasurables
.map { measurable: Measurable ->
measurable.measure(Constraints(maxSize.width, constraints.maxWidth))
}
// Get maximum width of dependent composable
val maxWidth = dependentPlaceables.maxOf { it.width }
println("🔥 DynamicWidthLayout-> maxSize width: ${maxSize.width}, height: ${maxSize.height}")
// If width of dependent composable is longer than main one, remeasure main one
// with dependent composable's width using it as minimumWidthConstraint
if (maxWidth > maxSize.width) {
println("🚀 DynamicWidthLayout REMEASURE MAIN COMPONENT")
// !!! 🔥🤔 CANNOT use SlotsEnum.Main here why?
mainPlaceables = subcompose(2, mainContent).map {
it.measure(Constraints(maxWidth, constraints.maxWidth))
}
}
// Our final maxSize is longest width and total height of main and dependent composables
maxSize = IntSize(
maxSize.width.coerceAtLeast(maxWidth),
maxSize.height + dependentPlaceables.maxOf { it.height }
)
layout(maxSize.width, maxSize.height) {
// Place layouts
mainPlaceables.forEach { it.placeRelative(0, 0) }
dependentPlaceables.forEach {
it.placeRelative(0, mainPlaceables.maxOf { it.height })
}
}
}
}
enum class SlotsEnum { Main, Dependent }
Usage
@Composable
private fun TutorialContent() {
val density = LocalDensity.current.density
Column(
modifier = Modifier
.fillMaxSize()
.verticalScroll(rememberScrollState())
) {
var mainText by remember { mutableStateOf(TextFieldValue("Main Component")) }
var dependentText by remember { mutableStateOf(TextFieldValue("Dependent Component")) }
OutlinedTextField(
modifier = Modifier
.padding(horizontal = 8.dp)
.fillMaxWidth(),
value = mainText,
label = { Text("Main") },
placeholder = { Text("Set text to change main width") },
onValueChange = { newValue: TextFieldValue ->
mainText = newValue
}
)
OutlinedTextField(
modifier = Modifier
.padding(horizontal = 8.dp)
.fillMaxWidth(),
value = dependentText,
label = { Text("Dependent") },
placeholder = { Text("Set text to change dependent width") },
onValueChange = { newValue ->
dependentText = newValue
}
)
DynamicWidthLayout(
modifier = Modifier
.padding(8.dp)
.background(Color.LightGray)
.padding(8.dp),
mainContent = {
println("🍏 DynamicWidthLayout-> MainContent {} composed")
Column(
modifier = Modifier
.background(orange400)
.padding(4.dp)
) {
Text(
text = mainText.text,
modifier = Modifier
.background(blue400)
.height(40.dp),
color = Color.White
)
}
},
dependentContent = { size: IntSize ->
// 🔥 Measure max width of main component in dp retrieved
// by subCompose of dependent component from IntSize
val maxWidth = with(density) {
size.width / this
}.dp
println(
"🍎 DynamicWidthLayout-> DependentContent composed " +
"Dependent size: $size, "
+ "maxWidth: $maxWidth"
)
Column(
modifier = Modifier
.background(pink400)
.padding(4.dp)
) {
Text(
text = dependentText.text,
modifier = Modifier
.background(green400),
color = Color.White
)
}
}
)
}
}
And full source code is here.
Upvotes: 12
Reputation: 29545
It's supposed to re-measure a component based on another component size...
SubcomposeLayout
doesn't remeasure. It allows deferring the composition and measure of content until its constraints from its parent are known and some its content can be measured, the results from which and can be passed as a parameter to the deferred content. The above example calculates the maximum size of the content generated by mainContent
and passes it as a parameter to deferredContent
. It then measures deferredContent
and places both mainContent
and deferredContent
on top of each other.
The simplest example of how to use SubcomposeLayout
is BoxWithConstraints that just passes the constraints it receives from its parent directly to its content. The constraints of the box are not known until the siblings of the box have been measured by the parent which occurs during layout so the composition of content
is deferred until layout.
Similarly, for the example above, the maxSize
of mainContent
is not known until layout so deferredContent
is called in layout once maxSize
is calculated. It always places deferredContent
on top of mainContent
so it is assumed that deferredContent
uses maxSize
in some way to avoid obscuring the content generated by mainContent
. Probably not the best design for a composable but the composable was intended to be illustrative not useful itself.
Note that subcompose
can be called multiple times in the layout
block. This is, for example, what happens in LazyRow
. The slotId
allows SubcomposeLayout
to track and manage the compositions created by calling subcompose
. For example, if you are generating the content from an array you might want use the index of the array as its slotId
allowing SubcomposeLayout
to determine which subcompose
generated last time should be used to during recomposition. Also, if a slotid
is not used any more, SubcomposeLayout
will dispose its corresponding composition.
As for where the slotId
goes, that is up to the caller of SubcomposeLayout
. If the content needs it, pass it as a parameter. The above example doesn't need it as the slotId
is always the same for deferredContent
so it doesn't need to go anywhere.
Upvotes: 26