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 },
)
}
Related
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
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.
I have a "simple" layout in Compose, where in a Column there are two elements:
top image
a grid of 4 squares underneath, each square containing some text
I'd like the layout to have the following behaviour:
set the maximum height of the image to be screenWidth
set the minimum height of the image to be 200.dp
the image should always be in a square container (cropping of the image is fine)
let the grid "grow" as much as it needs to, to wrap around the content, making the image shrink as necessary
This means that if the text in the squares is short, the image will cover a large square on the top. But if any of the square text is really, long, I want the whole grid to scale up and shrink the image. These are the desirable outcomes:
When text is short enough
When a piece of text is really long
I have tried this with ConstraintLayout in Compose, but I can't get the squares to scale properly.
With a Column, I can't get the options to grow with large content - the text just gets truncated and the image remains a massive square.
These are the components I'd built:
// the screen
Column {
Box(modifier = Modifier
.heightIn(min = 200.dp, max = screenWidth)
.aspectRatio(1f)
.border(BorderStroke(1.dp, Color.Green))
.align(Alignment.CenterHorizontally),
) {
Image(
painter = painterResource(id = R.drawable.puppy),
contentDescription = null,
contentScale = ContentScale.Crop
)
}
OptionsGrid(choicesList, modifier = Modifier.heightIn(max = screenHeight - 200.dp))
}
#Composable
fun OptionsGrid(choicesList: List<List<String>>, modifier: Modifier = Modifier) {
Column(
modifier = modifier
.border(1.dp, Color.Blue)
.padding(top = 4.dp, bottom = 4.dp)
.fillMaxHeight(),
verticalArrangement = Arrangement.Center
) {
choicesList.forEach { choicesPair ->
Row(modifier = Modifier.weight(0.5f)) {
choicesPair.forEach { choice ->
Box(
modifier = Modifier
.padding(4.dp)
.background(Color.White)
.weight(0.5f)
) {
Option(choice = choice)
}
}
}
}
}
}
#Composable
fun Option(choice: String) {
Box(
modifier = Modifier
.fillMaxSize()
.background(Color.Yellow)
.border(BorderStroke(1.dp, Color.Red)),
contentAlignment = Alignment.Center
) {
Text(
text = choice,
modifier = Modifier.padding(8.dp),
textAlign = TextAlign.Center,
)
}
}
Do I need a custom layout for this? I suppose what's happening here is that the Column is measuring the image first, letting it be its maximum height, because there is space for that on the screen, and then when measuring the grid, it gives it the remaining space and nothing more.
So I'd need a layout which starts measuring from the bottom?
Here's how you can do it without custom layout.
You need your image size to be calculated after OptionsGrid. In this case you can use Modifier.weight(1f, fill = false): it forces all the views without Modifier.weight to be layout before any weighted elements.
Modifier.weight will override your Modifier.heightIn, but we can restrict it size from the other side: using Modifier.layout on OptionsGrid. Using this modifier we can override constraints applied to the view.
p.s. Modifier.heightIn(max = screenWidth) is redundant, as views are not gonna grow more than screen size anyway, unless the width constraint is overridden, for example, with a scroll view.
.height(IntrinsicSize.Min) will stop OptionsGrid from growing more than needed. Note that is should be placed after Modifier.layout, as it sets height constraint to infinity. See why modifiers order matters.
val choicesList = listOf(
listOf(
LoremIpsum(if (flag) 100 else 1).values.first(),
"Short stuff",
),
listOf(
"Medium length text",
"Hi",
),
)
Column {
Box(
modifier = Modifier
.weight(1f, fill = false)
.aspectRatio(1f)
.border(BorderStroke(1.dp, Color.Green))
.align(Alignment.CenterHorizontally)
) {
Image(
painter = painterResource(id = R.drawable.profile),
contentDescription = null,
contentScale = ContentScale.Crop
)
}
OptionsGrid(
choicesList,
modifier = Modifier
.layout { measurable, constraints ->
val placeable = measurable.measure(constraints.copy(
// left 200.dp for min image height
maxHeight = constraints.maxHeight - 200.dp.roundToPx(),
// occupy all height except full image square in case of smaller text
minHeight = constraints.maxHeight - constraints.maxWidth,
))
layout(placeable.width, placeable.height) {
placeable.place(0, 0)
}
}
.height(IntrinsicSize.Min)
)
}
Result:
I suppose what's happening here is that the Column is measuring the
image first, letting it be its maximum height, because there is space
for that on the screen, and then when measuring the grid, it gives it
the remaining space and nothing more.
That is correct, it goes down the UI tree, measures the first child of the column(the box with the image) and since the image doesn't have any children, it returns it's size to the parent Column.
(see documentation)
I'm pretty sure this requieres a custom layout, so this is what I came up with:
First, modified your composables a bit for testing purposes (tweaked some modifiers and replaced the Texts with TextFields to be able to see how the UI reacts)
#ExperimentalComposeUiApi
#Composable
fun theImage() {
Box(
contentAlignment = Alignment.Center,
modifier = Modifier
.aspectRatio(1f)
.border(BorderStroke(1.dp, Color.Green))
.background(Color.Blue)
) {
Image(
painter = painterResource(id = R.drawable.ic_launcher_foreground),
contentDescription = null,
contentScale = ContentScale.Crop,
modifier = Modifier
.border(BorderStroke(2.dp, Color.Cyan))
)
}
}
#Composable
fun OptionsGrid(modifier: Modifier = Modifier) {
Column(
modifier = modifier
.border(1.dp, Color.Blue)
.padding(top = 4.dp, bottom = 4.dp)
.height(IntrinsicSize.Min),
verticalArrangement = Arrangement.Center
) {
repeat(2){
Row(modifier = Modifier.weight(0.5f)) {
repeat(2){
Box(
modifier = Modifier
.padding(4.dp)
.background(Color.White)
.weight(0.5f)
.wrapContentHeight()
) {
Option()
}
}
}
}
}
}
#Composable
fun Option() {
var theText by rememberSaveable { mutableStateOf("a")}
Box(
modifier = Modifier
.fillMaxSize()
.background(Color.Yellow)
.border(BorderStroke(1.dp, Color.Red)),
contentAlignment = Alignment.Center
) {
OutlinedTextField(value = theText, onValueChange = {theText = it})
}
}
And now, the custom layout
Since subcompose needs a slotId, and you only need IDs for the image and grid, you can create an Enum class with two ids.
enum class SlotsEnum {Main, Dependent}
slotID: A 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.
Then, with this composable, which receives a screen width, height, an optional modifier and the image, as well as the grid
#Composable
fun DynamicColumn(
screenWidth: Int,
screenHeight: Int,
modifier: Modifier = Modifier,
img: #Composable () -> Unit,
squares: #Composable () -> Unit
)
You can measure the total height of the grid and use that to calculate the height of the image (still haven't managed to a proper UI when scaled under 200dp, but it shouldn't be diffcult).
SubcomposeLayout { constraints ->
val placeableSquares = subcompose(SlotsEnum.Main, squares).map {
it.measure(constraints)
}
val squaresHeight = placeableSquares.sumOf { it.height }
val remainingHeight = screenHeight - squaresHeight
val imgMaxHeight = if (remainingHeight > screenWidth) screenWidth else remainingHeight
val placeableImage = subcompose(SlotsEnum.Dependent, img).map{
it.measure(Constraints(200, screenWidth, imgMaxHeight, imgMaxHeight))
}
Then, apply the constraints to the image and finally place the items.
layout(constraints.maxWidth, constraints.maxHeight) {
var yPos = 0
placeableImage.forEach{
it.place(x= screenWidth / 2 - it.width / 2, y= yPos)
yPos += it.height
}
placeableSquares.forEach{
it.place(x=0, y=yPos)
}
}
and finally, just call the previous composable, DynamicColumn:
#ExperimentalComposeUiApi
#Composable
fun ImageAndSquaresLayout(screenWidth: Int, screenHeight: Int) {
DynamicColumn(screenWidth = screenWidth, screenHeight = screenHeight,
img = { theImage() },
squares = { OptionsGrid() })
}
PS: possibly will update this tomorrow if I can fix the minimum width issue
How can i set width of a group of composables, siblings layout from top to bottom, to width of longest one?
What i try to build is exactly same thing as in the images above. For simplicity let's say quote the component at the top and message box which contains message and another container that stores date and message status.
The longest one of quote and message box must be set as parent width and other one must be set to same width as longest one which requires a remeasuring for short one i assume.
Also if message box gets to resized there needs to be an internal parameter that passes this width to set position of container that stores date and status. As can be seen clearly with bounds message text is moved to start while status to end when quote is longer than message box. When message has more than one line message box width and height are set with a calculation as telegram or whatsapp does.
Built this initially with Layout as
#Composable
private fun DynamicLayout(
modifier: Modifier = Modifier,
quote: #Composable () -> Unit,
message: #Composable () -> Unit
) {
val content = #Composable {
quote()
message()
}
Layout(content = content, modifier = modifier) { measurables, constraints ->
val placeableQuote = measurables.first().measure(constraints)
val quoteWidth = placeableQuote.width
val placeableMessage =
measurables.last()
.measure(Constraints(minWidth = quoteWidth, maxWidth = constraints.maxWidth))
val messageWidth = placeableMessage.width
val maxWidth = quoteWidth.coerceAtLeast(messageWidth)
val totalHeight = placeableQuote.height + placeableMessage.height
layout(maxWidth, totalHeight) {
placeableQuote.placeRelative(x = 0, y = 0)
placeableMessage.placeRelative(x = 0, y = placeableQuote.height)
}
}
}
Where i measure message box using width of quote constraint it works but only when quote is longer.
DynamicLayout(
quote = {
Text(
"QUOTE with a very long text",
modifier = Modifier
.background(Color(0xffF44336))
.height(60.dp),
color = Color.White
)
},
message = {
Text(
"MESSAGE Content",
modifier = Modifier
.background(Color(0xff9C27B0)),
color = Color.White
)
}
)
DynamicLayout(
quote = {
Text(
"QUOTE",
modifier = Modifier
.background(Color(0xffF44336))
.height(60.dp),
color = Color.White
)
},
message = {
Text(
"MESSAGE with very long Content",
modifier = Modifier
.background(Color(0xff9C27B0)),
color = Color.White
)
}
)
As it's must be remeasured i think solution for this question should be done with SubComposeLayout but couldn't figure out how to use it for this setup?
#Composable
private fun SubComponentLayout(
modifier: Modifier = Modifier,
mainContent: #Composable () -> Unit,
dependentContent: #Composable (Int) -> Unit
) {
SubcomposeLayout(modifier = modifier) { constraints ->
val mainMeasurables: List<Measurable> = subcompose(SlotsEnum.Main, mainContent)
val mainPlaceables: List<Placeable> = mainMeasurables.map {
it.measure(constraints)
}
val maxSize =
mainPlaceables.fold(IntSize.Zero) { currentMax: IntSize, placeable: Placeable ->
IntSize(
width = maxOf(currentMax.width, placeable.width),
height = maxOf(currentMax.height, placeable.height)
)
}
var maxWidth =
mainPlaceables.maxOf { it.width }
layout(maxSize.width, maxSize.height) {
println("🔥 SubcomposeLayout-> layout() maxSize width: ${maxSize.width}, height: ${maxSize.height}")
val dependentMeasurables: List<Measurable> = subcompose(
slotId = SlotsEnum.Dependent,
content = {
println("🍏 SubcomposeLayout-> layout()->subcompose() mainWidth ZERO")
dependentContent(0)
}
)
val dependentPlaceables: List<Placeable> = dependentMeasurables.map {
it.measure(constraints)
}
maxWidth = maxWidth.coerceAtLeast(
dependentPlaceables.maxOf { it.width }
)
subcompose(SlotsEnum.NEW) {
println("🍒 SubcomposeLayout-> layout()->subcompose() maxWidth: $maxWidth")
dependentContent(maxWidth)
}
mainPlaceables.forEach { it.placeRelative(0, 0) }
dependentPlaceables.forEach { it.placeRelative(0, 150) }
}
}
}
Why cannot remeasure same component second time with same id? When i try to call subCompose with SlotsEnum.Dependent it throws an exception
subcompose(SlotsEnum.NEW) {
println("🍒 SubcomposeLayout-> layout()->subcompose() maxWidth: $maxWidth")
dependentContent(maxWidth)
}
Still not remeasuring correctly after calling it? How can setting sibling can be solved with SubcomposeLayout?
I made a sample based on the sample provided by official documents and #chuckj's answer here.
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.
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