I’ve come across another interesting question in StackOverflow, the question asks how could we create a Grid composable that lets its children set individual column and row spans so that the composable fills the whole space allocated to it, and places its children given their column/row spans. An image of what we want to achieve may better explain what we are after:
So given a certain area, we place the children given their individual spans as best we can, leaving a gap where necessary (the black sections you see above), while using all the available space from the parent. Let’s see how we can accomplish this.
The first thing we need to do is determine how we are going to provide the column and row span to the main composable. To do this we can replicate what most composables from the Jetpack Compose libraries do, they offer an extension on
Modifier and a custom scope within the composable to use this extension. I have described this approach here so I won’t go over the specifics in this post, I’ll just show the code and briefly go over it:
We create a
GridScope interface that defines an extension on
Modifier. This extension allows us to specify the number of columns and rows that a child wants to occupy.
We then create a
GridData object that extends
ParentDataModifier — this is what we need in order to read the custom attributes (column and row spans) in our Layout.
To draw the children we need the equivalent of a
ViewGroup in the view system, which in Jetpack Compose is a
Layout. This is a composable that accepts a content lambda with the children to lay out, and a
MeasurePolicy object to determine how to measure and lay the children out. As is customary, we will also accept a
Modifier that we will pass to our root composable to make the Grid composable more customizable. For our Grid, we also need to know how many columns we need to render, so we will take that as an argument as well.
With that said, let’s see how we define our Grid composable:
The key thing to notice here is that our content is scoped to the
GridScope we defined above, so when adding the children we will have access to the
Modifier extension we defined earlier. We then call
Layout, and pass as content the
content lambda we receive, scoped to the
GridScope, and the input modifier.
Computing the number of rows
Next we need to figure out how many rows we will need to render the children. The way we will do that is by walking the children and attempting to place them in a row, based on their column span. Once they no longer fit in the current row, we will move them to the next one.
Once we have filled a row we need to determine what the column span for this row is, which it is simply the maximum of all the column spans of the children in this row.
Finally, the number of rows will be the sum of all the column spans. Let’s see how we do this:
- First we define some helper functions to retrieve the
rowSpanfrom our custom Modifier.
- Next we define a standard Grid of 1x1 as a fallback in case a child didn’t provide one.
- We now walk our children and get their required
GridDataand keep this in a list we name
- We then use a helper method to calculate how many rows we need, based on the
columnswe are given as input.
- In the helper method we walk the spans.
- For each span we get the
rowSpanthat the child wants to occupy. Note that we add a safety check to constraint the column span to at most the number of columns we want to render. Alternatively we could throw an error if the span count exceeds
- Now we try to fit the current child in the current row, by checking if the number of columns we have occupied in this row, plus the number of columns the child wants to occupy fit in the total number of columns we have.
- If the child fits, we increase our column span by the column span of this child and adjust our row span based on the row spans of all the children in this row.
- If the child does not fit, we then increase the number of rows by the previous row span, and reset our current column and row spans for a new row.
- Finally, we return the number of rows we need — note that the last row is not included in the previous calculation, so we add it there.
Building the Constraints
Now that we have calculated how many rows we need it’s time to measure the children. For this, we need to use specific
Constraints for each child, based on the column and row span for the child.
First we will create a basic
Constraint object that will represent a 1x1 cell, that’ll be our baseline that we will then use or each child. If a child wants to occupy 2 columns, then the
Constraint will be the base constraint, with twice the width.
The code to do so is below:
- We build our base 1x1
Constraintsby dividing the input
Constraintsby the number of columns and rows we need.
- Next we walk all the children in order to measure them.
- For each child we get the column and row span to use.
- Next we build individual
Constraintsfor each child, using the
baseConstraintsas a starting point and multiplying the width by the
columnSpanand the height by the
- Once we have the
Constraintsfor each child, we measure them all.
Placing the children
We’re almost there, we have calculated how may rows we need and have measured all the children. It is now time to place them in the layout. This will follow a similar pattern to the row calculation, we will attempt to place each child in the current column and, if it does not fit based on its column span, we will move it to the next one and increase the vertical offset accordingly.
Let’s see how we can achieve that:
- To place the children we use the
layoutmethod, passing in the incoming
- We initialize some variables to keep track of the (x, y) coordinates where to place the current child, and the column and row spans.
- Next we walk our placeables.
- For each placeable we get the required column and row span.
- Here we check if the child fits in the current row, by checking if the total column span fits in the number of columns.
- If the child does not fit we reset the column span to this child’s column span, we reset the x offset to 0 and we increase the y offset by the accumulated row span, times the height of an individual cell.
- If the child fits, then we increase the current column span by this child’s column span and keep track of the number of rows we will use for this column.
- Next we place the child at the calculated (x, y) coordinates.
- And finally we increase our x offset based on how many columns the child takes times the width of a base cell.
Optimizing the calculation
The code is now complete and we have our Grid that places the children based on their respective column and row spans. However, as you may have noticed, there is some duplication in the calculations for the number of rows and the placement logic, so we are basically doing the same calculation twice. We can optimize this by keeping track of the number of children that fit in each row so that we don’t need to recalculate this in the placement phase.
To do so, when we walk the children to calculate the rows we will keep track of how many children fit in this row and the row spans, so that we can leverage that information later.
Let’s update our row calculation function:
We define a
GridInfo class to hold the information we need. Now, when we walk the list of children, we keep track of how many children fit in this row and its spans.
To leverage this data we need to update our placement block as shown below:
In this simplified block we walk the grid info list, retrieve the child based on how many children we have processed so far and how many are in this row, and update the (x, y) coordinates based on the information for this grid.
The final code with a preview is below. I hope you found this useful, and I’ll see you on the next one. Happy coding!