Skip to content

Conversation

@t0maboro
Copy link
Contributor

@t0maboro t0maboro commented Sep 25, 2025

Description

This PR improves the way the BottomSheet on Android handles the on-screen keyboard (IME). Previously, the keyboard was not respected properly - opening the keyboard could cause content to be partially or fully obscured due to incorrect handling of insets.

With this update:

  1. We listen to dynamic bottom IME insets to track the keyboard position in real-time.
  2. The BottomSheet's Y translation now directly reflects the current IME inset, preventing it from being overlapped by the keyboard.
  3. Since we have implemented custom enter/exit animations (fade + slide), IME insets must be taken into account both during animations and in live state updates to avoid layout jumps.

There are two key touchpoints where we react to IME inset changes:

  • During enter/exit animations: When autofocus is enabled or when entering/leaving with the keyboard open, we need to align the BottomSheet's position with the keyboard height continuously.
  • In the onProgress callback: This listens to dynamic IME inset changes outside of enter/exit animations — e.g. when the keyboard resizes or changes without the BottomSheet visibility changing.

Priority is given to the animation layer for transforming the component's position, while the onProgress callback serves as a fallback when animations are not active. This prioritization enables us to maintain a smooth and consistent visual appearance across all lifecycle scenarios of the BottomSheet.

Fixes: #3181

Changes

  • Aligned the method for calculating available space for bottom sheet with Android
  • Fixed the height calculation on KeyboardDidHide state for fitToContents
  • Synchronized translationY updates between our custom entering/exiting animations with onProgress callback from insets listener.
  • Added a new example for testing this scenario

Screenshots / GIFs

Here you can add screenshots / GIFs documenting your change.

You can add before / after section if you're changing some behavior.

Before

test3248-main.mov

After

test3248.mov

Test code and steps to reproduce

Tested on android with Test3248

Tested with API levels 29 or higher, because I noticed another issue regarding bottom sheets on 28: https://github.com/software-mansion/react-native-screens-labs/issues/480

Checklist

  • Included code example that can be used to test this change
  • Ensured that CI passes

@t0maboro t0maboro marked this pull request as ready for review September 29, 2025 11:05
@t0maboro t0maboro force-pushed the @t0maboro/keyboard-avoiding-formsheet-android branch from 39c6b3e to bda8134 Compare September 29, 2025 11:15
@t0maboro t0maboro changed the title WIP - fix(Android, Stack): Moving formsheet above keyboard fix(Android, Stack): Moving formsheet above keyboard Sep 29, 2025
@t0maboro t0maboro marked this pull request as draft September 29, 2025 11:34
@t0maboro
Copy link
Contributor Author

Moving back to draft for a while, because I noticed some issues with autofocus and textinput below API 30

Copy link
Contributor

@kligarski kligarski left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I had already started the review before you moved the PR to draft so I'll finish it.

This looks great! I have some remarks/questions:

  1. Just to be sure, this is not a regression, right? (not respecting navigation bar insets)
Screen_recording_20250929_140451.mp4
  1. Do you think we can do something about the gap between keyboard and formsheet when they're both hiding at the same time? Should we create a ticket to investigate it in the future?
Screen_recording_20250929_135928.mp4
  1. We should probably revisit #2925 to fix this, right?
Screen_recording_20250929_140440.mp4

@t0maboro
Copy link
Contributor Author

t0maboro commented Sep 29, 2025

@kligarski answering your questions:

  1. Imo not a regression, because as a top edge I was considering top edge of the component, the same way as we'd put detent=1.0 previously. However, we should definitely consider whether this is still valid, as now we're avoiding the keyboard. cc @kkafar if you have any thoughts on that
Screenshot 2025-09-29 at 16 08 55
  1. I created a ticket on labs repo https://github.com/software-mansion/react-native-screens-labs/issues/484, but the problem here is that I'm dependent on onProgress callback, which will cause a slight delay.

  2. For status bar, I'd prefer to continue the work in fix(Android): status bar insets for formSheet #2925 - I can take it over and adapt to this PR (until anyone has any objections of at.1 whether the status bar is a regression or not)

@t0maboro t0maboro marked this pull request as ready for review September 29, 2025 14:13
@kligarski
Copy link
Contributor

kligarski commented Sep 29, 2025

@kligarski answering your questions:

Imo not a regression, because as a top edge I was considering top edge of the component, the same way as we'd put detent=1.0 previously. However, we should definitely consider whether this is still valid, as now we're avoiding the keyboard. cc @kkafar if you have any thoughts on that

I'm not sure if we're talking about the same thing: I meant the text input being placed behind navigation bar (3-button navigation at the bottom) in first fitToContents example. I checked the main and confirmed that this is not a regression so we're good.

