[ad_1]
Episode 3 of MAD Skills: Compose Layouts and Modifiers
In the previous MAD Skills article, you learned about the three phases of Compose that transform data into UI. We created a mental model to help us reason about our app’s design implementation. In this episode, we will use that mental model to learn to reason about modifier chaining and how it influences the sizes of our composables.
If you’ve got any questions so far from this series on Compose Layouts and Modifiers, we will have a live Q&A session on March 9th. Leave a comment here, on YouTube, or using #MADCompose on Twitter to ask your questions.
You can also watch this article as a MAD Skills video:
Remember from last episode that we have three phases of transforming data into UI:
- Composition: What to show
- Layout: Where to place it
- Drawing: How to render it
Modifiers can affect different phases. For example, the size
and padding
modifier influence the size and spacing of a composable during the layout phase, and the clip
modifier influences the shape of a composable during the draw phase:
We also know that we can add more than one modifier to a composable, creating a chain. However, it is not always apparent how the different modifiers in those chains affect each other.
Let’s start with some exercises. For each of the following code snippets, try to figure out which option would be the result of executing that snippet, option A or B:
Problem 1
/* Copyright 2023 Google LLC.
SPDX-License-Identifier: Apache-2.0 */Image(
painterResource(R.drawable.frag),
contentDescription = null,
Modifier
.fillMaxSize()
.size(50.dp)
)
Problem 2
/* Copyright 2023 Google LLC.
SPDX-License-Identifier: Apache-2.0 */Image(
painterResource(R.drawable.frag),
contentDescription = null,
Modifier
.fillMaxSize()
.wrapContentSize()
.size(50.dp)
)
Problem 3
/* Copyright 2023 Google LLC.
SPDX-License-Identifier: Apache-2.0 */Image(
painterResource(R.drawable.frag),
contentDescription = null,
Modifier
.clip(CircleShape)
.padding(10.dp)
.size(100.dp)
)
Not exactly sure about the answers? You’ve come to the right place then! Continue reading to understand more. (psst, the answers are at the end of the blog post)
To learn how to reason about modifier order, we will have to learn about the role of Constraints
during the layout phase.
Remember that in the last article we discussed how the layout phase follows a three-step algorithm to find each layout node’s width, height and x, y coordinate:
- Measure children: A node measures its children, if any.
- Decide own size: Based on those measurements, a node decides on its own size.
- Place children: Each child node is placed relative to a node’s own position.
Constraints help finding the right sizes for our nodes during the first two steps of this algorithm. They 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 first step of the algorithm. 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. Then, when it decides its own size, it also adheres to the constraints that were passed in by its own parents.
Types of constraints
Constraints can be bounded, indicating a minimum and a maximum width and height:
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 the node to follow an exact size requirement. The minimum and maximum bounds are set to the same value:
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:
To understand how constraints are passed from parent to child, and how sizes are then resolved based on those constraints, it’s best to walk through an example. However, this is much easier to present in a video format, so I’d suggest you watch the chapter “an example” of the MAD Skills video:
By watching the video you should have a good understanding of how constraints affect the size of composables, and how modifiers affect those constraints. In this section we’ll take a closer look at some specific modifiers and how they impact constraints.
size
modifier
Let’s look at the following UI tree, that should be rendered in a container of 300dp
by 200dp
. The constraints are bounded, allowing widths between 100dp
and 300dp
, and heights between 100dp
and 200dp
.
The size
modifier adapts the incoming constraints to match the value passed to it, for example150dp
:
But what if the requested size is too small or too big? That is, what if the width and height are smaller than the smallest constraint bound, or larger than the largest constraint bound?
In this case, the modifier will try to match the passed constraints as closely as it can, while still adhering to the constraints passed in:
This also explains why chaining multiple size
modifiers doesn’t work. The first size
modifier will set both the minimum and maximum constraints to a fixed value, and even though the second size
modifier requests a smaller or larger size, it still needs to adhere to the exact bounds passed in, so it will not override those values:
requiredSize modifier
If you do need your node to override the incoming constraints, you can replace the size
modifier with another modifier called requiredSize
. It will replace the incoming constraints and pass the size you specify instead, as exact bounds. Then, when the size is passed back up the tree, the child node will be centered in the available space:
width and height modifiers
In previous examples we used a size
modifier, that adapts both width and height of the constraints. However, we can also replace these with the width
modifier, that sets a fixed width but leaves the height undecided. Or we can use the height
modifier, that sets a fixed height but leaves the width undecided:
sizeIn modifier
If you need fine-grained control over the constraints, and want to adapt them to your exact needs, you can use the sizeIn
modifier:
Now that we learned about constraints and how they influence measurements, let’s return to our original use cases and find the right solutions.
Problem 1
/* Copyright 2023 Google LLC.
SPDX-License-Identifier: Apache-2.0 */Image(
painterResource(R.drawable.frag),
contentDescription = null,
Modifier
.fillMaxSize()
.size(50.dp)
)
Here’s the solution:
- The
fillMaxSize
modifier changes the constraints to set both the minimum width and height to the maximum value —300dp
in width, and200dp
in height. - So even though the
size
modifier wants to use a size of50dp
, it still needs to adhere to the incoming minimum constraints. And thus the size modifier will also output the exact constraint bounds of300
by200
, effectively ignoring the value provided in thesize
modifier. - The
Image
follows these bounds and reports a size of300
by200
, which is passed all the way up.
Problem 2
/* Copyright 2023 Google LLC.
SPDX-License-Identifier: Apache-2.0 */Image(
painterResource(R.drawable.frag),
contentDescription = null,
Modifier
.fillMaxSize()
.wrapContentSize()
.size(50.dp)
)
Here’s the solution:
- The
fillMaxSize
modifier will still behave the same, and adapt the constraints to set both the minimum width and height to the maximum value —300dp
in width, and200dp
in height. - The
wrapContentSize
modifier resets the minimum constraints. So whilefillMaxSize
resulted in fixed constraints,wrapContentSize
resets it back to bounded constraints. The following node can now take up the whole space again, or be smaller than the entire space. - The
size
modifier sets the constraints to minimum and maximum bounds of50
. - The
Image
resolves to a size of50
by50
, and thesize
modifier forwards that. - The
wrapContentSize
modifier 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 the minimum bounds that were passed into it.
By combining just three modifiers, we were able to define a size for our composable and center it in its parent!
Problem 3
/* Copyright 2023 Google LLC.
SPDX-License-Identifier: Apache-2.0 */Image(
painterResource(R.drawable.frag),
contentDescription = null,
Modifier
.clip(CircleShape)
.padding(10.dp)
.size(100.dp)
)
Here’s the solution:
- The
clip
modifier does not change the constraints. - The
padding
modifier lowers the maximum constraints. - The
size
modifier sets all constraints to100dp
. - The
Image
adheres to those constraints and reports a size of100
by100dp
. - The
padding
modifier adds10dp
on all sizes, so it increases the reported width and height by20dp
. - Now in the drawing phase, the
clip
modifier acts on a canvas of120
by120dp
. Thus it creates a circle mask of that size. - The
padding
modifier then insets its content by10dp
on all sizes, so it lowers the canvas size to100
by100dp
. - The
Image
is then drawn in that canvas. You can see that the image is clipped based on the original circle of120dp
, and thus we see a non-round result.
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 post we will show you how you can use this information to start implementing your own custom layout.
Got any questions? Leave a comment below or use the #MADCompose hashtag on Twitter and we will address your questions in our upcoming live Q&A for the series on March 9th. Stay tuned!
[ad_2]
Source link
Leave a Reply