Skip to content

Commit 0a97fd6

Browse files
authored
fix: double keyboard height (#1131)
## 📜 Description Fix the issue with double height when `react-native-keyboard-controller` and `react-native-unistyles` used together. ## 💡 Motivation and Context It turns out that `replaceSystemWindowInsets` method is deprecated on Android. In fact it also overwrite Android behavior. Starting from API 30+ all insets are divided into categories: nav bar insets, status bar insets, keyboard insets etc. When we call `replaceSystemWindowInsets` we may mess up `systemBar` insets. How? As per Android specification: > The status bar, caption bar, and navigation bar are called the system bars. However when we call `replaceSystemWindowInsets` and keyboard is currently visible (and keyboard is treated as system window inset) then we force-include keyboard insets in `systemBar`. When `react-native-unistyles` read system insets they get `keyboard` inset instead of `bottom-safe-area`. I tried to change a code, but if you even explicitly query `Type().navigationBars()` then keyboard height still included 🤯 > [!IMPORTANT] > The code with `replaceSystemWindowInsets` was added intentionally. Without this code we have a jump that was described in #1013 (comment) So to sum up - `replaceSystemWindowInsets` interfere with `systemBar` insets (keyboard height becomes included). Initial fix was attempted to make in 5f7d16e (we can not use only `navBarInsets.bottom` because it will be `48` when edge-to-edge mode gets disabled, so whole screen container gets pushed up and we have empty view in bottom of the screen). In this case we limit system inset to be only bottom tab bar when our view is active and we pass whole system inset when view is disabled (so that Android handles everything correctly/fallbacks to its default behavior). However then I realized, that e2e tests are failing (basically when keyboard disappear on Android 28 we don't receive onEnd event or receive it with coordinates as keyboard would be open). I thought that it stems from the fact that we are using a deprecated API so I re-worked code in 02f76f3 - it uses modern API, but the fix is very similar to what we had before: - first of all we don't overwrite bottom `systemBars` insets with system values 👍 - we need to call `ViewCompat.onApplyWindowInsets(v, adjustedInsets)` (it will prevent fake nav bar apdding from being added) - we need to replicate the fix when module is disabled - in this case pass system insets so system will push the content up However after testing new approach I also realized that e2e tests fails 😡 Turns out the internal implementation of `onApplyWindowInsets` in compat layer is different, and the easiest fix is to add conditional code, that will run old code for API < 30 and will run new code for APIs > 30. Since the new code was getting bigger and bigger I decided to move it into separate extension where I'm managing all these conditions. That code is complicated, but this is the only one reliable way I found to fix client project + not break example app 😓 ## 📢 Changelog <!-- High level overview of important changes --> <!-- For example: fixed status bar manipulation; added new types declarations; --> <!-- If your changes don't affect one of platform/language below - then remove this platform/language --> ### Android - created `replaceStatusBarInsets` extension; - use `replaceStatusBarInsets` extension in `EdgeToEdge` view. ## 🤔 How Has This Been Tested? Tested in client project on: API 29, API 30, API 31, API 33. Tested example project on API 28 (real device), API 33, API 36 (real device). ## 📸 Screenshots (if appropriate): ### Android 16 (Pixel 7 Pro) |Before|After| |------|------| |<video src="https://github.com/user-attachments/assets/ed76af1b-ac64-4f67-836c-b441ea173423">|<video src="https://github.com/user-attachments/assets/37b17239-6edb-41a2-94f4-8e08e347317c">| ### Android 12 (Pixel 6 Pro, API 31) |Before|After| |------|------| |<video src="https://github.com/user-attachments/assets/e232b66c-3cb6-46c0-9ef6-3baf0aa0c44e">|<video src="https://github.com/user-attachments/assets/1d336bf7-d9f1-4e35-96a1-d3b9729f04bd">| ## 📝 Checklist - [x] CI successfully passed - [x] I added new mocks and corresponding unit-tests if library API was changed
1 parent ad76c6a commit 0a97fd6

3 files changed

Lines changed: 44 additions & 9 deletions

File tree

.github/workflows/android-e2e-test.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,7 @@ jobs:
8080
e2e-test:
8181
name: ⚙️ Automated test cases
8282
runs-on: ubuntu-latest
83-
timeout-minutes: 60
83+
timeout-minutes: 90
8484
env:
8585
WORKING_DIRECTORY: example
8686
concurrency:

android/src/main/java/com/reactnativekeyboardcontroller/extensions/View.kt

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,9 @@ import android.annotation.SuppressLint
44
import android.graphics.Rect
55
import android.os.Build
66
import android.view.View
7+
import androidx.core.graphics.Insets
8+
import androidx.core.view.ViewCompat
9+
import androidx.core.view.WindowInsetsCompat
710
import com.reactnativekeyboardcontroller.log.Logger
811

912
/**
@@ -58,3 +61,41 @@ val View.screenLocation get(): IntArray {
5861

5962
return point
6063
}
64+
65+
/**
66+
* Safely replaces status bar insets so that when we edge-to-edge mode gets disabled/enabled
67+
* the app content is not jumping/resizing a window.
68+
* */
69+
@Suppress("DEPRECATION")
70+
fun View.replaceStatusBarInsets(
71+
insets: WindowInsetsCompat,
72+
isStatusBarTranslucent: Boolean,
73+
active: Boolean,
74+
): WindowInsetsCompat {
75+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
76+
val sysBars = insets.getInsets(WindowInsetsCompat.Type.systemBars())
77+
val navBars = insets.getInsets(WindowInsetsCompat.Type.navigationBars())
78+
val ime = insets.getInsets(WindowInsetsCompat.Type.ime())
79+
val adjustedTop = if (isStatusBarTranslucent) 0 else sysBars.top
80+
// pick bottom: use IME if present, otherwise nav bar bottom (respect translucency)
81+
val bottomFromImeOrNav = if (ime.bottom > 0) ime.bottom else navBars.bottom
82+
val adjustedInsets =
83+
WindowInsetsCompat
84+
.Builder(insets)
85+
.setInsets(
86+
WindowInsetsCompat.Type.systemBars(),
87+
Insets.of(sysBars.left, adjustedTop, sysBars.right, if (active) sysBars.bottom else bottomFromImeOrNav),
88+
).build()
89+
90+
return ViewCompat.onApplyWindowInsets(this, adjustedInsets)
91+
} else {
92+
val defaultInsets = ViewCompat.onApplyWindowInsets(this, insets)
93+
94+
return defaultInsets.replaceSystemWindowInsets(
95+
defaultInsets.systemWindowInsetLeft,
96+
if (isStatusBarTranslucent) 0 else defaultInsets.systemWindowInsetTop,
97+
defaultInsets.systemWindowInsetRight,
98+
defaultInsets.systemWindowInsetBottom,
99+
)
100+
}
101+
}

