I am confused by what the modifiers fillMaxWidth() and wrapContentWidth() in Jetpack Compose do. I have read through the documentation and looked at their implementations, but I am still not exactly sure how they work (internally).
Here is my understanding of what they do:
fillMaxWidth() sets the width of the element to the maximum width of its parent container. My understanding is that it does so by setting the minimum width and and the maximum width to be equal to the maximum width of the parent.
wrapContentWidth() causes a layout element to be as wide as the element's content.
My questions are:
In case of fillMaxWidth(), is my understanding of what it does internally correct (setting the minimum width and and the maximum width to be equal to the maximum width of the parent)?
In case of wrapContentWidth(), does is causes a layout element to be as wide as the element's content by setting the min and max width of the element to be the width of the content?
Also, what would the following modifier exactly do if I applied it to an element?
modifier = Modifier.fillMaxWidth().wrapContentWidth() Wouldn't the wrapContentWidth() just undo what fillMaxWidth did? And what if I switched the order and used modifier = Modifier.wrapContentWidth().fillMaxWidth()?
Thanks for all help.
fillMaxWidth() will set the min and max width of the composble to the maximum allowed by the container, to fill all the space
wrapContentWidth() will set the min width to 0 (so ignores a previous min width), and you can do the same with the max width if you set unbounded to true
You can combine them to have some effects, for instance you could use a fillMaxWidth to set a certain background color, then have a child composable centered in the same container using wrapContentWidth and wrapContentHeight, as seen here (I used wrapContentSize but the same applies to both wrapContentWidth and wrapContentHeight)
#Preview(widthDp = 360, heightDp = 360)
#Composable
private fun PreviewMOdifier() {
Box(
modifier = Modifier
.fillMaxWidth()
.background(Color.Red)
.wrapContentSize(Alignment.Center)
.background(Color.Blue)
) {
Text(
text = "This is the body",
modifier = Modifier
.padding(all = 16.dp)
.background(Color.Yellow)
)
}
}
Edit: here's another example, try removing wrapContentHeight in the Box modifier so see how the preview changes
#Preview(heightDp = 360)
#Composable
private fun PreviewThreeBlock() {
PlaygroundTheme {
Surface(
color = MaterialTheme.colorScheme.background
) {
Box(
modifier = Modifier.wrapContentHeight().background(Color.Red)
) {
Text(text = "FooBar")
}
}
}
}
In Jetpack Compose setting size of child or/and container Composables are done using Constraints coming from size Modifiers, parent or scroll as they return Constraints.Infinity.
Measuring a Composable with parent or modified Constraints determines which size that child Composable will have
You can check out Constraints section of this answer.
Modifier.fillmaxWidth(fraction) sets min and max width of Constraints to be equal so when you call measurable.measure(constraints) your child Composable is measured with this fixed value.
Modifier.wrapWidth() is actually most of the time is not required when your parent Composable is Row, Column or other built-in Composables because when you don't assign a size Modifier to your Composable it's measured with min-max constraints range of parent which is a range between 0 and parent width.
However, Box has a parameter called propagateMinConstraints or Surface which is a Box with propagateMinConstraints = true forces minimum Constraints to direct descendant as explained in this answer or as #Francesc explained.
#Composable
private fun WrapWidthExample() {
Row() {
Surface(
modifier = Modifier
.size(200.dp)
.border(2.dp, Color.Yellow),
onClick = {}) {
Column(
modifier = Modifier
.size(50.dp)
.background(Color.Red, RoundedCornerShape(6.dp))
) {
Box(
modifier = Modifier
.size(50.dp)
.background(Color.Green, RoundedCornerShape(6.dp))
)
}
}
Surface(
modifier = Modifier
.size(200.dp)
.border(2.dp, Color.Yellow),
onClick = {}) {
Column(
modifier = Modifier
.wrapContentWidth()
.background(Color.Red, RoundedCornerShape(6.dp))
) {
Box(
modifier = Modifier
.size(50.dp)
.background(Color.Green, RoundedCornerShape(6.dp))
)
}
}
}
}
Without Modifier.wrapContentWidth() 200.dp(525px in my device) is forced to red Column for measuring Measurable, but with wrapContentWidth allows that Composable to use its own min Constraints which is 0 because of not assigning any Modifier.
Allow the content to measure at its desired width without regard
for the incoming measurement * [minimum width
constraint][Constraints.minWidth], and, if [unbounded] is true, also
without * regard for the incoming measurement [maximum width
constraint][Constraints.maxWidth]. If * the content's measured size
is smaller than the minimum width constraint, [align] * it within
that minimum width space. If the content's measured size is larger
than the maximum * width constraint (only possible when [unbounded]
is true), [align] over the maximum * width space.
And the Constraints what is returned from this Modifier is as
val wrappedConstraints = Constraints(
minWidth = if (direction != Direction.Vertical) 0 else constraints.minWidth,
minHeight = if (direction != Direction.Horizontal) 0 else constraints.minHeight,
maxWidth = if (direction != Direction.Vertical && unbounded) {
Constraints.Infinity
} else {
constraints.maxWidth
},
maxHeight = if (direction != Direction.Horizontal && unbounded) {
Constraints.Infinity
} else {
constraints.maxHeight
}
)
Basically, wrapContentX can be used when parent forces child Composables to use its Constraints, this can be done when you build your custom Layouts for instance or using Surface as above.
In Jetpack Compose unless you chain Modifier.requiredX first size modifier is applied while the one after it unless there is no padding, size, etc between is ignored. You can check out requiredSize answer in the first link for more details.
Related
Im trying to have a composable scale inside a ConstrainLayout until a specified max size (usually by setting the .sizeIn, .widthIn or .heightIn Modifier) is reached or the borders of the constraints are reached.
Using only .sizeIn, .widthIn or .heightIn will lead to displaying the composable always at the min size.
So we specify .fillMaxSize on the composable to take the maximum amount of space (until the specified maxSize). So far so good..
But now when the ConstraintLayout gets smaller for example due to a smaller screen and therefore the borders of the composable are exceeding the constraintborders, the composable isn't scaled down.
That seems understandable because we haven't specified that the size of the composable should orient itself on the borders of the specified constraints. So we specify height = Dimensions.fillToConstraints and width = Dimensions.fillToConstraints to only use the space emerging from the given constraints.
And here the problem arises. height = Dimensions.fillToConstraints and width = Dimensions.fillToConstraints seem to overwrite the .sizeIn, .widthIn or .heightIn Modifier because now the size of the composable is alway as big as it can be inside the given constraints, not paying attention to the maxWidthor maxHeight specified earlier.
A workaround would be to put that composable inside another one for example a box and configure the Modifier like this:
ConstraintLayout {
...
Box(
modifier = Modifier.constrainAs(icon) {
top.linkTo(greeting.bottom) // some composable on top
bottom.linkTo(menuButtons.top) // some composable on bottom
start.linkTo(parent.start)
end.linkTop(parent.end)
height = Dimension.fillToConstraints
width = Dimension.fillToConstraints
}
) {
Icon( // the size responsive composable
imageVector = Icons.Outlined.RequestQuote,
contentDescription = null,
modifier = Modifier
.sizeIn(20.dp, 20.dp, 200.dp, 200.dp) // min and max size
.fillMaxSize()
.align(Alignment.Center)
)
}
...
}
This would give us the desired behavior. But in my opinion the additionally specified box represents a non needed overhead.
So my question is:
Is there a way to acquire this behavior without using a secondary composable only using a specific Modifier configuration?
This can be done directly in the constraints of the constraintAs
I think what you are looking for is something like
ConstraintLayout {
...
Icon(
imageVector = Icons.Outlined.RequestQuote,
contentDescription = null,
modifier = Modifier.constrainAs(icon) {
top.linkTo(greeting.bottom) // some composable on top
bottom.linkTo(menuButtons.top) // some composable on bottom
start.linkTo(parent.start)
end.linkTop(parent.end)
height = Dimension.preferredWrapContent.atMost(200.dp).atLeast(20.dp)
width = Dimension.preferredWrapContent.atMost(200.dp).atLeast(20.dp)
}
)
Based on the answer of #hoford who provided me with the idea of using the .atMost() and .atLeast() modifiers I came up with this:
ConstraintLayout {
...
Icon(
imageVector = Icons.Outlined.RequestQuote,
contentDescription = null,
modifier = Modifier.constrainAs(icon) {
top.linkTo(greeting.bottom) // some composable on top
bottom.linkTo(menuButtons.top) // some composable on bottom
start.linkTo(parent.start)
end.linkTop(parent.end)
height = Dimension.fillToConstraints.atMost(200.dp).atLeast(20.dp)
width = Dimension.fillToConstraints.atMost(200.dp).atLeast(20.dp)
}
)
I'm currently trying to recreate a behavior, upon adding a new element to a LazyColumn the items start shifting to the right, in order to represent a Tree and make the elements easier to read.
The mockup in question:
Documentation
Reading through the documentation of Jetpack Compose in Lists and grids I found the following.
Keep in mind that cases where you’re nesting different direction layouts, for example, a scrollable parent Row and a child LazyColumn, are allowed:
Row(
modifier = Modifier.horizontalScroll(scrollState)
) {
LazyColumn {
// ...
}
}
My implementation
Box(Modifier.padding(start = 10.dp)) {
Row(
modifier = Modifier
.horizontalScroll(scrollState)
.border(border = BorderStroke(1.dp, Color.Black))
) {
LazyColumn(
modifier = Modifier.fillMaxWidth()
) {
for (i in 0..25) {
item {
OptionItem(Modifier.padding(start = (i*20).dp))
}
item {
TaskItem(Modifier.padding(start = (i*10).dp))
}
}
}
}
.
.
.
}
OptionItem represents the element with the dot at the beginning, and TaskItem the other one.
When testing the LazyColumn, it appears as if instead of having a fixed size, the size of the column starts growing just after the elements have gone outside the screen, this causes a strange effect.
As you can see in the GIF, the width of the column starts increasing after the elements no longer fit in the screen.
The Question
I want to prevent this effect from happening, so is there any way I could maintain the width of the column to the maximum all the time?
The reason that applying a simple fillMaxWidth will not work because you are telling a composable to stretch to max, but that is impossible because the view itself can stretch indefinitely since it can be horizontally scrollable. I'm not sure why do you want to prevent this behavior but perhaps maybe you want your views to have some initial width then apply the padding, while maintaining the same width. what you can do in such case is simply give your composables a specific width, or what you can do is to get the width of the box and apply them to your composables by width (i used a text in this case)
val localDensity = LocalDensity.current
var lazyRowWidthDp by remember { mutableStateOf(0.dp) }
Box(
Modifier
.padding(start = 10.dp)
.onGloballyPositioned { layoutCoordinates -> // This function will get called once the layout has been positioned
lazyRowWidthDp =
with(localDensity) { layoutCoordinates.size.width.toDp() } // with Density is required to convert to correct Dp
}
) {
val scrollState = rememberScrollState()
LazyColumn(
modifier = Modifier
.fillMaxWidth()
.horizontalScroll(scrollState)
) {
items(25) { i ->
Text(
text = "Hello",
modifier = Modifier
.padding(start = (i * 20).dp)
.width(lazyRowWidthDp)
.border(1.dp, Color.Green)
)
}
items(25) { i ->
Text(
text = "World",
modifier = Modifier
.padding(start = (i * 10).dp)
.width(lazyRowWidthDp)
.border(1.dp, Color.Green)
)
}
}
}
Edit:
you can apply horizontal scroll to the lazy column itself and it will scroll in both directions
I have a component set with Modifier.wrapContentHeight() which height has different height depending on its content. I want to adjust other component's padding to the first component height dynamically. I tried something like this:
var height = 0
val resourceContentBottomPadding = height + 16
LazyColumn(
modifier = Modifier.fillMaxSize(),
contentPadding = PaddingValues(start = 16.dp, end = 16.dp, bottom = height.dp),
) { /.../ }
Column(
modifier = Modifier
.fillMaxWidth()
.wrapContentHeight()
.onGloballyPositioned {
height = it.size.height
}
) {
if (something)
SomeComposeElement()
Button()
}
Unfortunately it changes nothing.
If you put your LazyColumn and Column inside an ancestor Column and set modifier = Modifier.fillMaxWidth().weight(1f) for your LazyColumn it will only occupy space after Column is laid out which is total height - height of the Column
In Jetpack Compose, I have a card composable that I want to be a minimum of 100.dp tall. However, if I use heightIn, the card consumes all available height up until the max. How do I set a minHeight without consuming all height?
Surface(
modifier = heightIn(100.dp, 9999.dp),
) {
// Content
}
To be clear, what I want is the following algorithm:
height = if (contentHeight > minHeight) {
contentHeight
} else {
minHeight
}
Use Modifier.defaultMinSize
Surface(
modifier = Modifier.defaultMinSize(height = 100.dp),
) {
// Content
}
Edit
To be clear, what I want is the following algorithm:
height = if (contentHeight > minHeight) {
contentHeight } else {
minHeight }
Modifier.height(contentHeight.coerceAtLeast(minHeight)) is the shortened algorithm.
When a Layout measures and places children it checks constraints. You can find detail answered here about how it works.
For instance column has minHegith = 100.dp, while maxHeight is your composable height in dp. If it's root it's height of screen in dp
Column(modifier=Modifier.heightIn(min=100.dp)) {
Box(modifier=Modifier.fillMaxSize().background(Color.Yellow))
}
For instance this Box covers whole space because it's min and max constraint is Column's maxHeight. In this case using Modifier.heightIn() won't limit, so you need Modifier.height() such as in my original answer with a fixed value or use custom layout to check and assign .
layout(width, height){} inside Layout.
But there is something to note with Surface that it forces minimum constraints to direct descendant, you can check my answer about Surface here.
Surface(modifier = Modifier.heightIn(min = 100.dp)) {
Box(
modifier = Modifier
.size(300.dp)
.background(Color.Yellow)
)
}
This Box in example will cover all of the height because of being Surface's direct descendant.
Surface(modifier = Modifier.heightIn(min = 100.dp)) {
Column() {
Box(
modifier = Modifier
.size(300.dp)
.background(Color.Yellow)
)
}
}
This one will have 300.dp size while Column covers all available space of Surface
I have the following composable.
#Composable
fun Temp() {
Row(
modifier = Modifier
.background(Color.Red)
.height(IntrinsicSize.Min)
.fillMaxWidth()
) {
Text(text = "Hello", fontSize = 10.sp)
Icon(
imageVector = Icons.Default.Star,
contentDescription = "Star",
modifier = Modifier.fillMaxHeight()
)
}
}
The height of icon is not decreasing from 24.dp. Is there any way I can achieve this behavior. I want the icon size to just be the height of the parent row.
If the text is large. The icons size is increased. I think it has to be with icon minimum size being 24.dp. How can I make icon smaller?
Your code actually works as expected - that's how intrinsic calculations work.
Compose checks the min height of each view and chooses the maximum of those values. In your case, the min height of the image is related to the intrinsic size of the image, which you cannot control in the case of Icons.Default.
A possible solution is to use Modifier.layout. When Compose calculates the intrinsic height, the height constraint will be infinite, in which case you can layout it as a zero size view, so that your text will be the highest. When the intrinsic height is determined, you can measure and position the icon:
Row(
modifier = Modifier
.background(Color.Red)
.height(IntrinsicSize.Min)
.fillMaxWidth()
) {
Text(text = "Hello", fontSize = 10.sp)
Icon(
imageVector = Icons.Default.Star,
contentDescription = null,
modifier = Modifier
.layout { measurable, constraints ->
if (constraints.maxHeight == Constraints.Infinity) {
layout(0, 0) {}
} else {
val placeable = measurable.measure(constraints)
layout(placeable.width, placeable.height) {
placeable.place(0, 0)
}
}
}
)
}
Using Modifier.layout you can change size of view and its position. Usually you use it like this:
First parameter, measurable is an object on which you can call measure with constraints - the second layout parameter. measure is gonna calculate the size your view will take, taking constraints in count.
in layout you need to pass the desired view size - usually it can be taken from placeable from the previous step.
inside layout you need to call place on the placeable with the desired offset.
With height(IntrinsicSize.Min) layout content is getting called multiple times:
during the first call(s) max height constraint is equal to Infinity, so intrinsic calculations can select the correct size ignoring the parent size.
In the last call max height constraint is equal to the calculated parent intrinsic height.
In my code during first calls, when height constraint is equal to Infinity, I say that this view has zero size, so it's not counted in intrinsic measurements. When intrinsic height is defined, I can layout it with the final constraints.