Skip to content

Commit e979dbf

Browse files
authored
fix: don't modify edge-to-edge mode by default when module toggled on/off (#1412)
## 📜 Description Don't hook up in insets calculation since it infers with many other libraries (such as `react-native-screens`, `react-native-safe-area-context` and other native libs such as [RevenueCat paywalls](https://www.revenuecat.com/docs/tools/paywalls/displaying-paywalls)). ## 💡 Motivation and Context Original code has been introduced in #227 The idea was simple - to use this library we need: - enable edge-to-edge; - set keyboard callbacks; So when we turn off functionality we need: - disable edge-to-edge; - remove keyboard callbacks. However there was one problem - when we were disabling edge-to-edge we saw a visual jump: https://github.com/user-attachments/assets/f4880cce-14b7-495e-ac9f-25e39a80650a That was caused by `@react-navigation/stack` + `react-native-safe-area-context`. The `react-native-safe-area-context` was emitting "wrong" events (we expected `0` insets): <img width="161" height="28" alt="Image" src="https://github.com/user-attachments/assets/7598f932-503e-4e89-ba24-b2c1458e821d" /> This is why I added a code with nulling insets. However it opens new issues like #1292 or #1013 In this PR I'm revisiting the implementation and I do one small trick: now we will not toggle `edge-to-edge` mode (in Android 16 it's enabled by default anyway without ability to turn it off) - instead we'll apply bottom padding/margin when keyboard appears (only when module turned off and input mode=resize, since resize + edge-to-edge acts as a adjustNothing). Yes, this is effectively a simulation of pre-edge-to-edge behavior and this simulation may not work everywhere perfectly (for example Android 16 would act differently before, since edge-to-edge is not disableable and even if you turn off the mode the keyboard wouldn't resize the window - with this solution it will, but at the same it bring consistency and reduces API fragmentation which is also good). I don't think this is a breaking change, so it can be published as a patch release 🤞 Closes #1292 #1013 Possibly #1179 #1192 ## 📢 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 - removed `replaceStatusBarInsets`; - created extension for `windowSoftInputMode`; - use `windowSoftInputMode` extension in `KeyboardControllerModuleImpl` file; - don't `replaceStatusBarInsets` in global `setOnApplyWindowInsetsListener(rootView)`; - don't disable `edge-to-edge` mode; - apply bottom padding if module disabled and `ADJUST_RESIZE` mode is active and `preserveEdgeToEdge` is `false`; ## 🤔 How Has This Been Tested? Tested manually on Pixel 7 Pro (API 36). ## 📸 Screenshots (if appropriate): |Before|After| |-------|-----| |<img width="1080" height="2400" alt="image" src="https://github.com/user-attachments/assets/e09b0d80-8eac-47e3-a36a-8cf92ac9bde1" />|<img width="1080" height="2400" alt="image" src="https://github.com/user-attachments/assets/f1f2937f-bdec-4f74-86f1-b3f256a5655c" />| ## 📝 Checklist - [x] CI successfully passed - [x] I added new mocks and corresponding unit-tests if library API was changed
1 parent 86d8e27 commit e979dbf

4 files changed

Lines changed: 32 additions & 73 deletions

File tree

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

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package com.reactnativekeyboardcontroller.extensions
22

33
import android.view.View
44
import android.view.ViewGroup
5+
import android.view.WindowManager
56
import com.facebook.react.bridge.ReactContext
67
import com.facebook.react.uimanager.UIManagerHelper
78
import com.facebook.react.uimanager.common.UIManagerType
@@ -28,3 +29,12 @@ val ReactContext.content: ViewGroup?
2829
this.currentActivity?.window?.decorView?.rootView?.findViewById(
2930
androidx.appcompat.R.id.action_bar_root,
3031
)
32+
33+
val ReactContext.windowSoftInputMode: Int
34+
get() =
35+
this
36+
.currentActivity
37+
?.window
38+
?.attributes
39+
?.softInputMode
40+
?: WindowManager.LayoutParams.SOFT_INPUT_STATE_UNSPECIFIED

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

Lines changed: 0 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,7 @@ 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
87
import androidx.core.view.ViewCompat
9-
import androidx.core.view.WindowInsetsCompat
108
import com.reactnativekeyboardcontroller.log.Logger
119