image

@t0maboro
Copy link
Contributor Author

@kligarski answering your questions:
Imo not a regression, because as a top edge I was considering top edge of the component, the same way as we'd put detent=1.0 previously. However, we should definitely consider whether this is still valid, as now we're avoiding the keyboard. cc @kkafar if you have any thoughts on that

I'm not sure if we're talking about the same thing: I meant the text input being placed behind navigation bar (3-button navigation at the bottom) in first fitToContents example. I checked the main and confirmed that this is not a regression so we're good.

image

Sorry, I read all together before answering and idk why I started thinking that 1 and 3 are connected. Good that we have an alignment on this one, but we should rethink whether this is expected now

Copy link
Member

@kkafar kkafar left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm reviewing only code right now. I'll check the runtime tomorrow.

val startValueCallback = { _: Number? -> screen.height.toFloat() }
val evaluator = ExternalBoundaryValuesEvaluator(startValueCallback, { 0f })

return ValueAnimator.ofObject(evaluator, screen.height.toFloat(), 0f).apply {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reading this for the first time & I don't get why on slide in animator the start value is height and target value is 0, at least passed here. I also see that there is an evaluator.

I'll update the comment if I get this

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wrote this code intitially, but I do not remember that. 😄

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh, is it because the sheet has already target position & we translate it back to the animation start position? That might be it.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The initial sheet position is when the sheet bottom edge is equal to device's bottom edge, we're translating Y with a positive value to hide the sheet under the bottom edge of the device

Comment on lines +265 to +268
val detents = screen.sheetDetents
if (detents.isEmpty()) {
throw IllegalStateException("[RNScreens] Cannot determine sheet detent - detents list is empty")
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should have separate data structure for detents, making sure of this invariant. Let's create ticket for this.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

throw IllegalStateException("[RNScreens] Cannot determine sheet detent - detents list is empty")
}

val detentValue = detents[detents.size - 1].coerceIn(0.0, 1.0)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't get this part. Why do you take value of the largest detent here?

Copy link
Member

@kkafar kkafar Nov 5, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This method is used only when there is keyboard present, is that right? If so, let's name it appropriately, cause right now it's not obvious at all.

e.g. computeSheetOffsetYWithIMEPresent or something.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

also this place deserves its own comment, because it really defines the behaviour of the sheet -> that it expands to max detent when the keyboard shows, right?

Copy link
Contributor Author

@t0maboro t0maboro Nov 5, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes, the sheet expands to its max detent; the purpose of this code is to determine whether we're able to show the full sheet or if we need to cover it partially with the keyboard, definitely deserves some description, giving the information about the final offset from bottom, to which we should animate

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@t0maboro t0maboro requested a review from kkafar November 6, 2025 09:35
@kkafar
Copy link
Member

kkafar commented Nov 6, 2025

What about this behaviour?

It goes up, even if there is no IME present? Can we detect such case?

Screen.Recording.2025-11-06.at.11.10.01.mov

@kkafar
Copy link
Member

kkafar commented Nov 6, 2025

Also this is messed up a bit. Is this particular behaviour handled in the safe area view PR?
The text input lands under the status bar

Screen.Recording.2025-11-06.at.11.12.16.mov

Copy link
Member

@kkafar kkafar left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just last code remarks.

@t0maboro
Copy link
Contributor Author

t0maboro commented Nov 6, 2025

What about this behaviour?

It goes up, even if there is no IME present? Can we detect such case?

7187586 + d354569 (cleanup)

Also this is messed up a bit. Is this particular behaviour handled in the safe area view PR?
The text input lands under the status bar

As discussed internally, let's proceed with that in the PR with FormSheet - SAV fixes

@t0maboro t0maboro requested a review from kkafar November 6, 2025 12:41
## Description

Followup for:
#3248 (comment)

Note: @kkafar I'm aware that it differs from what we discussed
internally, but imo it may make sense to do it in the way I'm proposing
here, let me know what do you think

Fixes
software-mansion/react-native-screens-labs#537

## Changes

- Extracted sheet animation code to a separate class

## Test code and steps to reproduce

Regression testing on `Test3248`

## Checklist

- [x] Included code example that can be used to test this change
- [x] Ensured that CI passes
Copy link
Member

@kkafar kkafar left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good. Great job. Thank you!

@t0maboro t0maboro merged commit e609b6f into main Nov 7, 2025
5 checks passed
@t0maboro t0maboro deleted the @t0maboro/keyboard-avoiding-formsheet-android branch November 7, 2025 13:48
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Android] Bottom sheets do not resize when keyboard is open when using fitToContents

5 participants