compose-menu icon indicating copy to clipboard operation
compose-menu copied to clipboard

IME padding lag in ModalBottomSheet

Open Radiokot opened this issue 8 months ago • 10 comments

Hi 👋🏻 Thank you for your hard work on non-material components.

In my app, I have a ModalBottomSheet which presents a form containing an input field. The input field has a FocusRequester, and requestFocus() is called in a LaunchedEffect in order to show the keyboard. Sheet within the ModalBottomSheet has .imePadding() modifier. Unfortunately, in most cases (not always) this leads to a situation when first the enter animation is played behind the keyboard, and only then IME padding is applied. Any ideas how this can be fixed without introducing magic delays?

https://github.com/user-attachments/assets/45beff65-80f5-49d9-9066-37d83e6590fa

Radiokot avatar May 14 '25 19:05 Radiokot

I suspect that this is a combination of a few things and might not be related to Unstyled, but let's see what can be done.

Can you let me know the version of Android you are running in the video, and which soft keyboard you are using?

alexstyl avatar May 14 '25 19:05 alexstyl

  • It's Android 13 (SDK 33)
  • HeliBoard, but Samsung keyboard behaves in the same way

Radiokot avatar May 14 '25 19:05 Radiokot

Could you share a minimum reproducible code so that i can check what is happening on my end?

alexstyl avatar May 14 '25 19:05 alexstyl

val sheetState = rememberModalBottomSheetState(
    initialDetent = Hidden,
)

ModalBottomSheet(state = sheetState) {
    Scrim()

    Sheet(
        modifier = Modifier
            .fillMaxWidth()
            .background(Color.White)
            .imePadding()
    ) {
        Box(
            modifier = Modifier
                .fillMaxWidth()
                .height(200.dp)
                .padding(24.dp)
        ) {
            val focusRequester = remember {
                FocusRequester()
            }

            BasicTextField(
                value = "This is a test", onValueChange = {},
                modifier = Modifier
                    .focusRequester(focusRequester)
            )

            LaunchedEffect(Unit) {
                focusRequester.requestFocus()
            }
        }
    }
}

LaunchedEffect(Unit) {
    delay(1000)
    sheetState.targetDetent = FullyExpanded
}

https://github.com/user-attachments/assets/20a93510-9824-477e-8fbb-8bb69a8baa81

Radiokot avatar May 14 '25 19:05 Radiokot

tween() animation spec with the duration lower than 100 ms prevents this, but this is a damn fast animation 😅

Radiokot avatar May 14 '25 19:05 Radiokot

You shouldn't need to use any delays or animations. Something seems to be up. Let me investigate and get back to you asap :)

alexstyl avatar May 14 '25 19:05 alexstyl

As a temporary solution you can use this to wait until the sheet is fully visible before focusing the text field:

        LaunchedEffect(sheetState.isIdle) {
            if(sheetState.targetDetent == FullyExpanded) {
                focusRequester.requestFocus()
            }
        }

I am still investigating what is the cause of the delay, but this should be a much nicer UX for your app in the meantime

alexstyl avatar May 15 '25 20:05 alexstyl

Thanks, Alex, sure. In my case it's just more complicated because the sheet is used to show navigation destinations, so the content doesn't know about the sheet state. For now, I decided to use a very fast animation.

Radiokot avatar May 16 '25 06:05 Radiokot

IIRC you dont need to explicitly focus on the text field for it to gain focus. You could have the sheet itself focus its contents and the result should be the same. Not 100% sure though.

Anyhow, if the animation workaround works for you, im glad. Will keep you posted on any updates I have

alexstyl avatar May 16 '25 08:05 alexstyl

By the way, if you're interested in a navigator utilizing ModalBottomSheet: https://github.com/Radiokot/4money/blob/main/app/src/main/java/ua/com/radiokot/money/BottomSheetNavigation.kt

It uses a single sheet as a host, and if there are few back stack items, then the sheet shows the top one.

Radiokot avatar May 16 '25 15:05 Radiokot

Added a new verticalOffset parameter to the sheets for handling such cases. It allows you to contribute to the internal offset of the sheet for when setting an imePadding() is not working as expected (such as your case).

Before After

The updated version of your code would be:

val sheetState = rememberModalBottomSheetState(
    initialDetent = Hidden,
)

ModalBottomSheet(state = sheetState) {
    Scrim()

    Sheet(
        modifier = Modifier
            .fillMaxWidth()
            .background(Color.White),
//            .imePadding() <-- remove this
            verticalOffset = WindowInsets.ime.asPaddingValues().calculateBottomPadding(),
    ) {
        Box(
            modifier = Modifier
                .fillMaxWidth()
                .height(200.dp)
                .padding(24.dp)
        ) {
            val focusRequester = remember {
                FocusRequester()
            }

            BasicTextField(
                value = "This is a test", onValueChange = {},
                modifier = Modifier
                    .focusRequester(focusRequester)
            )

            LaunchedEffect(Unit) {
                focusRequester.requestFocus()
            }
        }
    }
}

LaunchedEffect(Unit) {
    delay(1000)
    sheetState.targetDetent = FullyExpanded
}

alexstyl avatar Jul 24 '25 03:07 alexstyl

Thank you, Alex! Looking forward to trying it out when the new version is released 🤝🏻

Radiokot avatar Jul 24 '25 06:07 Radiokot

@alexstyl Alex, the fix works for IME padding, but it doesn't go well with windowInsetsPadding modifier. In my app, I show bottom sheets with custom background and long lists, which are drawn behind the navigation bar.

If I combine imeAware = true for the Sheet and windowInsetsPadding(WindowInsets.navigationBars) modifier for the inner content, I get redundant bottom space when the keyboard is shown:

Image Image

However, if I use imePadding() modifier on the Sheet, it works well, but the original animation issue is of course present in this case:

Image Image

Seems that imeAware = true doesn't consume the bottom navigation bar inset when the keyboard is shown, unlike how it works with the imePadding() modifier.

Radiokot avatar Jul 24 '25 11:07 Radiokot

I can have the sheet consume the ime padding.

Any chance you can provide some code so that I can test out on my side? doesn't have to be working, but rather pseudo code with modifiers you use and in how many components ie

This will speed up the process

ModalBottomSheet {
   Sheet(imeAware = true, modifier = Modifier.navigationBarsPadding()) {
      
   }
}

alexstyl avatar Jul 24 '25 12:07 alexstyl

Thank you. Here's a snippet with imports:

https://gist.github.com/Radiokot/da3a3ef6abfa62166f55d1f6f985e5bf

It illustrates the unwanted space brought by navigation bars padding modifier when the keyboard is shown:

Image Image

Radiokot avatar Jul 24 '25 19:07 Radiokot

This was exactly what I needed. Thanks!

I just pushed a hot fix (https://github.com/composablehorizons/compose-unstyled/releases/tag/1.38.1). Could you kindly let me know if it fixes the issue for you?

alexstyl avatar Jul 25 '25 03:07 alexstyl

Many thanks for your work, Alex. It works as expected now 👏🏻 Please let me know if you accept Bitcoin or PayPal donations, I think the framework you built deserves financial support.

Radiokot avatar Jul 25 '25 08:07 Radiokot

Awesome! Glad we got it working :)

I don't accept donations from individuals. If you do want to support the open source project, you can buy the UI Kit I am working on at: https://composables.com/ui-kit

It is the styled version of Compose Unstyled that works for all platforms not just Android.

alexstyl avatar Jul 25 '25 08:07 alexstyl