1210
/**
@@ -61,41 +59,3 @@ val View.screenLocation get(): IntArray {
6159

6260
return point
6361
}
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/modules/KeyboardControllerModuleImpl.kt

Lines changed: 3 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ package com.reactnativekeyboardcontroller.modules
33
import android.content.Context
44
import android.os.Build
55
import android.view.View
6-
import android.view.WindowManager
76
import android.view.inputmethod.InputMethodManager
87
import com.facebook.react.bridge.Arguments
98
import com.facebook.react.bridge.Promise
@@ -12,6 +11,7 @@ import com.facebook.react.bridge.UiThreadUtil
1211
import com.reactnativekeyboardcontroller.extensions.dp
1312
import com.reactnativekeyboardcontroller.extensions.screenLocation
1413
import com.reactnativekeyboardcontroller.extensions.uiManager
14+
import com.reactnativekeyboardcontroller.extensions.windowSoftInputMode
1515
import com.reactnativekeyboardcontroller.interactive.KeyboardAnimationController
1616
import com.reactnativekeyboardcontroller.traversal.FocusedInputHolder
1717
import com.reactnativekeyboardcontroller.traversal.ViewHierarchyNavigator
@@ -21,7 +21,7 @@ class KeyboardControllerModuleImpl(
2121
) {
2222
private val uiManager = mReactContext.uiManager
2323
private val controller = KeyboardAnimationController()
24-
private val mDefaultMode: Int = getCurrentMode()
24+
private val mDefaultMode: Int = mReactContext.windowSoftInputMode
2525

2626
// region Module methods
2727
fun setInputMode(mode: Int) {
@@ -108,19 +108,11 @@ class KeyboardControllerModuleImpl(
108108
// region Helpers
109109
private fun setSoftInputMode(mode: Int) {
110110
UiThreadUtil.runOnUiThread {
111-
if (getCurrentMode() != mode) {
111+
if (mReactContext.windowSoftInputMode != mode) {
112112
mReactContext.currentActivity?.window?.setSoftInputMode(mode)
113113
}
114114
}
115115
}
116-
117-
private fun getCurrentMode(): Int =
118-
mReactContext
119-
.currentActivity
120-
?.window
121-
?.attributes
122-
?.softInputMode
123-
?: WindowManager.LayoutParams.SOFT_INPUT_STATE_UNSPECIFIED
124116
// endregion
125117

126118
// region Module constants

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

Lines changed: 19 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import android.content.res.Configuration
55
import android.os.Handler
66
import android.os.Looper
77
import android.view.WindowManager
8+
import android.view.WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE
89
import android.widget.FrameLayout
910
import androidx.core.view.ViewCompat
1011
import androidx.core.view.WindowCompat
@@ -14,14 +15,15 @@ import com.facebook.react.uimanager.ThemedReactContext
1415
import com.facebook.react.views.view.ReactViewGroup
1516
import com.reactnativekeyboardcontroller.extensions.content
1617
import com.reactnativekeyboardcontroller.extensions.removeSelf
17-
import com.reactnativekeyboardcontroller.extensions.replaceStatusBarInsets
1818
import com.reactnativekeyboardcontroller.extensions.requestApplyInsetsWhenAttached
1919
import com.reactnativekeyboardcontroller.extensions.rootView
20+
import com.reactnativekeyboardcontroller.extensions.windowSoftInputMode
2021
import com.reactnativekeyboardcontroller.listeners.KeyboardAnimationCallback
2122
import com.reactnativekeyboardcontroller.listeners.KeyboardAnimationCallbackConfig
2223
import com.reactnativekeyboardcontroller.log.Logger
2324
import com.reactnativekeyboardcontroller.modal.ModalAttachedWatcher
2425
import java.lang.ref.WeakReference
26+
import kotlin.math.max
2527

2628
private val TAG = EdgeToEdgeReactViewGroup::class.qualifiedName
2729

@@ -44,7 +46,6 @@ class EdgeToEdgeReactViewGroup(
4446
private var isStatusBarTranslucent = false
4547
private var isNavigationBarTranslucent = false
4648
private var isPreservingEdgeToEdge = false
47-
private var isEdgeToEdge = false
4849
var active: Boolean = false
4950
set(value) {
5051
field = value
@@ -110,47 +111,43 @@ class EdgeToEdgeReactViewGroup(
110111
FrameLayout.LayoutParams.MATCH_PARENT,
111112
)
112113

113-
val shouldApplyZeroPaddingTop = !active || this.isStatusBarTranslucent
114-
val shouldApplyZeroPaddingBottom = !active || this.isNavigationBarTranslucent
114+
val shouldApplyBottomPadding =
115+
!active && reactContext.windowSoftInputMode == SOFT_INPUT_ADJUST_RESIZE && !isPreservingEdgeToEdge
115116
val navBarInsets = insets.getInsets(WindowInsetsCompat.Type.navigationBars())
116117
val systemBarInsets = insets.getInsets(WindowInsetsCompat.Type.systemBars())
118+
val keyboardInsets =
119+
if (!shouldApplyBottomPadding) 0 else insets.getInsets(WindowInsetsCompat.Type.ime()).bottom
117120

118121
params.setMargins(
119122
navBarInsets.left,
120-
if (shouldApplyZeroPaddingTop) {
123+
if (this.isStatusBarTranslucent) {
121124
0
122125
} else {
123126
systemBarInsets.top
124127
},
125128
navBarInsets.right,
126-
if (shouldApplyZeroPaddingBottom) {
127-
0
129+
if (this.isNavigationBarTranslucent) {
130+
keyboardInsets
128131
} else {
129-
navBarInsets.bottom
132+
max(navBarInsets.bottom, keyboardInsets)
130133
},
131134
)
132135
content?.layoutParams = params
133136

134-
v.replaceStatusBarInsets(insets, this.isStatusBarTranslucent, active)
137+
ViewCompat.onApplyWindowInsets(v, insets)
135138
}
136139
}
137140
}
138141

139142
fun setEdgeToEdge() {
140-
val nextValue = active || isPreservingEdgeToEdge
141-
142-
if (isEdgeToEdge != nextValue) {
143-
isEdgeToEdge = nextValue
144-
145-
reactContext.currentActivity?.let {
146-
WindowCompat.setDecorFitsSystemWindows(
147-
it.window,
148-
!isEdgeToEdge,
149-
)
150-
}
151-
// unclear legacy flag if it was set earlier
152-
reactContext.currentActivity?.window?.clearFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN)
143+
reactContext.currentActivity?.let {
144+
WindowCompat.setDecorFitsSystemWindows(
145+
it.window,
146+
false,
147+
)
153148
}
149+
// unclear legacy flag if it was set earlier
150+
reactContext.currentActivity?.window?.clearFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN)
154151
}
155152

156153
private fun setupKeyboardCallbacks() {

0 commit comments

Comments
 (0)