JOLANDA VERHOEF: Modifiers in Compose can be chained,
and the order in which we do so matters.
But how exactly does it matter?
In this episode, we'll go beyond your intuition
and learn to reason about modifier chaining
and how it influences the sizes of our composables.
[MUSIC PLAYING]
In the previous episode, we looked at the three phases
of transforming data into UI--
composition, layout, and drawing.
We also learned how modifiers can affect different phases.
We used the size, padding, and clip modifiers
to change the size, spacing, and shape of a composable.
In this episode, I'd like to zoom in a little bit more
on the layout phase, because when
we start chaining more modifiers that influence a layout node's
measurements, and specifically when we start combining them,
it can be difficult to understand what
the results will actually be.
Let's take a look at some use cases.
Let's say we draw an image and apply this modifier chain.
What is the image going to look like?
Will it fill the whole screen, like option A at the top,
or will it limit itself to 50.dp, like option B?
Now, let's say we add a modifier, wrapContentSize,
in the middle.
What would the impact of that be?
Or what if we used this modifier chain for the image--
clipping the image, adding some padding,
and then setting its size?
Would this result in a nice round image, like option A?
Or would the image be cut off on the sides, like option B?
If you don't know the answers to these questions yet,
don't worry.
In about 10 minutes, you'll be ready to answer these
and any other layout questions people might throw at you.
How, you say?
By learning about constraints.
Remember that in the last episode,
we discussed the layout phase and how
it follows a three-step algorithm
to find each layout node's width, height,
and x-y coordinates.
Constraints help finding the right sizes for our nodes
during the first two steps of this algorithm.
Constraints are minimum and maximum bounds
for a node's width and height.
When the node decides on its size,
its measured size should fall in this given size range.
Constraints are passed down from parent to child in the UI tree
during the layout phase.
When a parent node measures its children,
it provides these constraints to each child
to let them know how big or small they're allowed to be.
Constraints can be bounded, indicating a minimum
and a maximum width and height.
In this case, the layout node should
choose a width between 100 and 300 dp
and a height between 100 and 200.
Constraints can also be unbounded, in which case,
the node is not constrained to any size.
The maximum width and height bounds
are then set to infinity.
Or constraints can be exact, asking a node
to follow an exact size requirement.
The minimum and maximum bounds are set to the same value,
in this case, 300 by 200 dp.
Of course, combinations of these are also valid,
for example, bounding the width while allowing
for an unbounded maximum height, or setting an exact width
but providing a bounded height.
As I said, constraints are passed down from parent
to child node in the UI tree when the parent is
going to measure its child.
When a node has decided its own size,
it communicates that size back up the tree.
Let's see how this works for our UI tree.
Say we're rendering this row inside a container that
is 300 by 200 dp.
The constraints passed to our row node
are bounded, and say that it can take
any width between 0 and 300 and any height between 0 and 200.
To decide the size it actually wants to occupy,
the row will measure its children.
It forwards the same constraints to its first child.
The clip modifier does not impact measurement,
so it simply forwards the constraints
to the next modifier.
The size modifier is used to specify an exact size.
It changes the constraints to be exact bounds, in this case,
bounds of 40 by 40 dp.
As the image node does not have any children,
it will decide its own size based on the constraints that
were passed in.
It will listen to the changed constraints,
and it will report a size of 40 by 40 dp.
The size modifier does not change that measured size,
and neither does the clip modifier.
Now, the row wants to place its children side by side,
so it should adapt to constraints
to make sure that its second child does not
go out of bounds.
It subtracts 40 dp from the maximum width constraint.
The padding modifier now wants to add 10 dp of space
at the start of its child, so it decreases the max width
constraint by a further 10 dp to 250.
To decide its size, the column needs to measure its children.
It first forwards the constraints to its first child.
This padding modifier will add vertical padding,
so it decreases the maximum height by 5 dp to 195.
The text decides its own size and reports a size of 140
by 15.
The padding modifier increases that size,
as the padding should be incorporated
in the text element.
It reports a size of 140 by 20.
Now, remember the column originally had
maximum bounds of 250 by 200.
Because a column places its children below each other,
the maximum height constraint passed to the second child
is decreased by the height of the first child,
in this case, 20 dp, so it becomes 180.
The second text reports a size of 100 by 15 dp,
so now the column knows enough to decide on its size.
It picks the maximum width of its children
and the sum of their heights, and it reports a size of 140
by 35.
The column's start padding adds another 10 dp
to the reported width, and now the row
can decide how big it wants to be.
It takes the sum of its children's widths
and the maximum of their heights.
This resolves to a total size of 190 by 40.
By now, you should have a good understanding
of how constraints affect the size of composables
and how modifiers affect those constraints.
Let's take a closer look at some modifiers
and how they adapt constraints.
We can start with a simple size modifier
that adapts constraints to fixed minimum and maximum values.
In this case, the constraints going in
are quite flexible, allowing widths between 0 and 300
and heights between 0 and 200.
The size modifier changes those constraints
to fixed width and height of 40 before forwarding to its child.
But what if the size you pass falls
outside of the constraints that were passed in?
So, for example, let's say we pass a size modifier of 400 dp.
In that case, the size modifier will
try to match the passed constraints
as closely as it can.
It will still listen to the constraints passed in
and not override them.
In this case, even though the modifier
specifies a size of 400 by 400, the resulting exact constraints
will be 300 by 200.
The same thing happens when the requested size is smaller
than the minimum bounds.
The constraints will be adapted to the lowest possible values
while still adhering to the constraints passed in.
This also explains why chaining multiple size
modifiers doesn't do anything.
This first size modifier will set both the minimum
and maximum bounds to a fixed value.
And even though the second size modifier
requests the size of 50 dp, it has
to adhere to the minimum bound values that were passed to it.
So it will still resolve to a size of 100 dp.
If you really don't want your node
to adhere to the incoming constraints,
you can replace a size modifier with another modifier called
requiredSize.
It will override the incoming constraints,
and it will pass the size that you specify, in this case,
50 dp.
When passing the size back up the tree,
it will reset the reported size to the incoming constraints,
here, 100 by 100 dp.
The child node will then be centered
in that available space.
Now, keep in mind that instead of using the size modifier that
adapts both width and height, we can also use the width modifier
to set a fixed width but leave the height undecided,
or we can use the height modifier to set a fixed height
but leave the width undecided.
Or we can even use a sizeIn modifier for even more
fine-grained control.
You can then specify the minimum and maximum width and height
individually.
Now that we learned about constraints
and how they influence measurements, let's
return to our original use cases and find the right solutions.
This image composable applies to fillMaxSize and the size
modifier, which means it results in a layout node that
looks like this.
Let's say the incoming constraints
are that the image can be anywhere up to 300 in width
and up to 200 dp in height.
The fillMaxSize modifier changes the constraints
to set both the width and height to the maximum value passed in,
300 dp in width and 200 dp in height.
So even though the size modifier wants to use a size of 50
by 50 dp, it still needs to adhere
to these incoming minimum constraints.
And thus, the size modifier will also
output the exact constraint bounds of 300
by 200, effectively ignoring the value that you
provided in the size modifier.
The image follows these bounds and reports a size of 300
by 200, which is passed all the way up.
And so we can see that the right answer was option A.
Now, what would happen when we add a wrapContentSize modifier
to the mix?
The fillMaxSize modifier will still behave the same,
but the wrapContent modifier resets the minimum constraints.
So while fillMaxSize resulted in fixed constraints,
wrapContent modifier resets it back to bounded constraints.
The following nodes can now take up the whole space again or be
smaller than that entire space.
And so the size modifier sets the constraints
to minimum and maximum bounds of 50.
The image thus resolves to a size of 50 by 50,
and the size modifier forwards that.
Now, the wrapContentSize has a special property.
It takes its child and puts it in the center
of the available minimum bounds that were passed to it.
The size it communicates to its parents
is thus equal to those minimum bounds
that were passed into it.
By combining just these three modifiers,
we were able to define a size for our composable
and center it in its parents.
Option B is the right answer.
And now if we look at our final example,
you might already be able to solve it using the knowledge
that we've gained so far.
Looking at our UI tree, there's nothing new.
The clip modifier does not change the constraints,
the padding modifier lowers the maximum constraints,
and the size modifier sets all the constraints to 100 dp.
The image adheres to those constraints,
and it reports a size of 100 by 100 dp.
The padding modifier adds 10 dp on all sizes,
so it increases the reported width and height by 20 dp.
Now, let's see what happens when we start drawing the nodes.
The clip modifier acts on a canvas of 120 by 120 dp.
Thus, it creates a circle mask of that size.
The padding modifier then insets its content
by 10 dp on all sizes, so it lowers the canvas size to 100
by 100 dp.
The image is then drawn in that canvas.
You can see that the image is clipped
based on the original circle of 120 dp.
And thus, we see this weird result.
Option B was the right answer.
Wow.
That was a lot.
You learned about constraints and used
them to reason about modifiers, how to order them,
and measurements.
In the next episode, Simona will show you
how you can use this information to start implementing
your own custom layout.
Thanks for watching.
[MUSIC PLAYING]