I define a val modifier with height(30.dp), then I pass it with modifier.height(5.dp) to Spacer function.
I think the final height of Spacer should be 5.dp because I have overwritten the height of val modifier.
It seems that the height of Spacer isn't 5.dp, what's wrong with my code?
Code A
val modifier = Modifier.fillMaxWidth().height(30.dp)
Spacer(modifier = modifier.height(5.dp))
Order of Modifiers in Composable functions is important.
Refer to this solution and other solutions in the question for more info on this.
If you want the height inside the Spacer to be the final one, you have to use Modifier.then().
The given code
val modifier = Modifier.fillMaxWidth().height(30.dp)
Spacer(modifier = modifier.height(5.dp))
is same as
Spacer(modifier = Modifier.fillMaxWidth().height(30.dp).height(5.dp))
But, if you change it using then() like this
val modifier = Modifier.fillMaxWidth().height(30.dp)
Spacer(modifier = Modifier.height(5.dp).then(modifier))
It would become
Spacer(modifier = Modifier.height(5.dp).fillMaxWidth().height(30.dp))
Sample code
#Composable
fun OrderOfModifiers() {
val modifier = Modifier
.fillMaxWidth()
.height(30.dp)
Column(
modifier = Modifier
.fillMaxSize(),
) {
Spacer(modifier = modifier
.height(5.dp)
.background(DarkGray),
)
Spacer(modifier = Modifier
.height(5.dp)
.then(modifier)
.background(Cyan),
)
}
}
Sample screenshot
When chaining size modifiers such as Modifier.height(height1).height(height2) first one is used by design. As in Abhimanyu's answer using Modifier.then() doesn't change this either, first one is used. Then changes in which order modifiers are applied.
This approach creates opportunity for developers to assign default size when no modifier with size is set by devs use this Composable. Slider is built in similar fashion, it covers full width by default.
When you create a Composable such as
#Composable
private fun MyComposable(modifier: Modifier=Modifier){
Box(modifier = modifier
.fillMaxWidth()
.size(48.dp))
}
And use it as
MyComposable(modifier = Modifier.border(3.dp, Color.Green))
Will result having a Composable with full screen width and 48.dp height. If you set a Modifier with a size default one gets overridden
MyComposable(modifier = Modifier.border(3.dp, Color.Red).size(50.dp))
I would like to implement a row of images with a maximum height of 15dp. If the screen is too thin to display the images with a height of 15dp, they should all be resized for the row to fit the screen width (images must always have the same height). I know it looks simple but I can't find a way to make it work.
Here is a quick graphic I made to explain what I want to achieve:
Any help would be greatly appreciated.
The problem here is that you are missing the ratio of width to height. I assume the ratio is 10.
Row(
modifier = Modifier
.heightIn(max = 15.dp)
.aspectRatio(10f)
) {
listOf(
1f to Color.Yellow,
0.7f to Color.Red,
1.5f to Color.Green,
1f to Color.Black,
).forEach { (w, c) ->
Box(
modifier= Modifier
.fillMaxHeight()
.weight(w)
.background(c)
)
}
}
If you set maxHeight to 1000dp to simulate a smaller screen, the width of Row will be limited to ScreenWidth.
I get it, you want to automatically determine the ratio based on the image size. In compose, the constraint is passed from the outside in, so it would be slightly unnatural to implement such a feature.
Box(
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center
) {
Column(verticalArrangement = Arrangement.spacedBy(16.dp)) {
Test(3)
Test(5)
Test(15)
}
}
#Composable
fun Test(times: Int) {
Row(
modifier = Modifier
.heightIn(max = 50.dp)
.height(IntrinsicSize.Min)
.background(Color.Yellow),
horizontalArrangement = Arrangement.spacedBy(4.dp)
) {
repeat(times) {
val painter = painterResource(id = R.drawable.ic_launcher_background)
Image(
painter = painter,
contentDescription = null,
modifier = Modifier
.fillMaxHeight()
.weight(
weight = painter.intrinsicSize.width / painter.intrinsicSize.height,
fill = false
)
)
}
}
}
I have a compose card and inside a row with circle views, I need to make it so the last item in this row is half shown only. Is there a easy way to achieve this without measuring the screen width after everything is drawn and then modify the padding for the row items dynamically to achieve it?)
If you need to calculate the number of elements depending on the size of the cell contents, it is impossible to do this without real measurements.
But if you know exactly how many elements you need to display, you can use Modifier.fillParentMaxWidth with the desired fraction. This is only available in lazy views like LazyRow.
Things are a bit more complicated with spacings: usually contentPadding is used to offset the first element, which reduces the parent size, depending on which Modifier.fillParentPaxMaxWidth calculates the actual value. Also, if you apply it to both start and end, you won't get the desired result. That's why I apply it only to start, and create an equivalent effect at the end with another item.
I also manually surround the Spacers item instead of using Arrangement.spacedBy, because the spacers should be inside the Modifier.fillParentMaxWidth too.
val spacing = 20.dp
val halfSpacing = spacing / 2
val shape = RoundedCornerShape(20)
LazyRow(
contentPadding = PaddingValues(start = halfSpacing),
modifier = Modifier
.padding(30.dp)
.border(1.dp, color = Color.Black, shape = shape)
.clip(shape)
.padding(vertical = 20.dp)
) {
items(10) {
Row(
Modifier
.fillParentMaxWidth(1f / 3.5f)
) {
Spacer(Modifier.size(halfSpacing))
Box(
Modifier
.weight(1f)
.aspectRatio(1f)
.background(Color.Blue, shape = CircleShape)
)
Spacer(Modifier.size(halfSpacing))
}
}
item {
Spacer(Modifier.size(halfSpacing))
}
}
Result:
LazyColumn variant:
val spacing = 20.dp
val halfSpacing = spacing / 2
val shape = RoundedCornerShape(20)
LazyColumn(
contentPadding = PaddingValues(top = halfSpacing),
modifier = Modifier
.padding(30.dp)
.border(1.dp, color = Color.Black, shape = shape)
.clip(shape)
.padding(horizontal = 20.dp)
) {
items(10) {
Column(
Modifier
.fillParentMaxHeight(1f / 3.5f)
) {
Spacer(Modifier.size(halfSpacing))
Box(
Modifier
.weight(1f)
.aspectRatio(1f)
.background(Color.Blue, shape = CircleShape)
)
Spacer(Modifier.size(halfSpacing))
}
}
item {
Spacer(Modifier.size(halfSpacing))
}
}
I have a "simple" layout in Compose, where in a Column there are two elements:
top image
a grid of 4 squares underneath, each square containing some text
I'd like the layout to have the following behaviour:
set the maximum height of the image to be screenWidth
set the minimum height of the image to be 200.dp
the image should always be in a square container (cropping of the image is fine)
let the grid "grow" as much as it needs to, to wrap around the content, making the image shrink as necessary
This means that if the text in the squares is short, the image will cover a large square on the top. But if any of the square text is really, long, I want the whole grid to scale up and shrink the image. These are the desirable outcomes:
When text is short enough
When a piece of text is really long
I have tried this with ConstraintLayout in Compose, but I can't get the squares to scale properly.
With a Column, I can't get the options to grow with large content - the text just gets truncated and the image remains a massive square.
These are the components I'd built:
// the screen
Column {
Box(modifier = Modifier
.heightIn(min = 200.dp, max = screenWidth)
.aspectRatio(1f)
.border(BorderStroke(1.dp, Color.Green))
.align(Alignment.CenterHorizontally),
) {
Image(
painter = painterResource(id = R.drawable.puppy),
contentDescription = null,
contentScale = ContentScale.Crop
)
}
OptionsGrid(choicesList, modifier = Modifier.heightIn(max = screenHeight - 200.dp))
}
#Composable
fun OptionsGrid(choicesList: List<List<String>>, modifier: Modifier = Modifier) {
Column(
modifier = modifier
.border(1.dp, Color.Blue)
.padding(top = 4.dp, bottom = 4.dp)
.fillMaxHeight(),
verticalArrangement = Arrangement.Center
) {
choicesList.forEach { choicesPair ->
Row(modifier = Modifier.weight(0.5f)) {
choicesPair.forEach { choice ->
Box(
modifier = Modifier
.padding(4.dp)
.background(Color.White)
.weight(0.5f)
) {
Option(choice = choice)
}
}
}
}
}
}
#Composable
fun Option(choice: String) {
Box(
modifier = Modifier
.fillMaxSize()
.background(Color.Yellow)
.border(BorderStroke(1.dp, Color.Red)),
contentAlignment = Alignment.Center
) {
Text(
text = choice,
modifier = Modifier.padding(8.dp),
textAlign = TextAlign.Center,
)
}
}
Do I need a custom layout for this? I suppose what's happening here is that the Column is measuring the image first, letting it be its maximum height, because there is space for that on the screen, and then when measuring the grid, it gives it the remaining space and nothing more.
So I'd need a layout which starts measuring from the bottom?
Here's how you can do it without custom layout.
You need your image size to be calculated after OptionsGrid. In this case you can use Modifier.weight(1f, fill = false): it forces all the views without Modifier.weight to be layout before any weighted elements.
Modifier.weight will override your Modifier.heightIn, but we can restrict it size from the other side: using Modifier.layout on OptionsGrid. Using this modifier we can override constraints applied to the view.
p.s. Modifier.heightIn(max = screenWidth) is redundant, as views are not gonna grow more than screen size anyway, unless the width constraint is overridden, for example, with a scroll view.
.height(IntrinsicSize.Min) will stop OptionsGrid from growing more than needed. Note that is should be placed after Modifier.layout, as it sets height constraint to infinity. See why modifiers order matters.
val choicesList = listOf(
listOf(
LoremIpsum(if (flag) 100 else 1).values.first(),
"Short stuff",
),
listOf(
"Medium length text",
"Hi",
),
)
Column {
Box(
modifier = Modifier
.weight(1f, fill = false)
.aspectRatio(1f)
.border(BorderStroke(1.dp, Color.Green))
.align(Alignment.CenterHorizontally)
) {
Image(
painter = painterResource(id = R.drawable.profile),
contentDescription = null,
contentScale = ContentScale.Crop
)
}
OptionsGrid(
choicesList,
modifier = Modifier
.layout { measurable, constraints ->
val placeable = measurable.measure(constraints.copy(
// left 200.dp for min image height
maxHeight = constraints.maxHeight - 200.dp.roundToPx(),
// occupy all height except full image square in case of smaller text
minHeight = constraints.maxHeight - constraints.maxWidth,
))
layout(placeable.width, placeable.height) {
placeable.place(0, 0)
}
}
.height(IntrinsicSize.Min)
)
}
Result:
I suppose what's happening here is that the Column is measuring the
image first, letting it be its maximum height, because there is space
for that on the screen, and then when measuring the grid, it gives it
the remaining space and nothing more.
That is correct, it goes down the UI tree, measures the first child of the column(the box with the image) and since the image doesn't have any children, it returns it's size to the parent Column.
(see documentation)
I'm pretty sure this requieres a custom layout, so this is what I came up with:
First, modified your composables a bit for testing purposes (tweaked some modifiers and replaced the Texts with TextFields to be able to see how the UI reacts)
#ExperimentalComposeUiApi
#Composable
fun theImage() {
Box(
contentAlignment = Alignment.Center,
modifier = Modifier
.aspectRatio(1f)
.border(BorderStroke(1.dp, Color.Green))
.background(Color.Blue)
) {
Image(
painter = painterResource(id = R.drawable.ic_launcher_foreground),
contentDescription = null,
contentScale = ContentScale.Crop,
modifier = Modifier
.border(BorderStroke(2.dp, Color.Cyan))
)
}
}
#Composable
fun OptionsGrid(modifier: Modifier = Modifier) {
Column(
modifier = modifier
.border(1.dp, Color.Blue)
.padding(top = 4.dp, bottom = 4.dp)
.height(IntrinsicSize.Min),
verticalArrangement = Arrangement.Center
) {
repeat(2){
Row(modifier = Modifier.weight(0.5f)) {
repeat(2){
Box(
modifier = Modifier
.padding(4.dp)
.background(Color.White)
.weight(0.5f)
.wrapContentHeight()
) {
Option()
}
}
}
}
}
}
#Composable
fun Option() {
var theText by rememberSaveable { mutableStateOf("a")}
Box(
modifier = Modifier
.fillMaxSize()
.background(Color.Yellow)
.border(BorderStroke(1.dp, Color.Red)),
contentAlignment = Alignment.Center
) {
OutlinedTextField(value = theText, onValueChange = {theText = it})
}
}
And now, the custom layout
Since subcompose needs a slotId, and you only need IDs for the image and grid, you can create an Enum class with two ids.
enum class SlotsEnum {Main, Dependent}
slotID: A unique id which represents the slot we are composing into.
If you have fixed amount or slots you can use enums as slot ids, or if
you have a list of items maybe an index in the list or some other
unique key can work. To be able to correctly match the content between
remeasures you should provide the object which is equals to the one
you used during the previous measuring. content - the composable
content which defines the slot. It could emit multiple layouts, in
this case the returned list of Measurables will have multiple
elements.
Then, with this composable, which receives a screen width, height, an optional modifier and the image, as well as the grid
#Composable
fun DynamicColumn(
screenWidth: Int,
screenHeight: Int,
modifier: Modifier = Modifier,
img: #Composable () -> Unit,
squares: #Composable () -> Unit
)
You can measure the total height of the grid and use that to calculate the height of the image (still haven't managed to a proper UI when scaled under 200dp, but it shouldn't be diffcult).
SubcomposeLayout { constraints ->
val placeableSquares = subcompose(SlotsEnum.Main, squares).map {
it.measure(constraints)
}
val squaresHeight = placeableSquares.sumOf { it.height }
val remainingHeight = screenHeight - squaresHeight
val imgMaxHeight = if (remainingHeight > screenWidth) screenWidth else remainingHeight
val placeableImage = subcompose(SlotsEnum.Dependent, img).map{
it.measure(Constraints(200, screenWidth, imgMaxHeight, imgMaxHeight))
}
Then, apply the constraints to the image and finally place the items.
layout(constraints.maxWidth, constraints.maxHeight) {
var yPos = 0
placeableImage.forEach{
it.place(x= screenWidth / 2 - it.width / 2, y= yPos)
yPos += it.height
}
placeableSquares.forEach{
it.place(x=0, y=yPos)
}
}
and finally, just call the previous composable, DynamicColumn:
#ExperimentalComposeUiApi
#Composable
fun ImageAndSquaresLayout(screenWidth: Int, screenHeight: Int) {
DynamicColumn(screenWidth = screenWidth, screenHeight = screenHeight,
img = { theImage() },
squares = { OptionsGrid() })
}
PS: possibly will update this tomorrow if I can fix the minimum width issue
I have a Stack widget which hosts a Box and an Image.
As the state changes, I want to scale the Box widget by whatever value the state has, for example by 2x.
I couldn't find anything about scaling the widgets on the Modifier or Box properties so I decided to react to the state changes by manipulating the size using "Modifier.size" which is not ideal for me.
So is there support for scaling the widgets or should I manually play with the size property?
-Thanks
#Composable
fun Pointer(modifier: Modifier = Modifier, state: TransitionState, onClick: () -> Unit) {
Stack(modifier) {
Box(
shape = CircleShape, backgroundColor = Color.Gray.copy(alpha = .3f),
modifier = Modifier.size(state[width])
)
Image(
asset = imageResource(id = R.drawable.ic_pointer),
modifier = Modifier
.clickable(onClick = onClick)
)
}
}
Compose 1.0.0-alpha08
As time passes by, there is a new compose version which renames 'drawLayer' to 'graphicsLayer' and adds a new scale modifier (uses 'graphicsLayer' underneath).
Thus composable will look like:
#Composable
fun Pointer(scale: Float, modifier: Modifier = Modifier) {
Box(modifier) {
Box(
modifier = Modifier
.matchParentSize()
.scale(scale)
.background(Color.Cyan, CircleShape)
)
Image(
imageVector = Icons.Filled.Done,
modifier = Modifier
.align(Alignment.Center)
)
}
}
Compose 1.0.0-alpha07 (original answer)
I believe you may achive the desired behavior with drawLayer modifier. For example a simple composable, which displays an scalable circle and an unscalable icon on top of it:
#Composable
fun Pointer(scale: Float, modifier: Modifier = Modifier) {
Box(modifier) {
Box(
modifier = Modifier
.matchParentSize()
.drawLayer(scaleX = scale, scaleY = scale)
.background(Color.Cyan, CircleShape)
)
Image(
asset = Icons.Filled.Done,
modifier = Modifier
.align(Alignment.Center)
)
}
}
And usage:
Pointer(
scale = 1f,
modifier = Modifier
.background(Color.Magenta)
.padding(25.dp)
.preferredSize(50.dp)
.align(Alignment.CenterHorizontally)
)
When scale = 1f
When scale = 2f
I don't want to care about the size of the box or explicitly keep a reference to it
That is going to be a problem. In Compose, widgets like Box() are stateless functions. You cannot ask a Box() how big it is — instead, you need to tell the Box() how big it is, using a suitable Modifier.
Frequently, "how big it is" is a fixed value or rule, set in the code (e.g., Modifier.size(200.dp)). You should be able to have the size be dependent upon some state that you track yourself, so long as that state is a State, so Compose knows to recompose (call your function again) when that State changes. If you go that route, then scaling is a matter of checking the current State value, applying your scale factor, and using the result for the new State value.