I'm trying to make a simple box (same height and width) based on a Column's wrapped height (two Text Composables). Using the aspectRatio Modifier property, the height is matching the full width, even with matchHeightConstraintsFirst = true. How do I get it to become a square from the wrap_content height?
#Composable
fun CompletionSquare(questionType: QuestionType, number: Int) {
Box(
modifier = Modifier
.wrapContentHeight()
.background(color = colorResource(id = R.color.gray))
.padding(1.dp)
) {
Column(
modifier = Modifier
.background(color = colorResource(id = R.color.teal))
.aspectRatio(matchHeightConstraintsFirst = true, ratio = 1.0f)
) {
Text(text = questionType.type)
Spacer(modifier = Modifier.height(16.dp))
Text(text = number.toString())
}
}
}
Related
How can I rotate composables and still make them fill their parent?
For example, I have a Column filling the Screen with two Boxes taking up half the size.
The size of the boxes seems to be calculated before the rotation and not after.
Column(
modifier = Modifier
.fillMaxSize()
.padding(8.dp),
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
Box(
modifier = Modifier
.rotate(90f)
.fillMaxSize()
.weight(1f)
.background(Color.Red),
contentAlignment = Alignment.Center
) {
Text("1", fontSize = 100.sp)
}
Box(
modifier = Modifier
.rotate(90f)
.fillMaxSize()
.weight(1f)
.background(Color.Blue),
contentAlignment = Alignment.Center
) {
Text("2", fontSize = 100.sp)
}
}
Edit after Thracians comment:
This looks better but the maxHeight I get seems wrong, I can see in the layout inspector that the size of the BoxWithConstraints is right
Column(
modifier = Modifier
.fillMaxSize()
.padding(8.dp),
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
BoxWithConstraints(modifier = Modifier.fillMaxSize().weight(1f)) {
Box(
modifier = Modifier
.rotate(90f)
.width(maxHeight)
.height(maxWidth)
.background(Color.Red),
contentAlignment = Alignment.Center
) {
Text("1", fontSize = 100.sp)
}
}
BoxWithConstraints(modifier = Modifier.fillMaxSize().weight(1f)) {
Box(
modifier = Modifier
.rotate(90f)
.width(maxHeight)
.height(maxWidth)
.background(Color.Blue),
contentAlignment = Alignment.Center
) {
Text("2", fontSize = 100.sp)
}
}
Modifier.rotate is
#Stable
fun Modifier.rotate(degrees: Float) =
if (degrees != 0f) graphicsLayer(rotationZ = degrees) else this
Modifier.graphicsLayer{} does not change dimensions and physical position of a Composable, and doesn't trigger recomposition which is very good for animating or changing visual presentation.
You can also see in my question here even i change scale and position green rectangle, position in parent is not changing.
A [Modifier.Element] that makes content draw into a draw layer. The
draw layer can be * invalidated separately from parents. A
[graphicsLayer] should be used when the content * updates
independently from anything above it to minimize the invalidated
content. * * [graphicsLayer] can also be used to apply effects to
content, such as scaling ([scaleX], [scaleY]), * rotation
([rotationX], [rotationY], [rotationZ]), opacity ([alpha]), shadow
However any Modifier after Modifier.graphicsLayer is applied based on new scale, translation or rotation. Easiest to see is drawing border before and after graphicsLayer.
Column(modifier = Modifier.fillMaxSize()) {
val context = LocalContext.current
Row(modifier = Modifier.fillMaxSize()) {
Box(
modifier = Modifier
.size(100.dp, 200.dp)
.border(2.dp, Color.Red)
.zIndex(1f)
.clickable {
Toast
.makeText(context, "Before Layer", Toast.LENGTH_SHORT)
.show()
}
.graphicsLayer {
translationX = 300f
rotationZ = 90f
}
.border(2.dp, Color.Green)
.clickable {
Toast
.makeText(context, "After Layer", Toast.LENGTH_SHORT)
.show()
}
)
Box(
modifier = Modifier
.fillMaxWidth()
.height(200.dp)
.background(Color.Yellow)
)
}
}
If you check this example you will see that even after we rotate Composable on left position of Box with yellow background doesn't change because we don't change actual form of Composable on left side.
#Composable
private fun MyComposable() {
Column(modifier = Modifier.fillMaxSize()) {
Spacer(modifier = Modifier.height(300.dp))
Column(modifier = Modifier.fillMaxSize()) {
var angle by remember { mutableStateOf(0f) }
LaunchedEffect(key1 = Unit) {
delay(2000)
angle = 90f
}
Box(
modifier = Modifier
.fillMaxSize()
.weight(1f)
.background(Color.Red)
.rotate(angle),
contentAlignment = Alignment.Center
) {
Text("1", fontSize = 100.sp)
}
Spacer(modifier =Modifier.height(8.dp))
Box(
modifier = Modifier
.fillMaxSize()
.weight(1f)
.background(Color.Blue)
.rotate(angle),
contentAlignment = Alignment.Center
) {
Text("2", fontSize = 100.sp)
}
}
}
}
Added Spacer to have uneven width and height for Boxes for demonstration.
As i posted in example gif order of rotate determines what we rotate.
modifier = Modifier
.fillMaxSize()
.weight(1f)
.background(Color.Blue)
.rotate(angle)
This sets the size, it never rotates parent but the content or child because we set size and background before rotation. This answer works if width of the child is not greater than height of the parent.
If child has Modifier.fillSize() or child's width is bigger than parent's height when we rotate as in the image left below. So we need to scale it back to parent after rotation since we didn't change parents dimensions.
#Composable
private fun MyComposable2() {
var angle by remember { mutableStateOf(0f) }
LaunchedEffect(key1 = Unit) {
delay(2000)
angle = 90f
}
Column(modifier = Modifier.fillMaxSize()) {
Spacer(modifier = Modifier.height(100.dp))
BoxWithConstraints(
modifier = Modifier
.fillMaxSize()
.padding(8.dp)
) {
val width = maxWidth
val height = maxHeight / 2
val newWidth = if (angle == 0f) width else height
val newHeight = if (angle == 0f) height else width
Column(modifier = Modifier.fillMaxSize()) {
Box(
modifier = Modifier
.border(3.dp, Color.Red)
.size(width, height)
.graphicsLayer {
rotationZ = angle
scaleX = newWidth/width
scaleY = newHeight/height
}
.border(5.dp, Color.Yellow),
contentAlignment = Alignment.Center
) {
Image(
modifier = Modifier
.border(4.dp, getRandomColor())
.fillMaxSize(),
painter = painterResource(id = R.drawable.landscape1),
contentDescription = "",
contentScale = ContentScale.FillBounds
)
}
Box(
modifier = Modifier
.border(3.dp, Color.Red)
.graphicsLayer {
rotationZ = angle
scaleX = newWidth/width
scaleY = newHeight/height
}
.size(width, height)
.border(5.dp, Color.Yellow),
contentAlignment = Alignment.Center
) {
Image(
modifier = Modifier
.border(4.dp, getRandomColor())
.fillMaxSize(),
painter = painterResource(id = R.drawable.landscape1),
contentDescription = "",
contentScale = ContentScale.FillBounds
)
}
}
}
}
}
on left we don't scale only rotate as in question
on right we scale child into parent based on height/width ratio
I am making a "ToggleGroup" with Jetpack-Compose, using essentially a Row into which I print Text. I manage to make it work if I tune the width manually (.width(70.dp) in the code below), but I would like it to automatically do that.
I essentially want this:
But without manually adding .width(70.dp), I get this:
My current (tuned) code is the following:
Row(
horizontalArrangement = Arrangement.End,
) {
options.forEach { option ->
val isSelected = option == selectedOption
val textColor = if (isSelected) Color.White else MaterialTheme.colors.primary
val backgroundColor = if (isSelected) Color.Gray else Color.White
Row(
horizontalArrangement = Arrangement.Center,
modifier = Modifier
.padding(
vertical = 6.dp, horizontal = 1.dp
)
.width(70.dp)
.background(backgroundColor)
.clickable { onSelectionChanged(option) }
) {
Text(
text = option,
color = textColor,
modifier = Modifier.padding(14.dp),
)
}
}
}
It feels similar to this question, but somehow it's different because I use Row and the question uses Column (or at least I did not manage to use Intrinsics correctly).
How could I do that?
Changes required.
1. On the parent Row, use Modifier.width(IntrinsicSize.Min)
(min|max)IntrinsicWidth: Given this height, what's the minimum/maximum width you can paint your content properly?
Source - Docs
2. Use Modifier.weight(1F) on all children.
Size the element's width proportional to its weight relative to other weighted sibling elements in the Row.
The parent will divide the horizontal space remaining after measuring unweighted child elements and distribute it according to this weight.
Source - Docs
3. Use Modifier.width(IntrinsicSize.Max) on all children.
This ensures the Text inside the children composables are not wrapped.
(You can verify this by removing the modifier and adding long text)
Screenshot
Sample code
#Composable
fun AutoWidthRow() {
val items = listOf("Item 1", "Item 2", "Item 300")
Row(
horizontalArrangement = Arrangement.End,
modifier = Modifier.width(IntrinsicSize.Min),
) {
items.forEach { option ->
Row(
horizontalArrangement = Arrangement.Center,
modifier = Modifier
.padding(
vertical = 6.dp, horizontal = 1.dp
)
.width(IntrinsicSize.Max) // Removing this will wrap the text
.weight(1F)
.background(Color.Black)
) {
Text(
text = option,
color = Color.White,
modifier = Modifier
.padding(14.dp),
)
}
}
}
}
You should create a custom Layout
#Composable
fun EqualSizeTiles(
modifier: Modifier = Modifier,
content: #Composable () -> Unit,
) {
Layout(
content = content,
modifier = modifier,
) { measurables, constraints ->
layoutTiles(
measurables,
constraints
)
}
}
private fun MeasureScope.layoutTiles(
measurables: List<Measurable>,
constraints: Constraints,
): MeasureResult {
val tileHeight = constraints.maxHeight
val tileWidths = measurables.map { measurable ->
measurable.maxIntrinsicWidth(tileHeight)
}
val tileWidth = tileWidths.maxOrNull() ?: 0
val tileConstraints = Constraints(
minWidth = tileWidth,
minHeight = 0,
maxWidth = tileWidth,
maxHeight = constraints.maxHeight,
)
val placeables = measurables.map { measurable ->
measurable.measure(tileConstraints)
}
val width = (placeables.size * tileWidth).coerceAtMost(constraints.maxWidth)
return layout(width = width, height = tileHeight) {
placeables.forEachIndexed { index, placeable ->
placeable.place(tileWidth * index, 0)
}
}
}
#Preview(showBackground = true, widthDp = 512)
#Composable
private fun EqualSizeTilesPreview() {
WeatherSampleTheme {
Surface(
modifier = Modifier
.fillMaxWidth()
.background(color = Color.Yellow)
) {
EqualSizeTiles(
modifier = Modifier
.height(64.dp)
.background(color = Color.Green)
.padding(all = 8.dp)
) {
Text(
text = "Left",
textAlign = TextAlign.Center,
modifier = Modifier
.background(color = Color.Red)
.padding(all = 8.dp)
.fillMaxHeight(),
)
Text(
text = "Center",
textAlign = TextAlign.Center,
modifier = Modifier
.background(color = Color.Yellow)
.padding(all = 8.dp)
.fillMaxHeight(),
)
Text(
text = "Right element",
textAlign = TextAlign.Center,
modifier = Modifier
.background(color = Color.Blue)
.padding(all = 8.dp)
.fillMaxHeight(),
)
}
}
}
}
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()
I want to show a list of elements in which each one is an expanded image on top and a title on the bottom.
The image must expand all of its available width but it must have a fixed height.
If the image is big enough I can do it, but if the image has a portrait aspect ratio it doesn't expand as I would like.
This is what I have right now:
The first image has a portrait aspect ratio, I would like it to expand to the available width but maintaining a fixed height.
This is the code for the list and each element:
#Composable
private fun List(memes: List<Meme>) {
LazyColumn(
modifier = Modifier
.fillMaxSize(),
verticalArrangement = Arrangement.spacedBy(12.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
itemsIndexed(memes) { index, meme ->
ListElement(meme, isFirst = index == 0, isLast = index == memes.size - 1)
}
}
}
#Composable
private fun ListElement(meme: Meme, isFirst: Boolean, isLast: Boolean) {
Card(
modifier = Modifier
.fillMaxWidth()
.padding(
start = marginLat,
top = if (isFirst) 8.dp else 0.dp,
end = marginLat,
bottom = if (isLast) 8.dp else 0.dp
),
elevation = 4.dp
) {
Column {
Image(
painter = rememberImagePainter(meme.url),
contentDescription = null,
contentScale = ContentScale.Crop,
modifier = Modifier
.fillMaxWidth()
.height(100.dp)
.padding(8.dp)
.clip(RoundedCornerShape(4.dp))
)
Text(meme.name, style = MaterialTheme.typography.h6, modifier = Modifier.padding(8.dp))
}
}
}
use ContentScale.FillWidth
Image(
painter = rememberImagePainter(meme.url),
contentDescription = null,
contentScale = ContentScale.FillWidth,
modifier = Modifier
.fillMaxWidth()
.height(100.dp)
.padding(8.dp)
.clip(RoundedCornerShape(4.dp))
)
Consider the following image:
I have a Column that has an Image together with another Column that has some Text elements. What I'm trying to do is to have the Image scale uniformly to the available space. I just can't make it work.
Column(
modifier = Modifier
.fillMaxSize()
.background(Color.Cyan)
.padding(horizontal = 16.dp),
verticalArrangement = Arrangement.SpaceEvenly
) {
Image(
modifier = Modifier.fillMaxWidth(),
painter = painterResource(R.drawable.rocket_boy),
contentDescription = null,
contentScale = ContentScale.Fit
)
Column(
modifier = Modifier.fillMaxWidth(),
horizontalAlignment = Alignment.CenterHorizontally
) {
repeat(20) {
Text(text = "${it}")
}
}
}
One of the things that I tried was setting the size of the Image to fillMaxSize and the weight of the second Column to 1f, but it didn't do anything. Maybe I need to have the size of the second Column fixed? Any help is very appreciated.
You haven't used Modifier.weight on the correct child. It should be applied to the view, which you need to fill the rest of the parent.
The parent will divide the vertical space remaining after measuring unweighted child elements and distribute it according to this weight.
Modifier.weight has argument fill with default parameter true. This default parameter works same as Modifier.fillMaxHeight() (in case of Column), if you don't need to fill all height available, you can specify false:
Column(
modifier = Modifier
.fillMaxSize()
.background(Color.Cyan)
.padding(horizontal = 16.dp),
verticalArrangement = Arrangement.SpaceEvenly,
horizontalAlignment = Alignment.CenterHorizontally,
) {
val painter = painterResource(R.drawable.ic_redo)
Image(
modifier = Modifier.weight(1f, fill = false)
.aspectRatio(painter.intrinsicSize.width / painter.intrinsicSize.height)
.fillMaxWidth(),
painter = painter,
contentDescription = null,
contentScale = ContentScale.Fit
)
Column(
horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier.fillMaxWidth()
) {
repeat(20) {
Text(text = "${it}")
}
}
}
It seems to your image size is less than the screen max width, for that reason the container of the image fill the width, but the image remains small, if you fill the height on the image it scales correctly but the image container fills all space leaving below the list. You could try setting and aspect ratio to the modifier to prevent container from filling all available space:
...
val painter = painterResource(id = R.drawable.ic_dismiss_24)
Image(
modifier = Modifier
.aspectRatio(ratio = painter.intrinsicSize.height /
painter.intrinsicSize.width)
.fillMaxWidth()
.fillMaxHeight(),
painter = painter,
contentDescription = null,
contentScale = ContentScale.Fit
)
...
Couldn't get it work right using a Column, but I succeeded using a ConstraintLayout and Nestor's help with the aspectRatio.
ConstraintLayout(
modifier = Modifier.fillMaxSize()
) {
val (button, text) = createRefs()
val painter = painterResource(R.drawable.rocket_boy)
Image(
modifier = Modifier
.aspectRatio(ratio = painter.intrinsicSize.width /
painter.intrinsicSize.height)
.padding(16.dp)
.constrainAs(text) {
top.linkTo(parent.top)
bottom.linkTo(button.top)
height = Dimension.preferredWrapContent
width = Dimension.preferredWrapContent
start.linkTo(parent.start)
end.linkTo(parent.end)
},
painter = painter,
contentDescription = null,
contentScale = ContentScale.Fit
)
Column(Modifier.constrainAs(button) {
bottom.linkTo(parent.bottom, margin = 16.dp)
top.linkTo(text.bottom)
start.linkTo(parent.start)
end.linkTo(parent.end)
height = Dimension.wrapContent
}) {
repeat(5) {
Text(text = "$it")
}
}
}
But if someone thinks that it can still be done with a Column instead of a ConstraintLayout, he can post an answer and I may accept it.
If we consider the height of the parent Column - as the sum of the heights of the Image and the nested Column - it can be argued that the height of your Image should be equal to the remainder of the height, after subtracting from the height of the parent Column - the height of the nested Column.
var textInColumnSize by remember { mutableStateOf(Size.Zero) }
var globalColumnSize by remember { mutableStateOf(Size.Zero) }
val imageHeight: Dp =
LocalDensity.current.run { (globalColumnSize.height -
textInColumnSize.height).toDp() }
Column(
modifier = Modifier
.fillMaxSize()
.onGloballyPositioned { coordinates ->
globalColumnSize = coordinates.size.toSize()
}
.background(Color.Cyan)
.padding(horizontal = 16.dp),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.SpaceEvenly
) {
Image(
modifier = Modifier
.fillMaxWidth()
.height(imageHeight),
painter = painterResource(R.drawable.rocket_boy),
contentDescription = null,
)
Column(
modifier = Modifier
.fillMaxWidth()
.onGloballyPositioned { coordinates ->
textInColumnSize = coordinates.size.toSize()
},
horizontalAlignment = Alignment.CenterHorizontally
) {
repeat(10) {
Text(text = "$it")
}
}
}