Stackademic

Stackademic is a learning hub for programmers, devs, coders, and engineers. Our goal is to democratize free coding education for the world.

Follow publication

Android/JetpackCompose: Component Sizing, Layout and Two Views Same Size 2 Ways

Itsuki
Stackademic
Published in
6 min readJun 2, 2024

In this article, we will be taking a look at how to obtain components sizing, position, and change the layout in Jetpack Compose.

Specifically, we will be using a simple example where we have two Texts, and we are trying to have them be of the same width as the one with the larger value (Basically the Jetpack compose version of what we have done in one of my previous article SwiftUI: Two Views Same Size 2 Ways).

And again, we have 2 different approaches.

And obviously, there is a 3rd way where you will just simply hard code the width and height! And obviously that is not what we are interested in so we will no go through that in this article!

Set up

As I mentioned above, we will have two Texts, long and short, placed vertically, and we will trying to give those two the same width as that of the longer one.


@Composable
fun TwoViewSameSizeDemo() {
Column(
verticalArrangement = Arrangement.spacedBy(20.dp, alignment = Alignment.CenterVertically),
horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier
.fillMaxSize()
.background(Color.LightGray)
) {
Text(
text = "short",
fontSize = 20.sp,
textAlign = TextAlign.Center,
color = Color.White,
modifier = Modifier
.background(Color.Black, RoundedCornerShape(16.dp))
.padding(16.dp)
)

Text(
text = "long long long long",
fontSize = 20.sp,
textAlign = TextAlign.Center,
color = Color.White,
modifier = Modifier
.background(Color.Black, RoundedCornerShape(16.dp))
.padding(16.dp)
)
}
}

And obviously, each Text only taking up the space needed. And that’s why we are here!

onGloballyPositioned

This is the modifier is called with the final LayoutCoordinates of the Layout when the global position of the content may have changed. That is the If the size or position has changed part in the graph below.

https://developer.android.com/jetpack/compose/phases

I personally see this as an analogous of the GeometryReader in SwiftUI and here is how we would use it to make two views of the same width as the larger one.

Three Steps!

  1. Keep a mutableState of width starting at 0.dp
  2. set the width to be the max of the current width and the component width onGloballyPositioned
  3. Add the .width modifier after the actual width is measured onGloballyPositioned

(This is probably the only time I have appreciated Compose more than SwiftUI. We get to attach modifier conditionally easily here whereas in SwiftUI, we will have to define an entirely new struct extending ViewModifier!)


@Composable
fun TwoViewSameSizeDemo() {
val density = LocalDensity.current

var width by remember { mutableStateOf(0.dp) }
var modifier = Modifier
.background(Color.Black, RoundedCornerShape(16.dp))
.padding(16.dp)
.onGloballyPositioned { coordinates ->
val coordinateWidthPx = coordinates.size.width
val coordinateWidthDp = with(density) {coordinateWidthPx.toDp()}
width = max(width, coordinateWidthDp)
}

if (width != 0.dp) {
modifier = modifier.width(width)
}


Column(
verticalArrangement = Arrangement.spacedBy(20.dp, alignment = Alignment.CenterVertically),
horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier
.fillMaxSize()
.background(Color.LightGray)
) {
Text(
text = "short",
fontSize = 20.sp,
textAlign = TextAlign.Center,
color = Color.White,
modifier = modifier
)

Text(
text = "long long long long",
fontSize = 20.sp,
textAlign = TextAlign.Center,
color = Color.White,
modifier = modifier
)
}
}

One thing I would like to point out here!

onGloballyPositioned returns the width in px. To convert it to dp, we will need to use the LocalDensity.current.

In addition to the size of the composable, we also have the information below included in our coordinates. Not what we are interested here though!

  • coordinates.positionInWindow: Position relative to the Window of the Application
  • coordinates.positionInRoot: Position relative to the Compose root
  • coordinates.providedAlignmentLines: alignment lines provided to the layout. Empty for Column
  • coordinates.parentLayoutCoordinates: LayoutCoordinates instance corresponding to the parent of Column

Do note that the coordinates.positionInRoot is always relative to the Coordinate (0, 0), the left upper corner of the screen, ignoring any paddings or offset applied to the root composable if you are only using Composables in the layout.

Layout

onGloballyPositioned is really to use. However, as you can see, recompositions will happen for multiple times and this can become extremely expensive when the number of elements increases!

And this is where Layout shines! All higher-level layouts like Column and Row are built with this composable and it really helps us avoiding unnecessary recompositions by using intrinsic measurements!

