Vlad
11/28/2024, 10:10 AM@Composable
fun NoClicksForContentBox(
clicksEnabled: Boolean,
content: @Composable () -> Unit
) {
// if clicksEnabled == false we need to intercept any clicks so content can't receive them
Box {
content()
}
}
We used in the Box a Spacer-overlay with fill max size to get take the clicks, but it adds other (measuring) issues for us while used in diff components.Stylianos Gakis
11/28/2024, 10:20 AM.pointerInput(Unit) {}
modifier, does it intercept the clicks?
Also, what were the measuring issues?
If you wrap something in a Box with propagateMinConstraints = true
it should more or less just be a no-op as far as the layout goes. Besides losing scope information like if you were in a RowScope etc.Vlad
11/28/2024, 10:22 AMVlad
11/28/2024, 10:25 AMSpacer(
modifier = Modifier
.pointerInput(Unit) {}
.fillMaxSize()
.background(color = containerColor.copy(alpha = 0.7f))
)
as a "Curtrain" on top of the Content arrived in the Bottom sheets. .fillMaxSize started to expand content and we got empty space.
To fix that we used IntrinsicSize.Max
for the outer box. But that doesn't work well if the content is LazyColumnVlad
11/28/2024, 10:37 AM.matchParentSize()
and it did the trick because I don't need IntrinsicSize.Max
now.Vlad
11/28/2024, 10:54 AM@Composable
fun AppLoadingCurtain(
modifier: Modifier = Modifier,
loading: Boolean,
containerColor: Color = AppTheme.colors.surfaceContainerLowest,
progressSize: Dp = 32.dp,
content: @Composable () -> Unit
) {
Box(
modifier = modifier,
) {
content()
if (loading) {
Spacer(
modifier = Modifier
.pointerInput(Unit) {}
.matchParentSize()
.background(color = containerColor.copy(alpha = 0.7f))
)
AppCircularProgressIndicator(
modifier = Modifier
.size(progressSize)
.align(Alignment.Center)
)
}
}
}
In case anyone will be googling the same issue, coz I didn't find anything useful for the task.
That what we use as a Generic progress state for many different components.
Idea is that it makes the content a lil bit grayed + not clickable.Stylianos Gakis
11/28/2024, 10:57 AMpropagateMinConstraints
on that outer box, otherwise the content()
lambda will lose whatever the min size used to be above this container. So this would mean that you are potentially introducing a behavior change when you do or do not have that curtain on top of everything.
Then on your AppCircularProgressIndicator
you can do wrapContentSize(Alignment.Center)
instead of .align(Alignment.Center)
Vlad
11/28/2024, 11:00 AMpropagateMinConstraints
yetStylianos Gakis
11/28/2024, 11:09 AMpropagateMinConstraints
will simply take what the min constraints were at that layer, and let the child also be measured with those.
Otherwise inside the box, everything will be allowed to take up a minimum of (0,0) size.
So then to counter-act the fact that the loading indicator will now take up as much space as the container would, you wrap its content size and it all works out in the end.
Good to note that the min constraints for the outer box are most likely gonna be equal to the max constraints, aka taking up the entire screen, because it's very likely that you are doing fillMaxSize()
on it, since you want your screen to take up the entire screen size by default. fillMaxSize()
does exactly that, sets the min constraints to be equal to the max ones.Vlad
11/28/2024, 11:58 AMStylianos Gakis
11/28/2024, 1:40 PMfillmaxWidth()
on.
If you now wrap it in a AppLoadingCurtain
, and your card's content is actually not naturally as big as the entire width, the card content will then not actually take up the entire width. Since inside your box the min constraints are still back to 0.Stylianos Gakis
11/28/2024, 1:49 PMSurface(color = Color.Transparent, modifier = Modifier.fillMaxSize()) {
Column {
Card(Modifier.fillMaxWidth()) {
Text("I am some short content")
}
}
}
This gives the output as in this picture [1]
---
Then if you decide to wrap some of the content you have with your curtain composable like this:
Surface(color = Color.Transparent, modifier = Modifier.fillMaxSize()) {
Column {
AppLoadingCurtain(Modifier.fillMaxWidth()) {
Card {
Text("I am some short content")
}
}
}
}
You will now get output like picture [2]
---
If however you then do make sure to do the propagation of the min constraints on your box
Box(modifier = modifier, propagateMinConstraints = true) {
content()
...
then without any code changes on the call-site you get picture [3] againStylianos Gakis
11/28/2024, 1:52 PMfillMaxWidth
from the Card there. Well very often, if you've extracted your composable to a separate function, you will pass a modifier
in there, and as a rule you need to pass that modifier
to the first, top-most composable that emits UI inside your function.
If you have made a change in a composable from this:
@Composable
private fun MyCard(modifier: Modifier = Modifier) {
Card(modifier) {
Text("I am some short content")
}
}
to this:
@Composable
private fun MyCard(modifier: Modifier = Modifier) {
AppLoadingCurtain(modifier) {
Card {
Text("I am some short content")
}
}
}
You would then experience this behavior change, only because you wrapped your content in your curtain container.
While if you do use propagateMinConstraints
, you will maintain the same correct behavior regardles of if you do or don't wrap anything with your curtain composable.Vlad
11/28/2024, 1:55 PMYou may wonder why you'd drop the fillMaxWidth from the Card there
That exactly what I was wondering before I saw this lineStylianos Gakis
11/28/2024, 1:56 PMVlad
11/28/2024, 1:56 PMpropagateMinConstraints
.
Because I know that I have plenty of boxes but pretty much never propagatingStylianos Gakis
11/28/2024, 1:58 PMYou need to apply a modifier to a composable that doesn't accept a Modifier parameter (e.g. a content lambda). In this case, make sure to also pass propagateMinConstraints or you're effectively adding a wrapContentWidth to the end of the modifier chain you pass to Box, which is rarely intentional.
So what it says is that if you wrap any composable in any sort of Box()
, if you do not also pass propagateMinConstraints
then you are effectively doing a wrapContentWidth
on the content you pass into the box.
That is the rule of thumb of what happens. If you realize that you would not call wrapContentSize()
in that scenario on your content, then you want propagateMinConstraints
as true
Stylianos Gakis
11/28/2024, 4:40 PMBox
alters the content's constraints exactly in the way we've been discussing here. There they do it by getting rid of the box since they could get away with it in that particular occasion.
You see, anyone can make this mistake, even if you are experienced enough to work in the Compose team itself. If it can happen there, it can happen to anyone. So my verdict is that everyone should read this article and help their future selves πVlad
11/28/2024, 4:41 PMStylianos Gakis
11/28/2024, 4:43 PMpropagateMinConstraints
just did not exist, then surely I would call it a hate relationship πZach Klippenstein (he/him) [MOD]
11/28/2024, 5:09 PMVlad
12/23/2024, 11:23 AM@Composable
fun AppLoadingCurtain(
modifier: Modifier = Modifier,
loading: Boolean,
containerColor: Color = AppTheme.colors.surfaceContainerLowest,
progressSize: Dp = 32.dp,
content: @Composable () -> Unit
) {
Box(
modifier = modifier,
propagateMinConstraints = true
) {
content()
if (loading) {
Box(
modifier = Modifier
.pointerInput(Unit) {}
.matchParentSize()
.background(color = containerColor.copy(alpha = 0.7f)),
propagateMinConstraints = true
) {
AppCircularProgressIndicator(
modifier = Modifier
.wrapContentSize(Alignment.Center)
.size(progressSize)
)
}
}
}
}
Turned out there is no need for additional spacer for the click blocking overlay, seems more declarative to put these into the Box modifiers.
UPD: Removed matchParentSize from Progress modifierStylianos Gakis
12/23/2024, 1:32 PMVlad
12/23/2024, 1:34 PMmatchParentSize on the progress indicator.
I do. Otherwise the Progress indicator is not centered vertically but Aligned to top, since outer box is bigger than the progressSize.
You also probably don't need the align TopStart
this is just default of the BoxStylianos Gakis
12/23/2024, 1:36 PMVlad
12/23/2024, 1:51 PMStylianos Gakis
12/23/2024, 1:54 PMprogressSize
which is within 0..full-screen-size so it gets set to that, and is aligned according to wrapContentSize.
I hope my explanation makes sense.Vlad
12/23/2024, 1:58 PMStylianos Gakis
12/23/2024, 2:00 PMVlad
12/23/2024, 2:06 PMStylianos Gakis
12/23/2024, 2:07 PMStylianos Gakis
12/23/2024, 2:08 PM