I am newbie in learning Android Jetpack Compose. And I am currently creating a Box, set the modifier height and width to be 100 dp, but when I put the Box outside of the Column, it fills the whole layout instead of being kept at the size of 100 x 100dp.
Here is the code:
#Composable
fun MainContent() {
Box(modifier = Modifier
.width(100.dp)
.height(100.dp)
.background(Color.Blue))
Column {
Box(modifier = Modifier
.width(100.dp)
.height(100.dp)
.background(Color.Red))
Box(modifier = Modifier
.width(100.dp)
.height(100.dp)
.background(Color.Green))
}
}
If the parent of your Composable is a Surface, it will try to fill the entire space with the first Composable it finds. So your code might look like this:
Surface(
modifier = Modifier.fillMaxSize()
) {
MainContent()
}
To remove this behavior, you need to wrap you Composable in a Box:
Surface(
modifier = Modifier.fillMaxSize()
) {
Box {
MainContent()
}
}
Before:
After:
Related
I am trying to create a Composable that wraps another content Composable and displays a CircularProgressBar as an overlay on top of it, covering the whole content Composable.
I almost got it working as wished, see the following to images:
Initial state
Loading state
But as you can see, the overlay fills the complete screen instead of just the gray LazyRow item and the text field. Thus, the button is pushed off screen.
This is my current code:
#Composable
fun LoadingBox(
modifier: Modifier = Modifier,
isLoading: Boolean = false,
loadingText: String,
content: #Composable() () -> Unit
) {
Box(modifier = modifier
.fillMaxWidth()
.wrapContentHeight()
) {
content()
if (isLoading) {
Surface(
modifier = Modifier
.fillMaxSize()
.alpha(0.5f),
) {
Column(
modifier = Modifier.fillMaxSize(),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
CircularProgressIndicator()
Text(text = loadingText)
}
}
}
}
}
On the screenshots, I am providing the gray box and the text field as the content parameter. So the overlay should only cover the gray LazyRow item and the text field.
I already stumbled across instrinsic measures, however I cannot use them as the App crashes when I provide a LazyRow as content due to following error:
java.lang.IllegalStateException: Asking for intrinsic measurements of SubcomposeLayout layouts is not supported. This includes components that are built on top of SubcomposeLayout, such as lazy lists, BoxWithConstraints, TabRow, etc. To mitigate this:
- if intrinsic measurements are used to achieve 'match parent' sizing,, consider replacing the parent of the component with a custom layout which controls the order in which children are measured, making intrinsic measurement not needed
You should:
add contentAlignment = Alignment.Center in the parent Box
Remove the Surface
remove the verticalArrangement in the Column
Add an other Box which you can fill with a translucent background
Something like:
Box(modifier = modifier
.fillMaxWidth(),
contentAlignment = Alignment.Center,
) {
content()
if (isLoading) {
Box(
Modifier
.matchParentSize()
.background(Color.Gray.copy(alpha = 0.5f))
)
Column(
modifier = Modifier.fillMaxWidth(),
horizontalAlignment = Alignment.CenterHorizontally
) {
androidx.compose.material.CircularProgressIndicator()
Text(text = loadingText)
}
}
}
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 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
)
}
}
I'm creating a component in Jetpack Compose and realized that when I'm making a Composable inside a Box it's possible that this component assumes 2 maximum fill possibilities: Modifier.fillMaxSize() and Modifier.matchParentSize(). As shown below:
Box(
modifier = modifier // This modifier is received by parameter of another composable function
) {
Canvas(modifier = Modifier.matchParentSize()) {
// Using match parent size
}
}
and
Box(
modifier = modifier // This modifier is received by parameter of another composable function
) {
Canvas(modifier = Modifier.fillMaxSize()) {
// Using fill max size
}
}
What is the practical difference between these 2 modes? And why can't I set Modifier.matchParentSize() to a Column or a Row?
From official doc
Modifier.fillMaxSize modifier, which makes an element occupy all available space, will take part in defining the size of the Box.
So it specifies the size of the element.
But if you use Modifier.matchParentSize() in an element inside of a box it has nothing to do with specifying the size of the box.
The size of the box will be measured by other children element of the box. Then the element with the Modifier.matchParentSize() will match and occupy that size.
You can't use .matchParentSize() in Row or Column because this modifier is part of the BoxScope interface. So it works only with boxscope.
For example, if you use fillMaxSize with something like the below.
Box() {
Text(
modifier = Modifier
.fillMaxSize()
.background(Color.Green),
text = ""
)
Text(
modifier = Modifier
.size(100.dp)
.background(Color.Blue),
text = "Hello",
)
}
You will get this. It will fill the entire screen because of that .fillMaxSize() modifier in the first child.
But if you use this
Box() {
Text(
modifier = Modifier
.matchParentSize()
.background(Green),
text = ""
)
Text(
modifier = Modifier
.size(100.dp),
text = "Hello",
)
}
It will take only 100.dp for the Hello text and then the green background will fill that 100.dp because of that .matchParentSize() modifier in the first child.
I could have used Box instead of Text but more Box can make it confused.
I am currently trying to implement a gridview, it consists of 2 columns. I know i can implement my own grid view using columns and rows, but i just want to use existing approach although it is experimental.
#Composable
fun MyGridScreen() {
LazyVerticalGrid(cells = GridCells.Fixed(2), modifier = Modifier.fillMaxSize(),contentPadding = PaddingValues(12.dp)) {
items(15) {
Box(
contentAlignment = Alignment.Center,
modifier = Modifier
.padding(10.dp)
.height(80.dp)
.background(Color.Red)
.aspectRatio(1f)
) {
Text("Grid item $it", color = Color.White)
}
}
}
}
Below is the result i achieved. I can't put space below the item :(
You need to set verticalArrangement and horizontalArrangement properties on the LazyVerticalGrid composable.
This will space the items by 10.dp in both the vertical and horizontal axis.
#Composable
fun MyGridScreen() {
LazyVerticalGrid(
cells = GridCells.Fixed(2),
modifier = Modifier.fillMaxSize(),
contentPadding = PaddingValues(12.dp),
verticalArrangement = Arrangement.spacedBy(10.dp),
horizontalArrangement = Arrangement.spacedBy(10.dp
) {
items(15) {
Box(
contentAlignment = Alignment.Center,
modifier = Modifier
.padding(10.dp)
.height(80.dp)
.background(Color.Red)
.aspectRatio(1f)
) {
Text("Grid item $it", color = Color.White)
}
}
}
}
Padding is exactly what you need in this case. But you need to understand that in compose modifiers order means a lot. By moving background modifier in between padding and sizes modifiers, you'll get what you need. And clickable ripple will work the same.
Check out more how modifiers order works: https://stackoverflow.com/a/65698101/3585796
.padding(10.dp)
.background(Color.Red)
.height(80.dp)
.aspectRatio(1f)
p.s. if you need your items to be a square, just put your box in one more box. Looks like width of items in LazyVerticalGrid gets overridden by the grid to fill max width, not sure if that's a bug or a feature.