Skip to content

Commit 01dbb7f

Browse files
authored
fix: react-native-navigation integration (#149)
## 📜 Description Fixed an integration with `react-native-navigation` without hacks. ## 💡 Motivation and Context To overcome the initial problem I've decided to re-create callback between `onAttachedToWindow`/`onDetachFromWindow` lifecycles. It improves the situation, but at some point of time you still may inconsistent values returned by hook and actual keyboard position. For me it seems like if the `WindowInsetsAnimationCompat.Callback` is attached to any parent view of the current screen (i. e. `decorView`, `rootView`, etc.), then it can be broken after some navigation cycles. This solution was inspired by Omelyan/RNNKeyboardController-Test@d5ee7ea. If we send events through overlay, then it's never broken. I had a look on a view hierarchy and realised, that overlays are not attached to parent views or to navigation tree: <img width="672" alt="image" src="https://github.com/kirillzyusko/react-native-keyboard-controller/assets/22820318/629002ec-7b41-479e-83b0-6122e75c5827"> I got an inspiration from this idea and decided to replicate this mechanism in native code. I've decided to attach a view as a child to `@id/action_bar_root` and setup callback on this view. In this case this view will not be a parent of navigation tree and will not be inside of navigation tree: <img width="675" alt="image" src="https://github.com/kirillzyusko/react-native-keyboard-controller/assets/22820318/18fc5714-6260-4b8f-b66a-7cee7ff3bc29"> However such approach didn't work out - it seems like if current view has `onAttachedToWindow`/`onDetachedFromWindow` calls, then Keyboard insets detection in the end will be broken on API < 30. I've tried to remove this `eventView` in `onDetachedFromWindow` and re-create it again in `onAttachedToWindow` - and such combination worked out. Fixes #130 ## 📢 Changelog ### Android - setup callbacks in `onAttachedToWindow`; - add `eventView` as a child of `@id/action_bar_root` view (in `onAttachedToWindow`); - attach a callback to `eventView` rather than `EdgeToEdgeReactViewGroup` (but still send events through `EdgeToEdgeReactViewGroup` id) - added `removeSelf` to `ViewGroup` extensions; - remove `eventView` in `onDetachedFromWindow`; - change `inputMode` only if new mode is not equal to current one; ### Docs - update `README.md`; ## 🤔 How Has This Been Tested? Tested on Pixel 6 Pro (API 28), emulator. ## 📸 Screenshots (if appropriate): https://github.com/kirillzyusko/react-native-keyboard-controller/assets/22820318/923fe0b6-cdb3-4bca-92e2-44a2c7fe678d ## 📝 Checklist - [x] CI successfully passed
1 parent e5b6ead commit 01dbb7f

File tree

4 files changed

+82
-47
lines changed

4 files changed

+82
-47
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ Keyboard manager which works in identical way on both iOS and Android.
1313
- module for changing soft input mode on Android 🤔
1414
- reanimated support 🚀
1515
- interactive keyboard dismissing 👆📱
16+
- works with any navigation library 🧭
1617
- and more is coming... Stay tuned! 😊
1718

1819
## Installation
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
package com.reactnativekeyboardcontroller.extensions
2+
3+
import android.view.ViewGroup
4+
5+
fun ViewGroup?.removeSelf() {
6+
this ?: return
7+
val parent = parent as? ViewGroup ?: return
8+
9+
parent.removeView(this)
10+
}

android/src/main/java/com/reactnativekeyboardcontroller/modules/KeyboardControllerModuleImpl.kt

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,7 @@ import com.facebook.react.bridge.ReactApplicationContext
55
import com.facebook.react.bridge.UiThreadUtil
66

77
class KeyboardControllerModuleImpl(private val mReactContext: ReactApplicationContext) {
8-
private val mDefaultMode: Int = mReactContext
9-
.currentActivity
10-
?.window
11-
?.attributes
12-
?.softInputMode
13-
?: WindowManager.LayoutParams.SOFT_INPUT_STATE_UNSPECIFIED
8+
private val mDefaultMode: Int = getCurrentMode()
149

1510
fun setInputMode(mode: Int) {
1611
setSoftInputMode(mode)
@@ -22,10 +17,21 @@ class KeyboardControllerModuleImpl(private val mReactContext: ReactApplicationCo
2217

2318
private fun setSoftInputMode(mode: Int) {
2419
UiThreadUtil.runOnUiThread {
25-
mReactContext.currentActivity?.window?.setSoftInputMode(mode)
20+
if (getCurrentMode() != mode) {
21+
mReactContext.currentActivity?.window?.setSoftInputMode(mode)
22+
}
2623
}
2724
}
2825

26+
private fun getCurrentMode(): Int {
27+
return mReactContext
28+
.currentActivity
29+
?.window
30+
?.attributes
31+
?.softInputMode
32+
?: WindowManager.LayoutParams.SOFT_INPUT_STATE_UNSPECIFIED
33+
}
34+
2935
companion object {
3036
const val NAME = "KeyboardController"
3137
}

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

Lines changed: 58 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import androidx.core.view.WindowInsetsCompat
1313
import com.facebook.react.uimanager.ThemedReactContext
1414
import com.facebook.react.views.view.ReactViewGroup
1515
import com.reactnativekeyboardcontroller.KeyboardAnimationCallback
16+
import com.reactnativekeyboardcontroller.extensions.removeSelf
1617
import com.reactnativekeyboardcontroller.extensions.requestApplyInsetsWhenAttached
1718
import com.reactnativekeyboardcontroller.extensions.rootView
1819

@@ -22,36 +23,72 @@ private val TAG = EdgeToEdgeReactViewGroup::class.qualifiedName
2223
class EdgeToEdgeReactViewGroup(private val reactContext: ThemedReactContext) : ReactViewGroup(reactContext) {
2324
private var isStatusBarTranslucent = false
2425
private var isNavigationBarTranslucent = false
26+
private var eventView: ReactViewGroup? = null
2527

26-
init {
27-
val activity = reactContext.currentActivity
28+
// region View lifecycles
29+
override fun onAttachedToWindow() {
30+
super.onAttachedToWindow()
2831

29-
if (activity != null) {
30-
val callback = KeyboardAnimationCallback(
31-
view = this,
32-
persistentInsetTypes = WindowInsetsCompat.Type.systemBars(),
33-
deferredInsetTypes = WindowInsetsCompat.Type.ime(),
34-
// We explicitly allow dispatch to continue down to binding.messageHolder's
35-
// child views, so that step 2.5 below receives the call
36-
dispatchMode = WindowInsetsAnimationCompat.Callback.DISPATCH_MODE_CONTINUE_ON_SUBTREE,
37-
context = reactContext,
38-
)
39-
ViewCompat.setWindowInsetsAnimationCallback(this, callback)
40-
ViewCompat.setOnApplyWindowInsetsListener(this, callback)
41-
this.requestApplyInsetsWhenAttached()
42-
} else {
32+
val activity = reactContext.currentActivity
33+
if (activity == null) {
4334
Log.w(TAG, "Can not setup keyboard animation listener, since `currentActivity` is null")
35+
return
36+
}
37+
38+
Handler(Looper.getMainLooper()).post(this::setupWindowInsets)
39+
WindowCompat.setDecorFitsSystemWindows(
40+
activity.window,
41+
false,
42+
)
43+
44+
eventView = ReactViewGroup(context)
45+
val root = this.getContentView()
46+
root?.addView(eventView)
47+
48+
val callback = KeyboardAnimationCallback(
49+
view = this,
50+
persistentInsetTypes = WindowInsetsCompat.Type.systemBars(),
51+
deferredInsetTypes = WindowInsetsCompat.Type.ime(),
52+
dispatchMode = WindowInsetsAnimationCompat.Callback.DISPATCH_MODE_CONTINUE_ON_SUBTREE,
53+
context = reactContext,
54+
)
55+
56+
eventView?.let {
57+
ViewCompat.setWindowInsetsAnimationCallback(it, callback)
58+
ViewCompat.setOnApplyWindowInsetsListener(it, callback)
59+
it.requestApplyInsetsWhenAttached()
4460
}
4561
}
4662

63+
override fun onDetachedFromWindow() {
64+
super.onDetachedFromWindow()
65+
66+
eventView.removeSelf()
67+
}
68+
// endregion
69+
70+
// region Props setters
71+
fun setStatusBarTranslucent(isStatusBarTranslucent: Boolean) {
72+
this.isStatusBarTranslucent = isStatusBarTranslucent
73+
}
74+
75+
fun setNavigationBarTranslucent(isNavigationBarTranslucent: Boolean) {
76+
this.isNavigationBarTranslucent = isNavigationBarTranslucent
77+
}
78+
// endregion
79+
80+
// region Private functions/class helpers
81+
private fun getContentView(): FitWindowsLinearLayout? {
82+
return reactContext.currentActivity?.window?.decorView?.rootView?.findViewById(
83+
androidx.appcompat.R.id.action_bar_root,
84+
)
85+
}
86+
4787
private fun setupWindowInsets() {
4888
val rootView = reactContext.rootView
4989
if (rootView != null) {
5090
ViewCompat.setOnApplyWindowInsetsListener(rootView) { v, insets ->
51-
val content =
52-
reactContext.rootView?.findViewById<FitWindowsLinearLayout>(
53-
androidx.appcompat.R.id.action_bar_root,
54-
)
91+
val content = getContentView()
5592
val params = FrameLayout.LayoutParams(
5693
FrameLayout.LayoutParams.MATCH_PARENT,
5794
FrameLayout.LayoutParams.MATCH_PARENT,
@@ -80,24 +117,5 @@ class EdgeToEdgeReactViewGroup(private val reactContext: ThemedReactContext) : R
80117
}
81118
}
82119
}
83-
84-
override fun onAttachedToWindow() {
85-
super.onAttachedToWindow()
86-
87-
Handler(Looper.getMainLooper()).post(this::setupWindowInsets)
88-
reactContext.currentActivity?.let {
89-
WindowCompat.setDecorFitsSystemWindows(
90-
it.window,
91-
false,
92-
)
93-
}
94-
}
95-
96-
fun setStatusBarTranslucent(isStatusBarTranslucent: Boolean) {
97-
this.isStatusBarTranslucent = isStatusBarTranslucent
98-
}
99-
100-
fun setNavigationBarTranslucent(isNavigationBarTranslucent: Boolean) {
101-
this.isNavigationBarTranslucent = isNavigationBarTranslucent
102-
}
120+
// endregion
103121
}

0 commit comments

Comments
 (0)