I need size of my composable to draw dynamic lines but I don't want to get size by:
var size by remember { mutableStateOf(IntSize.Zero) }
Modifier.onSizeChanged{size = it}
or
Modifier.onGloballyPositioned{size = it.size}
because I don't want to recompose.
Currently I am getting size from BoxWithConstraints and passing as parameter like this:
fun DrawLines(intSize:IntSize){
// handle lines
}
Is there any better approach or that's all I can do for now?
Thanks for help.
If you are gonna use size of your Composable for drawing lines you can change your function to extension of DrawScope which returns size of your Composable. If that's not the case check answer below.
fun DrawScope.drawLine() {
this.size
}
And call this function inside either of
Modifier.drawBehind{}, Modifier.drawWithContent{} or Modifier.drawWithCache{}. Also you can pass size inside these Modifiers if you don't want to change your function.
BoxWithConstraints and SubcomposeLayout
BoxWithConstraints is not always reliable to get exact size of your content, it, as the name suggests, is good for getting Constraints. I have a detailed answer about Constraints and what BoxConstraints return with which size modifier here. You can check out Constraints section of answer to examine outcomes with each Modifier.
For instance
BoxWithConstraints() {
Text(
modifier = Modifier
.size(200.dp)
.border(2.dp, Color.Red),
text = "Constraints: ${constraints.minWidth}, max: ${constraints.maxWidth}"
)
}
Will return minWidth = 0px, and maxWidth 1080px(width of my device in px) instead of 525px which is 200.dp in my device.
And you can't get dimensions from Layout alone without recomposing either that's why BoxWithConstraints uses SubcomposeLayout to pass Constraints to content. You can check out this question to learn about SubcomposeLayout.
BoxWithConstraints source code
#Composable
#UiComposable
fun BoxWithConstraints(
modifier: Modifier = Modifier,
contentAlignment: Alignment = Alignment.TopStart,
propagateMinConstraints: Boolean = false,
content:
#Composable #UiComposable BoxWithConstraintsScope.() -> Unit
) {
val measurePolicy = rememberBoxMeasurePolicy(contentAlignment, propagateMinConstraints)
SubcomposeLayout(modifier) { constraints ->
val scope = BoxWithConstraintsScopeImpl(this, constraints)
val measurables = subcompose(Unit) { scope.content() }
with(measurePolicy) { measure(measurables, constraints) }
}
}
SubcomposeLayout 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.
In the implementation below, it can be customized as required, i use several versions of it based on why it's needed.
You can customize how you sum or max width or height, what will be
layout width or height, how you place your items to have a behavior like Row, Column or Box depends on your needs. You can limit to one Composable or multiple ones is up to you. Only thing that is required is passing Size/IntSize/DpSize from one Composable to another.
/**
* SubcomposeLayout that [SubcomposeMeasureScope.subcompose]s [mainContent]
* and gets total size of [mainContent] and passes this size to [dependentContent].
* This layout passes exact size of content unlike
* BoxWithConstraints which returns [Constraints] that doesn't match Composable dimensions under
* some circumstances
*
* #param placeMainContent when set to true places main content. Set this flag to false
* when dimensions of content is required for inside [mainContent]. Just measure it then pass
* its dimensions to any child composable
*
* #param mainContent Composable is used for calculating size and pass it
* to Composables that depend on it
*
* #param dependentContent Composable requires dimensions of [mainContent] to set its size.
* One example for this is overlay over Composable that should match [mainContent] size.
*
*/
#Composable
fun DimensionSubcomposeLayout(
modifier: Modifier = Modifier,
placeMainContent: Boolean = true,
mainContent: #Composable () -> Unit,
dependentContent: #Composable (Size) -> Unit
) {
SubcomposeLayout(
modifier = modifier
) { constraints: Constraints ->
// Subcompose(compose only a section) main content and get Placeable
val mainPlaceables: List<Placeable> = subcompose(SlotsEnum.Main, mainContent)
.map {
it.measure(constraints.copy(minWidth = 0, minHeight = 0))
}
// Get max width and height of main component
var maxWidth = 0
var maxHeight = 0
mainPlaceables.forEach { placeable: Placeable ->
maxWidth += placeable.width
maxHeight = placeable.height
}
val dependentPlaceables: List<Placeable> = subcompose(SlotsEnum.Dependent) {
dependentContent(Size(maxWidth.toFloat(), maxHeight.toFloat()))
}
.map { measurable: Measurable ->
measurable.measure(constraints)
}
layout(maxWidth, maxHeight) {
if (placeMainContent) {
mainPlaceables.forEach { placeable: Placeable ->
placeable.placeRelative(0, 0)
}
}
dependentPlaceables.forEach { placeable: Placeable ->
placeable.placeRelative(0, 0)
}
}
}
}
enum class SlotsEnum { Main, Dependent }
Usage
val content = #Composable {
Box(
modifier = Modifier
.size(200.dp)
.background(Color.Red)
)
}
val density = LocalDensity.current
DimensionSubcomposeLayout(
mainContent = { content() },
dependentContent = { size: Size ->
content()
val dpSize = density.run {size.toDpSize() }
Box(Modifier.size(dpSize).border(3.dp, Color.Green))
},
placeMainContent = false
)
or
DimensionSubcomposeLayout(
mainContent = { content() },
dependentContent = { size: Size ->
val dpSize = density.run {size.toDpSize() }
Box(Modifier.size(dpSize).border(3.dp, Color.Green))
}
)
Result
In example below we set size of Box with green border based on Box with red background. This can be complicated for a beginner but that's how you get dimensions without recomposing a Composable. SubcomposeLayout question and answers in the link provided above might help. I posted several answers and linked other answers show how to use it.
Extra Section
Layouts, Scopes, and Constraining Siblings
You can use layout in similar way Box, Row, Column does with a scope to pass information from inside to content using an interface, implementation and changing properties of this implementation
interface DimensionScope {
var size: Size
}
class DimensionScopeImpl(override var size: Size = Size.Zero) : DimensionScope
And implementing DimensionScope and Layout.
#Composable
private fun DimensionLayout(
modifier: Modifier = Modifier,
content: #Composable DimensionScope.() -> Unit
) {
val dimensionScope = remember{DimensionScopeImpl()}
Layout(
modifier = modifier,
// 🔥 since we invoke it here it will have Size.Zero
// on Composition then will have size value below
content = { dimensionScope.content() }
) { measurables: List<Measurable>, constraints: Constraints ->
val placeables = measurables.map { measurable: Measurable ->
measurable.measure(constraints)
}
val maxWidth = placeables.maxOf { it.width }
val maxHeight = placeables.maxOf { it.height }
dimensionScope.size = Size(maxWidth.toFloat(), maxHeight.toFloat())
layout(maxWidth, maxHeight) {
placeables.forEach { placeable: Placeable ->
placeable.placeRelative(0, 0)
}
}
}
}
Since we invoke before being able to measure, and with Layout we can only measure once, we won't be able to pass correct Size to DimensionScopeImpl on first composition as i mentioned above. On next recompositions since we remember DimensionScopeImpl we get the correct size and Text size is correctly set and we see Text with border.
Column(modifier = Modifier.fillMaxSize().padding(20.dp)) {
val density = LocalDensity.current
var counter by remember { mutableStateOf(0) }
DimensionLayout {
Box(
modifier = Modifier
.size(200.dp)
.background(Color.Red)
)
val dpSize = density.run { size.toDpSize() }
Text(
text = "counter: $counter", modifier = Modifier
.size(dpSize)
.border(3.dp, Color.Green)
)
}
Button(onClick = { counter++ }) {
Text("Counter")
}
}
We are not able to get correct size because we needed to invoke dimensionScope.content() before measuring but in some cases you might be able to get Constraints, size or parameters from parent or your calculation. When that's the case you can pass you Size. I made an image that passes drawing area based on ContentScale as you can see here using scope.
Selectively measuring to match one Sibling to Another
Not being able to pass using Layout doesn't mean we can't set other sibling to same size and use its dimensions if needed.
For demonstration we will change dimensions of second Composable to firs one's
#Composable
private fun MatchDimensionsLayout(
modifier: Modifier = Modifier,
content: #Composable () -> Unit
) {
Layout(
modifier = modifier,
content = content
) { measurables: List<Measurable>, constraints: Constraints ->
// For demonstration we will change dimensions of second Composable to firs ones
require(measurables.size == 2)
val firstMeasurable = measurables.first()
val secondMeasurable = measurables.last()
val firsPlaceable = firstMeasurable.measure(constraints)
// Measure with first one's width and height
val secondPlaceable =
secondMeasurable.measure(Constraints.fixed(firsPlaceable.width, firsPlaceable.height))
// Set width and height of this Composable width of first one, height total of first
// and second
val containerWidth = firsPlaceable.width
val containerHeight = firsPlaceable.height + secondPlaceable.height
layout(containerWidth, containerHeight) {
firsPlaceable.placeRelative(0,0)
val y = firsPlaceable.height
secondPlaceable.placeRelative(0,y)
}
}
}
Demonstration
MatchDimensionsLayout {
BoxWithConstraints {
Text(
modifier = Modifier
.size(200.dp)
.border(2.dp, Color.Red),
text = "Constraints: ${constraints.minWidth}\n" +
"max: ${constraints.maxWidth}"
)
}
BoxWithConstraints {
Text(
modifier = Modifier
.size(400.dp)
.border(2.dp, Color.Red),
text = "Constraints: ${constraints.minWidth}\n" +
"max: ${constraints.maxWidth}"
)
}
}
Since we matched size of second one to first one using Constraints.fixed for measuring BoxWithConstraints now returns dimensions of first or main Composable even if we are not able to pass dimensions from Layout as parameters.
You can also use Modifier.layoutId() instead of first or second to select Composable that you need to use as reference for measuring others
Related
I know this seems trivial but I've looked up every doc, maybe I'm missing something.
I am trying to get this
across different device screen size (focusing on compact types). I successfully got screen height using LocalConfiguration.current.screenHeightDp
Now I'm implementing edge-to-edge display.
I followed this doc, but i get this
looking at this there is a space between them (blue), because of the feature to draw behind systembars. then i tried to get the systemBars height(top & bottom) to add to the boxes resp to compensate for the extra space.
I used inset.getInsets(WindowInsetsCompat.Type.systemBars()) which returns the heights of the systembars but after that the boxes overlap(yellow)
I logged the values I'm getting from the insets, it was quite more than the values of LocalConfiguration.current.screenHeightDP
How do i compensate for the space or get the correct height for the systemBars?
Here is my code :
#Composable
fun MyBox() {
val view = LocalView.current
val inset = ViewCompat.getRootWindowInsets(view)!!
val insetSystemBar = inset.getInsets(WindowInsetsCompat.Type.systemBars()) //Insets{left=0, top=136, right=0, bottom=132}
val insetStatusBar = insetSystemBar.top // 136
val insetNavBar = insetSystemBar.bottom // 132
val configuration = LocalConfiguration.current
val screenHeight = configuration.screenHeightDp //This returns 753
val testHeightRatio = 0.5
val TopBoxHeight = (((screenHeight * testHeightRatio).roundToInt())).dp // 377
val BottomBoxHeight = (((screenHeight * testHeightRatio).roundToInt())).dp // 377
val TopBoxAndStatusBarHeight = TopBoxHeight + insetStatusBar.dp // 513
val BottomBoxAndNavBar = BottomBoxHeight + insetNavBar.dp // 509
MyBoxTheme {
Surface(Modifier.fillMaxSize()) {
Box(
Modifier
.fillMaxSize()
.background(Color(0xFF00B8D4))
) {
Box(
modifier = Modifier
.height(BottomBoxAndNavBar)
.fillMaxWidth()
.align(Alignment.BottomCenter)
.background(Color(0x86FF0303)),
)
Box(
modifier = Modifier
.height(TopBoxAndStatusBarHeight)
.fillMaxWidth()
.background(Color(0x8FFFDD03))
.align(Alignment.TopCenter)
)
}
}
}
}
So I want to get the compose height/width before it's drawn into UI screen, does anyone know if it's possible to do that? And if yes, how?
I tried Modifier.onPlaced { } but I think it's already drawn into the UI before I do any modification with that information
If you provide code i might be able to provide more specific sample of SubcomposeLayout but i use it to measure dimensions of a Composable without triggering recomposition and when BoxWithConstraints is not enough. As in link in comment under some conditions maxWidth or height does not match composable dimensions.
I use it subcompose some section of Composables to pass required dimensions or like building a Slider with dynamic thumb size, chat rows, and so on. I posted several answer about SubcomposeLayout. You can check out this link to see some samples
The recent one i use is a sample one with single main and dependent Composables as in this link
#Composable
internal fun DimensionSubcomposeLayout(
modifier: Modifier = Modifier,
mainContent: #Composable () -> Unit,
dependentContent: #Composable (DpSize) -> Unit
) {
val density = LocalDensity.current
SubcomposeLayout(
modifier = modifier
) { constraints: Constraints ->
// Subcompose(compose only a section) main content and get Placeable
val mainPlaceable: Placeable = subcompose(SlotsEnum.Main, mainContent)
.map {
it.measure(constraints.copy(minWidth = 0, minHeight = 0))
}.first()
val dependentPlaceable: Placeable =
subcompose(SlotsEnum.Dependent) {
dependentContent(
DpSize(
density.run { mainPlaceable.width.toDp() },
density.run { mainPlaceable.height.toDp() }
)
)
}
.map { measurable: Measurable ->
measurable.measure(constraints)
}.first()
layout(mainPlaceable.width, mainPlaceable.height) {
dependentPlaceable.placeRelative(0, 0)
}
}
}
/**
* Enum class for SubcomposeLayouts with main and dependent Composables
*/
enum class SlotsEnum { Main, Dependent }
Passing a Composable as mainContent param will return its size in Dp without recomposition for dependentContent.
In jetpack Compose when you use Layout or SubcomposeLayout you measure your Composable with the Constraints limits.
If there is a fixed size Modifier, Modifier.size(500.dp), let's say 1000x1000 in Int, if any child Composable needs to have 1100x1000px size, layout function should be as
layout(1100, 1100){
}
since Constraints has maxWidth=1000, and maxHeight=1000 as max limits when Modifier.size() or Modifier.sizeIn(maxWidth, maxHeight) used Composable breaks and jumps as difference between size from Modifier and the one you assign to layout function above.
Because of this i need to increase size of Modifier before i set it to
SubcomposeLayout(
modifier = modifier
) { constraints ->
layout(width, height) {
}
}
Is this possible with any Modifier such as Modifier.increseSize(15.dp)? I know there is no such Modifier but what i need is increasing size of a Modifier before setting it to a Layout
What i do is a Scalable SubcomposeLayout. First gets size of its main content(Image) then adds handle size to content size and sets layout(width,height) function total size as sum of handle and content which is bigger than content size or Modifier.size(), this works when Constraints don't return a bounded upper width or height to limit measurement.
Total layout size must be bigger than content size because touchable region should cover handles' every quarter for to be clickable as you can see in the image even outside the content area handles receive gestures.
#Composable
internal fun TransformSubcomposeLayout(
modifier: Modifier = Modifier,
handleRadius: Dp = 15.dp,
mainContent: #Composable () -> Unit,
dependentContent: #Composable (IntSize) -> Unit
) {
val handleRadiusInPx = with(LocalDensity.current) {
handleRadius.roundToPx()
}
SubcomposeLayout(
modifier = modifier
) { constraints ->
// Subcompose(compose only a section) main content and get Placeable
val mainPlaceables: List<Placeable> = subcompose(SlotsEnum.Main, mainContent)
.map {
it.measure(constraints)
}
// Get max width and height of main component
val maxSize =
mainPlaceables.fold(IntSize.Zero) { currentMax: IntSize, placeable: Placeable ->
IntSize(
width = maxOf(currentMax.width, placeable.width),
height = maxOf(currentMax.height, placeable.height)
)
}
// Set sum of content and handle size as total size of this Composable
val width = maxSize.width + 2 * handleRadiusInPx
val height = maxSize.height + 2 * handleRadiusInPx
val dependentPlaceables = subcompose(SlotsEnum.Dependent) {
dependentContent(maxSize)
}.map {
it.measure(constraints)
}
layout(width, height) {
dependentPlaceables.forEach { placeable: Placeable ->
// place Placeables inside content area by offsetting by handle radius
placeable.placeRelative(
(width - placeable.width) / 2,
(height - placeable.height) / 2
)
}
}
}
}
enum class SlotsEnum { Main, Dependent }
And i pass the content size so i can size the contents
#Composable
fun TransformLayout(
modifier: Modifier = Modifier,
enabled: Boolean = true,
handleRadius: Dp = 15.dp,
handlePlacement: HandlePlacement = HandlePlacement.Corner,
onDown: (Transform) -> Unit = {},
onMove: (Transform) -> Unit = {},
onUp: (Transform) -> Unit = {},
content: #Composable () -> Unit
) {
TransformSubcomposeLayout(
// 🔥 !!! I'm not able to pass modifier from function because if it contains
// a Modifier.size() it breaks layout() function
// and because of that i can't set border, zIndex or any other Modifier from
// user. ZIndex must be set here to work, since it works against siblings
modifier = Modifier.border(3.dp, Color.Green),
handleRadius = handleRadius.coerceAtLeast(12.dp),
mainContent = {
// 🔥 Modifier from user is only used for measuring size
Box(
modifier = modifier,
contentAlignment = Alignment.Center
) {
content()
}
},
dependentContent = { intSize: IntSize ->
val dpSize = with(LocalDensity.current) {
val rawWidth = intSize.width.toDp()
val rawHeight = intSize.height.toDp()
DpSize(rawWidth, rawHeight)
}
TransformLayoutImpl(
enabled = enabled,
handleRadius = handleRadius,
dpSize = dpSize,
handlePlacement = handlePlacement,
onDown = onDown,
onMove = onMove,
onUp = onUp,
content = content
)
}
)
}
I want to use this layout as
val density = LocalDensity.current
val size = (500 / density.density).dp
TransformLayout(
modifier = Modifier.size(size).border(5.dp, Color.Red).zIndex(100f),
enabled = enabled,
handlePlacement = HandlePlacement.Side
) {
Image(
painter = painterResource(id = R.drawable.landscape1),
contentScale = ContentScale.FillBounds,
contentDescription = "",
)
}
If i set layout(500+, 500+) when this modifier size is 500x500 and set it as SubcomposeLayout Modifier Composable is not placed in correct position.
Changing Modifier size maybe possible with creating a Modifier such as SizeModifier. If anyone posts accurate answer that accomplishes task that way would be more than welcome.
I solved this by first setting a requiredSize to have minimum dimensions for this Composable.
#Composable
fun TransformLayout(
modifier: Modifier = Modifier,
enabled: Boolean = true,
handleRadius: Dp = 15.dp,
handlePlacement: HandlePlacement = HandlePlacement.Corner,
onDown: (Transform) -> Unit = {},
onMove: (Transform) -> Unit = {},
onUp: (Transform) -> Unit = {},
content: #Composable () -> Unit
) {
MorphSubcomposeLayout(
modifier = modifier
.requiredSizeIn(
minWidth = handleRadius * 2,
minHeight = handleRadius * 2
)
}
In SubcomposeLayout instead of increasing size of Composable by handles which doesn't work with Modifier.size
I constrained maximum dimensions to maxWidth and maxHeight of constraints with
// Get max width and height of main component
var maxWidth = 0
var maxHeight = 0
mainPlaceables.forEach { placeable: Placeable ->
maxWidth += placeable.width
maxHeight = placeable.height
}
val handleSize = handleRadiusInPx * 2
maxWidth = maxWidth.coerceAtMost(constraints.maxWidth - handleSize)
maxHeight = maxHeight.coerceAtMost(constraints.maxHeight - handleSize)
val maxSize = IntSize(maxWidth, maxHeight)
And passed with subcompose() function to dependent Composable as dimensions of content
val dependentPlaceables = subcompose(SlotsEnum.Dependent) {
dependentContent(maxSize)
}.map {
it.measure(constraints)
}
When i set dimensions of parent Composable with layout(width, height)
added the handle size or area i subtract initially
width = maxSize.width + 2 * handleRadiusInPx
height = maxSize.height + 2 * handleRadiusInPx
layout(width, height) {
dependentPlaceables.forEach { placeable: Placeable ->
placeable.placeRelative(0, 0)
}
}
This way i'm able to shrink content area by handle size when there is a fixed size modifier, if there is no fixed size modifier to limit measurement with upper bound as big as Composable it works as in question.
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.
How does subcompose function work?
What's the point of 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?
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.
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.
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
I want to get height of my button(or other element) in Jetpack Compose. Do you know how to get?
If you want to get the height of your button after composition, then you could use: onGloballyPositionedModifier.
It returns a LayoutCoordinates object, which contains the size of your button.
Example of using onGloballyPositioned Modifier:
#Composable
fun OnGloballyPositionedExample() {
// Get local density from composable
val localDensity = LocalDensity.current
// Create element height in pixel state
var columnHeightPx by remember {
mutableStateOf(0f)
}
// Create element height in dp state
var columnHeightDp by remember {
mutableStateOf(0.dp)
}
Column(
modifier = Modifier
.onGloballyPositioned { coordinates ->
// Set column height using the LayoutCoordinates
columnHeightPx = coordinates.size.height.toFloat()
columnHeightDp = with(localDensity) { coordinates.size.height.toDp() }
}
) {
Text(text = "Column height in pixel: $columnHeightPx")
Text(text = "Column height in dp: $columnHeightDp")
}
}
A complete solution would be as follows:
#Composable
fun GetHeightCompose() {
// get local density from composable
val localDensity = LocalDensity.current
var heightIs by remember {
mutableStateOf(0.dp)
}
Box(modifier = Modifier.fillMaxSize()) {
// Important column should not be inside a Surface in order to be measured correctly
Column(
Modifier
.onGloballyPositioned { coordinates ->
heightIs = with(localDensity) { coordinates.size.height.toDp() }
}) {
Text(text = "If you want to know the height of this column with text and button in Dp it is: $heightIs")
Button(onClick = { /*TODO*/ }) {
Text(text = "Random Button")
}
}
}
}
Using Modifier.onSizeChanged{} or Modifier.globallyPositioned{} might cause infinite recompositions if you are not careful as in OPs question when size of one Composable effects another.
https://developer.android.com/reference/kotlin/androidx/compose/ui/layout/package-summary#(androidx.compose.ui.Modifier).onSizeChanged(kotlin.Function1)
Using the onSizeChanged size value in a MutableState to update layout
causes the new size value to be read and the layout to be recomposed
in the succeeding frame, resulting in a one frame lag.
You can use onSizeChanged to affect drawing operations. Use Layout or
SubcomposeLayout to enable the size of one component to affect the
size of another.
Even though it's ok to draw if the change in frames is noticeable by user it won't look good
For instance
Column {
var sizeInDp by remember { mutableStateOf(DpSize.Zero) }
val density = LocalDensity.current
Box(modifier = Modifier
.onSizeChanged {
sizeInDp = density.run {
DpSize(
it.width.toDp(),
it.height.toDp()
)
}
}
.size(200.dp)
.background(Color.Red))
Text(
"Hello World",
modifier = Modifier
.background(Color.White)
.size(sizeInDp)
)
}
Background of Text moves from initial background that cover its bounds to 200.dp size on next recomposition. If you are doing something that changes any UI drawing from one dimension to another it might look as a flash or glitch.
First alternative for getting height of an element without recomposition is using BoxWithConstraints
BoxWithConstraints' BoxScope contains maxHeight in dp and constraints.maxHeight in Int.
However BoxWithConstraints returns constraints not exact size under some conditions like using Modifier.fillMaxHeight, not having any size modifier or parent having vertical scroll returns incorrect values
You can check this answer out about dimensions returned from BoxWithConstraints, Constraints section shows what you will get using BoxWithConstraints.
verticalScroll returns Constraints.Infinity for height.
Reliable way for getting exact size is using a SubcomposeLayout
How to get exact size without recomposition?
**
* SubcomposeLayout that [SubcomposeMeasureScope.subcompose]s [mainContent]
* and gets total size of [mainContent] and passes this size to [dependentContent].
* This layout passes exact size of content unlike
* BoxWithConstraints which returns [Constraints] that doesn't match Composable dimensions under
* some circumstances
*
* #param placeMainContent when set to true places main content. Set this flag to false
* when dimensions of content is required for inside [mainContent]. Just measure it then pass
* its dimensions to any child composable
*
* #param mainContent Composable is used for calculating size and pass it
* to Composables that depend on it
*
* #param dependentContent Composable requires dimensions of [mainContent] to set its size.
* One example for this is overlay over Composable that should match [mainContent] size.
*
*/
#Composable
fun DimensionSubcomposeLayout(
modifier: Modifier = Modifier,
placeMainContent: Boolean = true,
mainContent: #Composable () -> Unit,
dependentContent: #Composable (Size) -> Unit
) {
SubcomposeLayout(
modifier = modifier
) { constraints: Constraints ->
// Subcompose(compose only a section) main content and get Placeable
val mainPlaceables: List<Placeable> = subcompose(SlotsEnum.Main, mainContent)
.map {
it.measure(constraints.copy(minWidth = 0, minHeight = 0))
}
// Get max width and height of main component
var maxWidth = 0
var maxHeight = 0
mainPlaceables.forEach { placeable: Placeable ->
maxWidth += placeable.width
maxHeight = placeable.height
}
val dependentPlaceables: List<Placeable> = subcompose(SlotsEnum.Dependent) {
dependentContent(Size(maxWidth.toFloat(), maxHeight.toFloat()))
}
.map { measurable: Measurable ->
measurable.measure(constraints)
}
layout(maxWidth, maxHeight) {
if (placeMainContent) {
mainPlaceables.forEach { placeable: Placeable ->
placeable.placeRelative(0, 0)
}
}
dependentPlaceables.forEach { placeable: Placeable ->
placeable.placeRelative(0, 0)
}
}
}
}
enum class SlotsEnum { Main, Dependent }
Usage
val content = #Composable {
Box(
modifier = Modifier
.size(200.dp)
.background(Color.Red)
)
}
val density = LocalDensity.current
DimensionSubcomposeLayout(
mainContent = { content() },
dependentContent = { size: Size ->
content()
val dpSize = density.run {size.toDpSize() }
Box(Modifier.size(dpSize).border(3.dp, Color.Green))
},
placeMainContent = false
)
or
DimensionSubcomposeLayout(
mainContent = { content() },
dependentContent = { size: Size ->
val dpSize = density.run {size.toDpSize() }
Box(Modifier.size(dpSize).border(3.dp, Color.Green))
}
)
You could also use BoxWithConstraints as follows:
Button(onClick = {}){
BoxWithConstraints{
val height = maxHeight
}
}
But I'm not sure it fits your specific usecase.
I managed to implement a workaround to reduce (not completely removes) the number of recomposition when using Modifier.onSizeChanged by making the composable that reads from it now stateless. Not the best approach available but it gets the job done.
#Composable
fun TextToMeasure(
currentHeight: Dp,
onHeightChanged: (Dp) -> Unit,
modifier: Modifier = Modifier,
) {
val density = LocalDensity.current
Text(
text = "Hello Android",
modifier = modifier
.onSizeChanged { size ->
val newHeight = with(density) { size.height.toDp }
if (newHeight != currentHeight) {
onHeightChanged(newHeight)
}
},
)
}
#Composable
fun MainScreen() {
val height = remember { mutableStateOf(0.dp) }
TextToMeasure(
currentHeight = height.value,
onHeightChanged = { height.value = it },
)
}