And obviously, we have to work harder because, now, we have to measure and lay out all children manually!

Here is the basic flow of using Layout!

  • Pass in the composables we want to lay out in content
  • Get a list of children that need to be measured in measurables and their constraints from the parent in constraints.
  • constrain child views further if needed
  • measure them with constraints. This will return a Placeable layout that has its new size. Note that a Measurable can only be measured once inside a layout pass.
  • Set the size of the parent layout
  • Place children in the parent layout

Here is how we can apply it to make two view being of the same width.


@Composable
fun TwoViewSameSizeDemo() {
val spacingDp = 20
val density = LocalDensity.current
val spacingPx = with(density) {spacingDp.dp.toPx().roundToInt()}

Layout(
modifier = Modifier
.background(Color.LightGray)
.fillMaxSize()
,
content = {
Text(
text = "shorts",
textAlign = TextAlign.Center,
fontSize = 20.sp,
color = Color.White,
maxLines = 1,
modifier = Modifier
.layoutId("short")
.background(Color.Black, RoundedCornerShape(16.dp))
.padding(16.dp)
)

Text(
text = "long long long long",
textAlign = TextAlign.Center,
fontSize = 20.sp,
color = Color.White,
maxLines = 1,
modifier = Modifier
.layoutId("long")
.background(Color.Black, RoundedCornerShape(16.dp))
.padding(16.dp)
)

},
measurePolicy = { measurables, constraints ->
val shortMeasurable = measurables.find { it.layoutId == "short" }
?: error("short text not found")
val longMeasurable = measurables.find { it.layoutId == "long" }
?: error("long text not found")

val (textWidth, textHeight) = calculateTextsSize(measurables, constraints)
val placeables = listOf(
shortMeasurable.measure(Constraints.fixed(textWidth, textHeight)),
longMeasurable.measure(Constraints.fixed(textWidth, textHeight))
)
layout(
width = textWidth ,
height = textHeight * 2 + spacingPx,
placementBlock = { placeTexts(spacingPx, placeables) }
)

}
)
}

private fun calculateTextsSize(
measurables: List<Measurable>,
constraints: Constraints,
)
: Pair<Int, Int> {
val textWidths = measurables.map { it.maxIntrinsicWidth(constraints.maxHeight) }
val maxWidth = textWidths.max()
val height = measurables.first().minIntrinsicHeight(maxWidth)
return Pair(maxWidth, height)
}


private fun Placeable.PlacementScope.placeTexts(
spacing: Int,
placeables: List<Placeable>
)
{
var yPosition = 0
placeables.forEach { placeable ->
placeable.placeRelative(x = 0, y = yPosition)
yPosition += placeable.height + spacing
}
}

We first find the maxIntrinsicWidth of each measurable by using maxIntrinsicWidth(constraints.maxHeight) , we then find the max value among all measurables and set it to be the width constraint for all of them.

We have set the maxLines to be 1 so the height will be the same for all measurables and we can simply just use the minIntrinsicHeight(maxWidth) for the first.

However, if you are expecting multi-line text, for example, and want to make all the Texts of the same height as well, all you have to do is to copy and paste the width part and change the width to height!

When we place the placeables in Placeable.PlacementScope.placeTexts, since we want them to be of a Column, we keep the x to be the same and simply increment y.

Note!

Again, everything is in px when working with Layout! Therefore, if you are like be trying to make it the same look as that when using a Column Composable and define the spacing to be in dp, make sure to convert it to px using LocalDensity.

Why would Android? Google? Jetpack Compose decide to use two totally different units, px and dp for measuring and defining sizes? My guess is that px is the left over from xml era and dp is the fancy new Compose. But that is just my guess… If you know the exact answer, please leave me a comment! I would be happy to peak into Google’s mind!

Anyway, let’s give it a run! And exactly the same as what we have above!

We have used the Layout composable here because we want to measure and layout multiple composables. However, if you are only interested in the layout of one single composable, you can also use the layout modifier.

That’s all I have for today!

Thank you for reading! And listening to my complaint about Android!

Happy lay outing!

Sign up to discover human stories that deepen your understanding of the world.

Free

Distraction-free reading. No ads.

Organize your knowledge with lists and highlights.

Tell your story. Find your audience.

Membership

Read member-only stories

Support writers you read most

Earn money for your writing

Listen to audio narrations

Read offline with the Medium app

Published in Stackademic

Stackademic is a learning hub for programmers, devs, coders, and engineers. Our goal is to democratize free coding education for the world.

Responses (1)

Write a response