How would y’all write a composable screen where yo...
# compose
s
How would y’all write a composable screen where you want to have one item (TextField) in the center of the screen, and one button which is sort of anchored to the bottom of the screen (aka comes higher when the IME comes up too) My problem with this is that I can’t figure out a nice way to have that item be in the middle of the screen, without then having that and the bottom attached item go on top of each other. Pictures in thread.
Something like this is what I have in mind
But when the screen is small, and I use a box, something like this would happen
And when doing it in a column and doing something like
Copy code
Column {
 Spacer(weight(1f))
 TextField()
 Spacer(weight(1f))
 Button()
}
Then I’d get something like this instead. Where the TextField obviously goes really high up.
I wonder if I am really being stupid here and there’s something obvious I could be doing 😅
o
What do you expect for smaller screen? The textfield gets pushed up?
s
Yeah, moves up together with the button (keeping a minimum space between the two)
o
That's tricky really. I would imagine you could use constraint layout with button constrained to the bottom and it's top to the textfield. Textfield as well constrained to the button and to the parent top. Button would have a bias of 1, pushing it all the way down whereas textfield would stay in the middle. Or something similar. Need to validate this to be sure though
This might have similar behaviour as the column example you posted though.
s
Yeah it’s more tricky than what I’d like it to be 😄 Yeah even with CL it may be not what I am looking for. And in general I haven’t used CL in compose before tbh, I try to avoid it when I can do something simpler instead.
o
Another approach would be to observe the softkeyboard state and reduce the Spacer weight when it opens.
s
What I am trying to do now, is have them all in a column, with the weights as I described there, and if the IME is showing, then simply not have the weight below the text field. Then to make this smooth, I can animate the content placement of the textfield so that it moves between those two positions as the IME is coming in or out
Another approach would be to observe the softkeyboard state and reduce the Spacer weight when it opens.
I tried something like this too, but the values I get from the ime is a value in pixels on how big the IME is. I then had to take this value and kinda normalize it between 0 and 1. And even after I did so, the TextField was first moving a bit down and then up again, like bouncing around a bit, so I scrapped that idea too.
Thanks for sharing your thoughts on this and helping me out btw, I really appreciate it!
s
Can you do something like:
Copy code
Column {
  Box(Modifier.weight(1f) {
    TextField(Modifier.align(Alignment.Center))
  }
  Button()
}
t
I feel your pain 🙂 I wish the whole IME thing were simpler
s
Hmm don’t think the IME is the issue here, it could be some other content that was expanding at the bottom of the screen too, it’d have the same effect.
s
Oh! Sorry, misunderstood. Gotcha.
I guess you need a custom layout.
t
Taking the keyboard away and just making it any component, the whole "center" (for the text field) gets arbitrary. I know exactly how I'd do this in iOS with constraints, but I'm not as strong with Android constraints. iOS would be a series of lesser priority constraints: • vspace between button and text set to >= 20 (or whatever) at a priority of 100 • vposition of text set to screen height / 2 (plus any edge adjustments) at a priority of 99 The screen height, rather than the parent height, is the important part. Because when you're "centering", the centering is relative to a view that is normally anticipated to be full-ish screen
a
Don't be afraid of writing a custom layout. It's probably easier than you think 😉
c
I haven't tried this... but if I had to, then I think I would have done it how you explained. Centered text field. button with imePadding. and a spacer with weight 1f. Very "common" case I suppose though. let me know what you end up doing.
s
Yeah I did go with a custom layout after-all and it seems to be doing what I want it to so far! Context, this Layout is part of a scrollable Column, where the other child is that top app bar that you see, so in small screens it should still work. The code is here:
Copy code
private const val TextFieldId = "TextFieldId"
private const val ContinueButtonId = "ContinueButtonId"
private const val BottomWindowInsetsId = "BottomWindowInsetsId"

Layout(
  content = {
    InputTextField(
      ...
      modifier = Modifier.layoutId(TextFieldId),
    )
    ContinueButton(
      ...
      modifier = Modifier.layoutId(ContinueButtonId),
    )
    Spacer(
      Modifier
        .layoutId(BottomWindowInsetsId)
        .windowInsetsPadding(
          WindowInsets.safeDrawing.only(WindowInsetsSides.Bottom),
        ),
    )
  },
  modifier = Modifier.weight(1f),
) { measurables, constraints ->
  val looseConstraints = constraints.copy(minWidth = 0, minHeight = 0)
  val textFieldPlaceable = measurables.first { it.layoutId == TextFieldId }.measure(looseConstraints)
  val buttonPlaceable = measurables.first { it.layoutId == ContinueButtonId }.measure(looseConstraints)
  val insetsPlaceable =
    measurables.first { it.layoutId == BottomWindowInsetsId }.measure(looseConstraints)
  val maxWidth = constraints.maxWidth
  val maxHeight = constraints.maxHeight
  val spacingHeight = 16.dp.roundToPx() // The space between the three items
  layout(maxWidth, maxHeight) {
    val insetsYPosition = maxHeight - insetsPlaceable.height
    insetsPlaceable.place(0, insetsYPosition)
    val buttonYPosition = insetsYPosition - spacingHeight - buttonPlaceable.height
    buttonPlaceable.place(0, buttonYPosition)
    val textFieldYPosition = minOf(
      (maxHeight / 2) - (textFieldPlaceable.height / 2),
      buttonYPosition - spacingHeight - textFieldPlaceable.height,
    )
    textFieldPlaceable.place(0, textFieldYPosition)
  }
}
The explanation in words is that it takes the available space (Below the topAppBar all the way to the bottom of the screen, since I’m going edge to edge), and places the insets at the bottom. Then a 16.dp space over it. Then the bottom attached button. Then a 16.dp space over that. Then for the text-field, all the magic happens in this line
Copy code
val textFieldYPosition = minOf(
  (maxHeight / 2) - (textFieldPlaceable.height / 2),
  buttonYPosition - spacingHeight - textFieldPlaceable.height,
)
It finds the center of that available screen space (Yes this isn’t the absolute center of the phone screen, but of the available space, so minus topAppBar which also includes the status bar, but this is good enough for me for now. if I wanted to I could add that TopAppBar into the Layout too and it’d be super simple to do as well). And it also finds the position where it’d be 16dps above the botton. And then it simply places it at the one which is higher up. This also makes it so that as the keyboard is animating up in the latest OS versions, I get it to feel like it gets attached to the button and animates up with it. So cool, and it was actually so simple to write anyway. I was just initially scared since I didn’t know how it’d play with keyboard animations, but nah, it plays super well with it. I’ll just try to tweak it a bit so that the top app bar doesn’t get hidden like that on the small screen scenario as seen in the video p.s. Thanks everyone for chiming in here, I really appreciate all of you 🤗
o
This is super cool. Thanks for sharing your final solution.
s
Aha, I see what’s wrong with the height going over the top app bar but not becoming scrollable. I decide the height of that Layout using weight(1f), which in the situation where it’s smaller than what the content is, then the content is drawn out of bounds and it’s centered in the available space, therefore going over the top app bar. I guess what I would need here is a way to do weight(1f) for that case, but when the available space is smaller than what I need, still measure as bigger than what this weight(1f) would give me, and therefore make the column scrollable again. Damn, super tricky, I don’t know how to do this again now, will bash my head against it for a bit more 😅 But I guess this is the answer as to why I didn’t go with a custom layout to start with, usually it’s the niche situations that are hard with it, not the easy scenarios where your expectations (like having enough space) are true.
e
Another approach is using a custom
Arrangement.Vertical
. Here is an arrangement that does something similar but not exactly the same.
Copy code
private object SearchEmptyArrangementWith16Spacing : Arrangement.Vertical {

  override val spacing = 16.dp

  override fun Density.arrange(totalSize: Int, sizes: IntArray, outPositions: IntArray) {
    require(sizes.size > 2)

    val spacing = spacing.roundToPx()
    val buttonIndex = sizes.lastIndex
    val buttonHeight = sizes.last()
    outPositions[buttonIndex] = totalSize - buttonHeight // Place button at the bottom

    val remainingSize = totalSize - buttonHeight - spacing
    var groupSize = (sizes.size - 2) * spacing // Add spacing between elements excl button
    for (i in 0 until buttonIndex) { // Add sizes of elements excl button
      groupSize += sizes[i]
    }

    if (groupSize <= remainingSize) {
      var current = CenterVertically.align(groupSize, remainingSize)
      for (i in 0 until buttonIndex) {
        outPositions[i] = current
        current += sizes[i] + spacing
      }
    } else {
      var current = remainingSize
      for (i in buttonIndex - 1 downTo 0) {
        current -= sizes[i]
        outPositions[i] = current
        current -= spacing
      }
    }
  }
}
It places a button (last node in the column) at the bottom and for the rest of the content, if there is enough space, they are all grouped & centered or else if there is not enough space for all of them, we arrange them bottom up (the bottom elements are the most important, so we make sure they show first)
177 Views