Hello. Is there is a simple way which I don't see ...
# compose
v
Hello. Is there is a simple way which I don't see to intercept/block all clicks for the content of a container? Basically we want some kinda of universal "Loading state" box which should prevent any interaction with whatever content it has.
Copy code
@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.
s
If you add a empty pointerInput
.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.
v
No, empty pointerInput doesn't do the trick. I guess because the click already processed by content and the outer box doesn't have a chance.
The issue we faced with having this
Copy code
Spacer(
                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 LazyColumn
Seems like replacing .fillMaxSize with the
Copy code
.matchParentSize()
and it did the trick because I don't need
IntrinsicSize.Max
now.
πŸ‘ 1
Copy code
@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.
❌ 1
s
I would suggest you do
propagateMinConstraints
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)
v
I wanted to do so, but when I did I got issue with the progress taking full size when .fillMaxSize passed outside yes. I will see that, thank you. Still not comfortable with the
propagateMinConstraints
yet
s
Yeah,
propagateMinConstraints
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.
πŸ€” 1
v
We actually very rarely do the fillMaxSize on the Curtain. But there are cases sure. We wrap often small elements aka: Profile image while updating, Switch element with label, Sheets content etc. Well, because of the designs of course
s
It's still relevant. Let's say you have say some card which you were doing
fillmaxWidth()
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.
πŸ€” 1
You have this code:
Copy code
Surface(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:
Copy code
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
Copy code
Box(modifier = modifier, propagateMinConstraints = true) {
  content()
  ...
then without any code changes on the call-site you get picture [3] again
You may wonder why you'd drop the
fillMaxWidth
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:
Copy code
@Composable
private fun MyCard(modifier: Modifier = Modifier) {
  Card(modifier) {
    Text("I am some short content")
  }
}
to this:
Copy code
@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.
v
You may wonder why you'd drop the fillMaxWidth from the Card there
That exactly what I was wondering before I saw this line
πŸ‘ 1
s
Hehe yeah, I have had the same question in the past, but then I noticed that well, it does happen much more often than you'd imagine with how we use modifiers in extracted composables.
v
Is there are simple rule when we should
propagateMinConstraints
. Because I know that I have plenty of boxes but pretty much never propagating
s
There was a post recently by here by @Zach Klippenstein (he/him) [MOD] https://blog.zachklipp.com/centering-in-compose/ and while the entire post is about constraints and talks about much more that what we are talking about here, it has one piece of information which I think is super interesting and very relevant here. Particularly this:
Copy code
You 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
πŸ‘€ 1
And boy do I have a very interesting thing for you to see. This is a commit inside BasicTextField itself https://android-review.googlesource.com/c/platform/frameworks/support/+/3345024 which fixes this problem where a container
Box
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 πŸ˜„
v
The compose is love/hate relationship.
s
Wouldn't call it a hate relationship. You are provided with the tools to do what you want to do, and the API of doing or not doing this is just flipping one boolean. If there was no way to go around this and
propagateMinConstraints
just did not exist, then surely I would call it a hate relationship πŸ˜„
z
I wish more foundation composables propagated constraints by default. It would make them much more useful and their behavior easier to predict. Box for one perhaps, but especially Column and Row.
βž• 1
v
Can't edit initial Curtain code, so posting V2 with the changes/notes in the thread:
Copy code
@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 modifier
s
Since you propagate min constraints, I don't think you need the matchParentSize on the progress indicator. You also probably don't need the align TopStart on the box since you again just have the child take up the entire size instead. I'd try to remove those and see if it all still works the same. Just for the sake of having it look a bit simpler.
v
matchParentSize 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 Box
s
If you propagate min constraints, and the box itself has matchParentSize, won't the progress indicator take up the entire size itself too anyway? Then wrapContentSize can be used for the alignment
πŸ€” 1
v
No clue how you visualize that. But you seem to be right, again. When I replaced the Spacer with Box for blocking overlay part I forgot to add to it propagating to true and that's why I needed it. After adding propagating adding matchParentSize seem to be redundant or just effectively bad.
s
How I visualize this is this. The outer box is match parent size so I imagine it just takes up the entire size of your content, aka it has min and max constraints equal to that max size. Then you propagate those min constraints, so whatever child you put in your box will have minimum constraints of the entire size available again. The progress indicator is that child, so it receives min constraints of the max size, therefore it will be measured as such. You then do wrapContentSize on it, which effectively just turns the minimum constraints back to 0, 0 and lets you align it as you wish. Then the progress indicator has min constraints of 0, 0 and max constraints of the full size. You then give it size
progressSize
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.
πŸ‘ 1
v
Yea it does. I just not sure how do I train my brain to automatically visualize that as I code. I hope I will just gain that skill with experience. Before this thread I didn't think pretty much at all about the propagation so. Thank you for your inputs
s
Oh yeah it took me a long time to feel somewhat confident with how all the constraints are passed down etc. and I absolutely still make wrong assumptions and make mistakes, but all you gotta do is look how it looks like and start adjusting it until it clicks πŸ˜… If this thread is your first time when you think about propagation then don't worry about it, you'll get used to it now that you know more about it!
v
Do you have any tricks when you learn by doing such things? I usually add different .background(Color)'s to the elements or between modifiers to see with my eyes what is happening. Is it stupid?
s
Nah that's how I do it too. I do prefer .border(Color.Red, 1.dp) myself, and change colors for individual items. But honestly that's exactly what I've done numerous times to try and make all of this make sense in my head.