40

I want to get height of my button(or other element) in Jetpack Compose. Do you know how to get?

Renattele Renattele
  • 1,626
  • 2
  • 15
  • 32

5 Answers5

51

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")
    }
}
Mohamed Rejeb
  • 2,281
  • 1
  • 7
  • 16
user3872620
  • 1,036
  • 10
  • 12
  • 10
    Note: onGloballyPositioned returns size in px so I converted it to dp before using(like setting paddings, heights, etc). – Renattele Renattele Mar 23 '21 at 14:05
  • How to convert to dp? – Hussien Fahmy Apr 29 '22 at 16:05
  • 6
    val heightInDp = with(LocalDensity.current) { heightInPx.toDp() } – user3872620 Apr 30 '22 at 19:37
  • 1
    It's not fine with padding and preview. – Psijic Sep 06 '22 at 11:58
  • If you are doing some UI operation like resizing or drawing you will experience a jump or flash on screen. Because using `Modifier.onSizeChanged` or `Modifier.onGloballyPositioned` requires you to have `MutableState` which triggers a recomposition when you set inside these function that can be noticeable in some cases. If that's the case either use `BoxWithConstraints`, and in some conditions max height constraint is not equal to actual height then you need to use a SubcomposeLayout – Thracian Sep 06 '22 at 13:27
11

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")
            }
        }
    }
}
F.Mysir
  • 2,838
  • 28
  • 39
5

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))
    }
)
Thracian
  • 43,021
  • 16
  • 133
  • 222
1

You could also use BoxWithConstraints as follows:

Button(onClick = {}){

   BoxWithConstraints{
     val height = maxHeight

   }
}

But I'm not sure it fits your specific usecase.

Mauro Banze
  • 1,898
  • 15
  • 11
0

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 },
    )
}
uragiristereo
  • 546
  • 1
  • 6
  • 9