Jetpack Compose - Row of images with a max height - android

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
)
)
}
}
}

Related

Why are the modifier sizes not overwritten?

I have a composable function it assigns a size to a surface.
#Composable
private fun CreateImage(modifier: Modifier = Modifier) {
Surface(
modifier = modifier
.size(150.dp)
.padding(5.dp),
shape = CircleShape,
border = BorderStroke(1.dp, Color.LightGray)
) {
Image(
painter = painterResource(id = R.drawable.profile_image),
contentDescription = "Profile Image",
contentScale = ContentScale.Crop
)
}
}
When I call another method and change the size in the modifier parameter shouldn't it stick to 150dp.
If I call this method:
#Composable
private fun ChangeSize(name: String) {
CreateImage(Modifier.size(100.dp))
}
It stays with the size 100dp even though in CreateImage I set it to 150dp. Why is the size not changing to 150dp and staying 100dp?
I thought it was supposed to change it to 150dp. Why is that not the case?
Modifier uses first size it got and it's a great feature when people don't provide any size to your Composable.
For instance
CircleDisplay(
modifier = circleModifier
.widthIn(min = 70.dp)
.heightIn(min = 70.dp),
color = color
)
if anyone doesn't provide any Modifier with any width or height instead of having 0 height and width you give a minimum size to your Composable. You can change this to max or exact size depending on your implementation. But when user modifier has some width height instead of yours the one provided by them is used thanks to using first size.
Default Composables like Slider also use this pattern, so without setting any dimension it has 48.dp height and fills max width of its parent.
BoxWithConstraint under Slider is as
BoxWithConstraints(
modifier
.minimumTouchTargetSize()
.requiredSizeIn(minWidth = ThumbRadius * 2, minHeight = ThumbRadius * 2)
.sliderSemantics(value, tickFractions, enabled, onValueChange, valueRange, steps)
.focusable(enabled, interactionSource)
) {
// Rest of the Slider Implementation
}
The reason why your composable will always have a size of 150.dp is because of how modifiers are applied. When modifiers are chained onto a composable, they are applied sequentially from top to bottom until the last modifier is applied. This can be demonstrated with a very simple Box composable
#Composable
fun Screen(){
Box(
modifier = Modifier
.size(100.dp)
.background(color = Color.Yellow)
.background(color = Color.Green)
.background(color = Color.Red)
)
In this simple Box composable, the rendered color will be Color.Red because it will be the last color applied before it gets drawn onto the screen.
A similar thing is happening in your example above. Even though you are calling your composable with a modifier of 100.dp, the final size that get's applied is 150.dp because your modifier with 100.dp get's applied too early in the modifier chain.
Replace your composable with this one and it should work as expected
#Composable
private fun CreateImage(modifier: Modifier = Modifier) {
Surface(
modifier = Modifier
.size(150.dp)
.padding(5.dp)
.then(modifier), //this last modifier will override everything above
shape = CircleShape,
border = BorderStroke(1.dp, Color.LightGray)
) {
Image(
painter = painterResource(id = R.drawable.profile_image),
contentDescription = "Profile Image",
contentScale = ContentScale.Crop
)
}
}

Setting LazyColumn Item Height to Maximum Available in Jetpack Compose [duplicate]

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))
}
}

Spacing views inside a compose row so the last view always is half shown

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))
}
}

Overlap two Box jetpack compose

I'm trying to overlap two Box or perhaps is better to use Row on this case.
My design is one Row overlapped with another one, and I've wrapped it on a Column, is that correct?
This is the design, what I'd like to have is the rectangle of the top be the same size of the one below and then move it some pixels as you can see in the image, but they should have the same width but not the same height.
Is it okay if the hierarchy is :
Column
Box (the one of the top)
Row
Box (the one of the bottom)
Row (inside there is text and it's all the same align)
......
I've faced with this some days ago and I solved it using ConstraintLayout.
What I had to do is :
Add implementation "androidx.constraintlayout:constraintlayout-compose:1.0.0-beta02" to build.gradle
Wrap every Box in a ConstraintLayout { .. }
Inside each Box add a Modifier.constrainAs to align the Top Bottom Start End as you want.
If you want the first box be the same width as the second one without hardcoding the dps you should use width = Dimension.fillToConstraints
fillToConstraints - the layout will expand to fill the space defined by its constraints in that dimension.
Basic example without hard-coding :
ConstraintLayout() {
val (title, description) = createRefs()
Box(
modifier = Modifier
.padding(start = 28.dp)
.background(color = Red)
.padding(
horizontal = 16.dp,
)
.constrainAs(title) {
top.linkTo(parent.top)
start.linkTo(parent.start)
end.linkTo(parent.end)
width = Dimension.fillToConstraints
}
) {
Text(text = "Hello World")
}
Box(
modifier = Modifier
.padding(end = 4.dp)
.background(Color.Magenta)
.padding(bottom = 5.dp, start = 8.dp, end = 16.dp, top = 4.dp)
.constrainAs(description) {
top.linkTo(title.top, margin = 16.dp)
start.linkTo(parent.start)
end.linkTo(parent.end)
bottom.linkTo(parent.bottom)
}
) {
Text(text = "Skizo-ozᴉʞS rules")
}
}
Now you have to play with the padding according to your UI and adapt it, result is something like this :
This is way using BoxWithConstraints and not using fixed width and height:
BoxWithConstraints(
Modifier
.background(color = Color.Blue)
.padding(20.dp)) {
val boxWidth = this.maxWidth
Box(
modifier = Modifier
.width(boxWidth - 10.dp)
.background(Color.Red)
) {
Text(text = "Hello Android")
}
Column() {
Spacer(modifier = Modifier
.height(10.dp)
.width(10.dp))
Row( ) {
Spacer(modifier = Modifier.width(10.dp))
Box(
modifier = Modifier
.width(boxWidth)
.zIndex(2f)
.background(Color.Yellow)
) {
Text("aa", modifier = Modifier.background(color = Color.Green))
}
}
}
}
In order for the Composables to overlap, you should put them in the same Box. Try this out:
Box(modifier = Modifier.size(width = 300.dp, height = 100.dp)) {
Row(modifier = Modifier
.size(width = 200.dp, height = 50.dp)
.background(color = Color.Blue)
.align(Alignment.TopEnd)) {}
Row(modifier = Modifier
.size(width = 200.dp, height = 70.dp)
.background(color = Color.Red)
.align(Alignment.BottomStart)) {}
}
You can achieve this in many ways,
#Composable
fun BoxOverBox() {
Box(
modifier = Modifier.fillMaxSize()
.background(Color.White),
contentAlignment = Alignment.Center
) {
Box(
modifier = Modifier
.width(200.dp)
.height(50.dp)
.background(Color.Red)
)
Box(
modifier = Modifier
.width(200.dp)
.height(50.dp)
.zIndex(2f)
.graphicsLayer {
translationX = -50f
translationY = 50f
}
.background(Color.Blue)
)
}
}
I think you must use "matchParentSize" modifier that is avaliabele inside BoxScope, I mean inside Box composable, that modifer measure other children size except itself when it has join the composable at first time to apply the same size to itself.
you can see this modifier in documentation
https://developer.android.com/reference/kotlin/androidx/compose/foundation/layout/BoxScope#(androidx.compose.ui.Modifier).matchParentSize()

How to resize an item in a Compose Column, depending on another item

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

Categories

Resources