Not only modifiers can be easily modified in Jetpack Compose

Piotr Prus
Google for Developers Europe
4 min readJun 14, 2023

--

Photo by Tolga Ulkan on Unsplash

I bet everyone that use Jetpack Compose for a while knows that making a custom modifier is not only possible, but also very easy and helpful, but did you know you can make your own Alignment for Row/Column? During my Jetpack Compose exploration, I found that you can customize basically everything and in this blog post, I will show you one cool example.

The challenge

I needed to present items in a row that are equally distributed from their centers, not having even spacing between them. The below graph presents the problem. The red crossbars represent equal spacing. The green crossbars have slightly different lengths. Our goal is to make green crossbars equal in length.

Arrangement.SpaceEvenly

The closest arrangement to this in question is Arrangement.SpaceEvenly. Let’s check the code for it:

@Stable
val SpaceEvenly = object : HorizontalOrVertical {
override val spacing = 0.dp

override fun Density.arrange(
totalSize: Int,
sizes: IntArray,
layoutDirection: LayoutDirection,
outPositions: IntArray
) = if (layoutDirection == LayoutDirection.Ltr) {
placeSpaceEvenly(totalSize, sizes, outPositions, reverseInput = false)
} else {
placeSpaceEvenly(totalSize, sizes, outPositions, reverseInput = true)
}

override fun Density.arrange(
totalSize: Int,
sizes: IntArray,
outPositions: IntArray
) = placeSpaceEvenly(totalSize, sizes, outPositions, reverseInput = false)

override fun toString() = "Arrangement#SpaceEvenly"
}

The main function that does the placement magic here is Density.arrange , followed by placeSpaceEvenly.

internal fun placeSpaceEvenly(
totalSize: Int,
size: IntArray,
outPosition: IntArray,
reverseInput: Boolean
) {
val consumedSize = size.fold(0) { a, b -> a + b }
val gapSize = (totalSize - consumedSize).toFloat() / (size.size + 1)
var current = gapSize
size.forEachIndexed(reverseInput) { index, it ->
outPosition[index] = current.roundToInt()
current += it.toFloat() + gapSize
}
}

Breaking this 👆code line by line:

  • consumedSize sums all the sizes of child items
  • gapSize takes the totalSize of the parent, subtracts the size of the children, and divides the result by the number of elements plus 1. This equation gives us an equal gap to apply to the layout
  • Lastly, for each element, we specify the new position by adding a gap to our position of the element

It does not look complicated, right? Let's make our custom Arrangement object.

Spread over center

I will call the custom arrangement: Arrangement.SpreadOverCenter and extend it with Arrangement.Horizontal. As in the above example, I will need to override the function arrange().

fun Arrangement.SpreadOverCenter(minSpace: Dp = 0.dp): Arrangement.Horizontal =
object : Arrangement.Horizontal {
override val spacing: Dp
get() = minSpace

override fun Density.arrange(
totalSize: Int,
sizes: IntArray,
layoutDirection: LayoutDirection,
outPositions: IntArray
) {
// Our code goes here
}

override fun toString() = "Arrangement#spreadOverCenter($minSpace)"
}
override fun Density.arrange(
totalSize: Int,
sizes: IntArray,
layoutDirection: LayoutDirection,
outPositions: IntArray
) {
if (sizes.isEmpty()) return
val firstWidth = sizes.first()
val lastWidth = sizes.last()
val equalSpace = (totalSize - firstWidth.div(2) - lastWidth.div(2)).div(
(sizes.size.minus(1).coerceAtLeast(1))
)
sizes.forEachIndexed { index, size ->
outPositions[index] = firstWidth.div(2) + equalSpace * index - size.div(2)
}
}

Arranging the positions step by step:

  1. I am checking if sizes are not empty and returning the function otherwise.
  2. I assign two values for the first and the last element width.
  3. To calculate the equalSpace, I am taking the totalSize of the parent row, subtracting half of the first and half of the last width, and dividing the result by the number of elements minus 1.
  4. Having calculated the space(gap) I can now apply the new position to each element by multiplying the gap and subtracting half of the element size. Voila, we arrange the elements of a row with a custom arrangement object.

The result

Here is the final result for boxes with different sizes. The first row presents the same layout positioned using Arrangement.SpaceEvenly. The second row is built using custom Arrangement.SpreadOverCenter

As you can see the Composable Modifier is not the only object that can be easily customized. I encourage you to experiment not only with Arrangement but with all the different objects and factors that are used to position the layout. Happy composing! 🎹 👨🏼‍💻

Thanks, Damian Petla for the review

--

--

Piotr Prus
Google for Developers Europe

Android Developer @Tilt, Enthusiast of kotlin, jetpack compose and clean architecture. Currently Composing and KMMing all the things ❤️