android/src/main/java/com/reactnativekeyboardcontroller/views/EdgeToEdgeReactViewGroup.kt

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import com.facebook.react.uimanager.ThemedReactContext
1414
import com.facebook.react.views.view.ReactViewGroup
1515
import com.reactnativekeyboardcontroller.extensions.content
1616
import com.reactnativekeyboardcontroller.extensions.removeSelf
17+
import com.reactnativekeyboardcontroller.extensions.replaceStatusBarInsets
1718
import com.reactnativekeyboardcontroller.extensions.requestApplyInsetsWhenAttached
1819
import com.reactnativekeyboardcontroller.extensions.rootView
1920
import com.reactnativekeyboardcontroller.listeners.KeyboardAnimationCallback
@@ -130,14 +131,7 @@ class EdgeToEdgeReactViewGroup(
130131
)
131132
content?.layoutParams = params
132133

133-
val defaultInsets = ViewCompat.onApplyWindowInsets(v, insets)
134-
135-
defaultInsets.replaceSystemWindowInsets(
136-
defaultInsets.systemWindowInsetLeft,
137-
if (this.isStatusBarTranslucent) 0 else defaultInsets.systemWindowInsetTop,
138-
defaultInsets.systemWindowInsetRight,
139-
defaultInsets.systemWindowInsetBottom,
140-
)
134+
v.replaceStatusBarInsets(insets, this.isStatusBarTranslucent, active)
141135
}
142136
}
143137
}

0 commit comments

Comments
 (0)