react-native-keyboard-controller 1.7.0 → 1.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (133) hide show
  1. package/README.md +2 -0
  2. package/android/gradle.properties +1 -1
  3. package/android/src/fabric/java/com/reactnativekeyboardcontroller/KeyboardControllerViewManager.kt +5 -0
  4. package/android/src/main/java/com/reactnativekeyboardcontroller/events/FocusedInputLayoutChangedEvent.kt +41 -0
  5. package/android/src/main/java/com/reactnativekeyboardcontroller/extensions/ThemedReactContext.kt +9 -0
  6. package/android/src/main/java/com/reactnativekeyboardcontroller/extensions/View.kt +7 -0
  7. package/android/src/main/java/com/reactnativekeyboardcontroller/listeners/FocusedInputLayoutObserver.kt +93 -0
  8. package/android/src/main/java/com/reactnativekeyboardcontroller/{KeyboardAnimationCallback.kt → listeners/KeyboardAnimationCallback.kt} +86 -55
  9. package/android/src/main/java/com/reactnativekeyboardcontroller/managers/KeyboardControllerViewManagerImpl.kt +6 -0
  10. package/android/src/main/java/com/reactnativekeyboardcontroller/views/EdgeToEdgeReactViewGroup.kt +120 -50
  11. package/android/src/paper/java/com/reactnativekeyboardcontroller/KeyboardControllerViewManager.kt +5 -0
  12. package/ios/Extensions.swift +7 -0
  13. package/ios/KeyboardController.xcodeproj/project.pbxproj +41 -7
  14. package/ios/events/FocusedInputLayoutChangedEvent.h +16 -0
  15. package/ios/events/FocusedInputLayoutChangedEvent.m +75 -0
  16. package/ios/observers/FocusedInputLayoutObserver.swift +136 -0
  17. package/ios/{KeyboardControllerView.mm → views/KeyboardControllerView.mm} +54 -3
  18. package/ios/{KeyboardControllerViewManager.mm → views/KeyboardControllerViewManager.mm} +7 -0
  19. package/ios/{KeyboardControllerViewManager.swift → views/KeyboardControllerViewManager.swift} +25 -1
  20. package/jest/index.js +16 -0
  21. package/lib/commonjs/animated.js +34 -6
  22. package/lib/commonjs/animated.js.map +1 -1
  23. package/lib/commonjs/bindings.js.map +1 -1
  24. package/lib/commonjs/bindings.native.js.map +1 -1
  25. package/lib/commonjs/components/KeyboardAvoidingView/hooks.js +9 -1
  26. package/lib/commonjs/components/KeyboardAvoidingView/hooks.js.map +1 -1
  27. package/lib/commonjs/components/KeyboardAvoidingView/index.js +7 -4
  28. package/lib/commonjs/components/KeyboardAvoidingView/index.js.map +1 -1
  29. package/lib/commonjs/components/KeyboardStickyView/index.js +44 -0
  30. package/lib/commonjs/components/KeyboardStickyView/index.js.map +1 -0
  31. package/lib/commonjs/components/index.js +7 -0
  32. package/lib/commonjs/components/index.js.map +1 -1
  33. package/lib/commonjs/constants.js.map +1 -1
  34. package/lib/commonjs/context.js +9 -4
  35. package/lib/commonjs/context.js.map +1 -1
  36. package/lib/commonjs/hooks.js +16 -1
  37. package/lib/commonjs/hooks.js.map +1 -1
  38. package/lib/commonjs/index.js +8 -2
  39. package/lib/commonjs/index.js.map +1 -1
  40. package/lib/commonjs/internal.js.map +1 -1
  41. package/lib/commonjs/monkey-patch.android.js +32 -20
  42. package/lib/commonjs/monkey-patch.android.js.map +1 -1
  43. package/lib/commonjs/monkey-patch.js +11 -1
  44. package/lib/commonjs/monkey-patch.js.map +1 -1
  45. package/lib/commonjs/reanimated.js +5 -2
  46. package/lib/commonjs/reanimated.js.map +1 -1
  47. package/lib/commonjs/reanimated.native.js +18 -1
  48. package/lib/commonjs/reanimated.native.js.map +1 -1
  49. package/lib/commonjs/replicas.js +1 -1
  50. package/lib/commonjs/replicas.js.map +1 -1
  51. package/lib/commonjs/specs/KeyboardControllerViewNativeComponent.js.map +1 -1
  52. package/lib/commonjs/specs/KeyboardGestureAreaNativeComponent.js.map +1 -1
  53. package/lib/commonjs/specs/NativeKeyboardController.js.map +1 -1
  54. package/lib/commonjs/specs/NativeStatusBarManagerCompat.js.map +1 -1
  55. package/lib/commonjs/types.js.map +1 -1
  56. package/lib/module/animated.js +37 -9
  57. package/lib/module/animated.js.map +1 -1
  58. package/lib/module/bindings.js.map +1 -1
  59. package/lib/module/bindings.native.js +1 -1
  60. package/lib/module/bindings.native.js.map +1 -1
  61. package/lib/module/components/KeyboardAvoidingView/hooks.js +9 -1
  62. package/lib/module/components/KeyboardAvoidingView/hooks.js.map +1 -1
  63. package/lib/module/components/KeyboardAvoidingView/index.js +9 -6
  64. package/lib/module/components/KeyboardAvoidingView/index.js.map +1 -1
  65. package/lib/module/components/KeyboardStickyView/index.js +35 -0
  66. package/lib/module/components/KeyboardStickyView/index.js.map +1 -0
  67. package/lib/module/components/index.js +1 -0
  68. package/lib/module/components/index.js.map +1 -1
  69. package/lib/module/constants.js.map +1 -1
  70. package/lib/module/context.js +9 -4
  71. package/lib/module/context.js.map +1 -1
  72. package/lib/module/hooks.js +14 -1
  73. package/lib/module/hooks.js.map +1 -1
  74. package/lib/module/index.js +1 -2
  75. package/lib/module/index.js.map +1 -1
  76. package/lib/module/internal.js.map +1 -1
  77. package/lib/module/monkey-patch.android.js +26 -20
  78. package/lib/module/monkey-patch.android.js.map +1 -1
  79. package/lib/module/monkey-patch.js +3 -0
  80. package/lib/module/monkey-patch.js.map +1 -1
  81. package/lib/module/reanimated.js +3 -1
  82. package/lib/module/reanimated.js.map +1 -1
  83. package/lib/module/reanimated.native.js +16 -0
  84. package/lib/module/reanimated.native.js.map +1 -1
  85. package/lib/module/replicas.js +2 -2
  86. package/lib/module/replicas.js.map +1 -1
  87. package/lib/module/specs/KeyboardControllerViewNativeComponent.js.map +1 -1
  88. package/lib/module/specs/KeyboardGestureAreaNativeComponent.js.map +1 -1
  89. package/lib/module/specs/NativeKeyboardController.js.map +1 -1
  90. package/lib/module/specs/NativeStatusBarManagerCompat.js.map +1 -1
  91. package/lib/module/types.js.map +1 -1
  92. package/lib/typescript/animated.d.ts +8 -1
  93. package/lib/typescript/components/KeyboardAvoidingView/hooks.d.ts +1 -0
  94. package/lib/typescript/components/KeyboardAvoidingView/index.d.ts +2 -1
  95. package/lib/typescript/components/KeyboardStickyView/index.d.ts +20 -0
  96. package/lib/typescript/components/index.d.ts +1 -0
  97. package/lib/typescript/context.d.ts +6 -3
  98. package/lib/typescript/hooks.d.ts +9 -2
  99. package/lib/typescript/index.d.ts +1 -2
  100. package/lib/typescript/internal.d.ts +1 -1
  101. package/lib/typescript/monkey-patch.android.d.ts +2 -1
  102. package/lib/typescript/monkey-patch.d.ts +2 -0
  103. package/lib/typescript/reanimated.d.ts +2 -1
  104. package/lib/typescript/reanimated.native.d.ts +2 -1
  105. package/lib/typescript/specs/KeyboardControllerViewNativeComponent.d.ts +13 -0
  106. package/lib/typescript/types.d.ts +17 -0
  107. package/package.json +9 -28
  108. package/src/animated.tsx +53 -9
  109. package/src/bindings.native.ts +1 -1
  110. package/src/bindings.ts +3 -1
  111. package/src/components/KeyboardAvoidingView/hooks.ts +9 -1
  112. package/src/components/KeyboardAvoidingView/index.tsx +15 -13
  113. package/src/components/KeyboardStickyView/index.tsx +63 -0
  114. package/src/components/index.ts +1 -0
  115. package/src/constants.ts +2 -0
  116. package/src/context.ts +16 -5
  117. package/src/hooks.ts +16 -6
  118. package/src/index.ts +1 -3
  119. package/src/internal.ts +3 -1
  120. package/src/monkey-patch.android.ts +27 -21
  121. package/src/monkey-patch.ts +3 -0
  122. package/src/reanimated.native.ts +30 -1
  123. package/src/reanimated.ts +13 -2
  124. package/src/replicas.ts +2 -2
  125. package/src/specs/KeyboardControllerViewNativeComponent.ts +16 -1
  126. package/src/specs/KeyboardGestureAreaNativeComponent.ts +2 -1
  127. package/src/specs/NativeKeyboardController.ts +2 -1
  128. package/src/specs/NativeStatusBarManagerCompat.ts +2 -1
  129. package/src/types.ts +28 -1
  130. /package/ios/{KeyboardMoveEvent.h → events/KeyboardMoveEvent.h} +0 -0
  131. /package/ios/{KeyboardMoveEvent.m → events/KeyboardMoveEvent.m} +0 -0
  132. /package/ios/{KeyboardMovementObserver.swift → observers/KeyboardMovementObserver.swift} +0 -0
  133. /package/ios/{KeyboardControllerView.h → views/KeyboardControllerView.h} +0 -0
package/README.md CHANGED
@@ -13,6 +13,8 @@ Keyboard manager which works in identical way on both iOS and Android.
13
13
  - module for changing soft input mode on Android 🤔
14
14
  - reanimated support 🚀
15
15
  - interactive keyboard dismissing 👆📱
16
+ - prebuilt components (`KeyboardStickyView`, re-worked `KeyboardAvoidingView`) 📚
17
+ - easy focused input information retrieval 📝 🔮
16
18
  - works with any navigation library 🧭
17
19
  - and more is coming... Stay tuned! 😊
18
20
 
@@ -1,4 +1,4 @@
1
- KeyboardController_kotlinVersion=1.6.21
1
+ KeyboardController_kotlinVersion=1.8.22
2
2
  KeyboardController_compileSdkVersion=33
3
3
  KeyboardController_targetSdkVersion=33
4
4
  KeyboardController_minSdkVersion=16
@@ -37,6 +37,11 @@ class KeyboardControllerViewManager(mReactContext: ReactApplicationContext) :
37
37
  return manager.setNavigationBarTranslucent(view as EdgeToEdgeReactViewGroup, value)
38
38
  }
39
39
 
40
+ @ReactProp(name = "enabled")
41
+ override fun setEnabled(view: ReactViewGroup, value: Boolean) {
42
+ return manager.setEnabled(view as EdgeToEdgeReactViewGroup, value)
43
+ }
44
+
40
45
  override fun getExportedCustomDirectEventTypeConstants(): MutableMap<String, Any> {
41
46
  return manager.getExportedCustomDirectEventTypeConstants()
42
47
  }
@@ -0,0 +1,41 @@
1
+ package com.reactnativekeyboardcontroller.events
2
+
3
+ import com.facebook.react.bridge.Arguments
4
+ import com.facebook.react.bridge.WritableMap
5
+ import com.facebook.react.uimanager.events.Event
6
+
7
+ data class FocusedInputLayoutChangedEventData(
8
+ val x: Double,
9
+ val y: Double,
10
+ val width: Double,
11
+ val height: Double,
12
+ val absoluteX: Double,
13
+ val absoluteY: Double,
14
+ val target: Int,
15
+ )
16
+
17
+ class FocusedInputLayoutChangedEvent(
18
+ surfaceId: Int,
19
+ viewId: Int,
20
+ private val event: FocusedInputLayoutChangedEventData,
21
+ ) : Event<KeyboardTransitionEvent>(surfaceId, viewId) {
22
+ override fun getEventName() = "topFocusedInputLayoutChanged"
23
+
24
+ // All events for a given view can be coalesced
25
+ override fun getCoalescingKey(): Short = 0
26
+
27
+ override fun getEventData(): WritableMap? = Arguments.createMap().apply {
28
+ putInt("target", event.target)
29
+ putMap(
30
+ "layout",
31
+ Arguments.createMap().apply {
32
+ putDouble("x", event.x)
33
+ putDouble("y", event.y)
34
+ putDouble("width", event.width)
35
+ putDouble("height", event.height)
36
+ putDouble("absoluteX", event.absoluteX)
37
+ putDouble("absoluteY", event.absoluteY)
38
+ },
39
+ )
40
+ }
41
+ }
@@ -2,6 +2,15 @@ package com.reactnativekeyboardcontroller.extensions
2
2
 
3
3
  import android.view.View
4
4
  import com.facebook.react.uimanager.ThemedReactContext
5
+ import com.facebook.react.uimanager.UIManagerHelper
6
+ import com.facebook.react.uimanager.events.Event
7
+ import com.facebook.react.uimanager.events.EventDispatcher
5
8
 
6
9
  val ThemedReactContext.rootView: View?
7
10
  get() = this.currentActivity?.window?.decorView?.rootView
11
+
12
+ fun ThemedReactContext?.dispatchEvent(viewId: Int, event: Event<*>) {
13
+ val eventDispatcher: EventDispatcher? =
14
+ UIManagerHelper.getEventDispatcherForReactTag(this, viewId)
15
+ eventDispatcher?.dispatchEvent(event)
16
+ }
@@ -48,3 +48,10 @@ fun View.copyBoundsInWindow(rect: Rect) {
48
48
  )
49
49
  }
50
50
  }
51
+
52
+ val View.screenLocation get(): IntArray {
53
+ val point = IntArray(2)
54
+ getLocationOnScreen(point)
55
+
56
+ return point
57
+ }
@@ -0,0 +1,93 @@
1
+ package com.reactnativekeyboardcontroller.listeners
2
+
3
+ import android.view.View.OnLayoutChangeListener
4
+ import android.view.ViewTreeObserver.OnGlobalFocusChangeListener
5
+ import com.facebook.react.uimanager.ThemedReactContext
6
+ import com.facebook.react.uimanager.UIManagerHelper
7
+ import com.facebook.react.views.textinput.ReactEditText
8
+ import com.facebook.react.views.view.ReactViewGroup
9
+ import com.reactnativekeyboardcontroller.events.FocusedInputLayoutChangedEvent
10
+ import com.reactnativekeyboardcontroller.events.FocusedInputLayoutChangedEventData
11
+ import com.reactnativekeyboardcontroller.extensions.dispatchEvent
12
+ import com.reactnativekeyboardcontroller.extensions.dp
13
+ import com.reactnativekeyboardcontroller.extensions.screenLocation
14
+
15
+ val noFocusedInputEvent = FocusedInputLayoutChangedEventData(
16
+ x = 0.0,
17
+ y = 0.0,
18
+ width = 0.0,
19
+ height = 0.0,
20
+ absoluteX = 0.0,
21
+ absoluteY = 0.0,
22
+ target = -1,
23
+ )
24
+
25
+ class FocusedInputLayoutObserver(val view: ReactViewGroup, private val context: ThemedReactContext?) {
26
+ // constructor variables
27
+ private val surfaceId = UIManagerHelper.getSurfaceId(view)
28
+
29
+ // state variables
30
+ private var lastFocusedInput: ReactEditText? = null
31
+ private var lastEventDispatched: FocusedInputLayoutChangedEventData = noFocusedInputEvent
32
+
33
+ // listeners
34
+ private val layoutListener =
35
+ OnLayoutChangeListener { v, left, top, right, bottom, oldLeft, oldTop, oldRight, oldBottom ->
36
+ this.syncUpLayout()
37
+ }
38
+ private val focusListener = OnGlobalFocusChangeListener { oldFocus, newFocus ->
39
+ // unfocused or focused was changed
40
+ if (newFocus == null || oldFocus != null) {
41
+ lastFocusedInput?.removeOnLayoutChangeListener(layoutListener)
42
+ lastFocusedInput = null
43
+ }
44
+ if (newFocus is ReactEditText) {
45
+ lastFocusedInput = newFocus
46
+ newFocus.addOnLayoutChangeListener(layoutListener)
47
+ this.syncUpLayout()
48
+ }
49
+ // unfocused
50
+ if (newFocus == null) {
51
+ dispatchEventToJS(noFocusedInputEvent)
52
+ }
53
+ }
54
+
55
+ init {
56
+ view.viewTreeObserver.addOnGlobalFocusChangeListener(focusListener)
57
+ }
58
+
59
+ fun syncUpLayout() {
60
+ val input = lastFocusedInput ?: return
61
+
62
+ val (x, y) = input.screenLocation
63
+ val event = FocusedInputLayoutChangedEventData(
64
+ x = input.x.dp,
65
+ y = input.y.dp,
66
+ width = input.width.toFloat().dp,
67
+ height = input.height.toFloat().dp,
68
+ absoluteX = x.toFloat().dp,
69
+ absoluteY = y.toFloat().dp,
70
+ target = input.id,
71
+ )
72
+
73
+ dispatchEventToJS(event)
74
+ }
75
+
76
+ fun destroy() {
77
+ view.viewTreeObserver.removeOnGlobalFocusChangeListener(focusListener)
78
+ }
79
+
80
+ private fun dispatchEventToJS(event: FocusedInputLayoutChangedEventData) {
81
+ if (event != lastEventDispatched) {
82
+ lastEventDispatched = event
83
+ context.dispatchEvent(
84
+ view.id,
85
+ FocusedInputLayoutChangedEvent(
86
+ surfaceId,
87
+ view.id,
88
+ event = event,
89
+ ),
90
+ )
91
+ }
92
+ }
93
+ }
@@ -1,9 +1,10 @@
1
- package com.reactnativekeyboardcontroller
1
+ package com.reactnativekeyboardcontroller.listeners
2
2
 
3
3
  import android.animation.ValueAnimator
4
4
  import android.os.Build
5
5
  import android.util.Log
6
6
  import android.view.View
7
+ import android.view.ViewTreeObserver.OnGlobalFocusChangeListener
7
8
  import androidx.core.animation.doOnEnd
8
9
  import androidx.core.graphics.Insets
9
10
  import androidx.core.view.OnApplyWindowInsetsListener
@@ -15,11 +16,11 @@ import com.facebook.react.bridge.WritableMap
15
16
  import com.facebook.react.modules.core.DeviceEventManagerModule
16
17
  import com.facebook.react.uimanager.ThemedReactContext
17
18
  import com.facebook.react.uimanager.UIManagerHelper
18
- import com.facebook.react.uimanager.events.Event
19
- import com.facebook.react.uimanager.events.EventDispatcher
20
19
  import com.facebook.react.views.textinput.ReactEditText
21
20
  import com.facebook.react.views.view.ReactViewGroup
21
+ import com.reactnativekeyboardcontroller.InteractiveKeyboardProvider
22
22
  import com.reactnativekeyboardcontroller.events.KeyboardTransitionEvent
23
+ import com.reactnativekeyboardcontroller.extensions.dispatchEvent
23
24
  import com.reactnativekeyboardcontroller.extensions.dp
24
25
  import kotlin.math.abs
25
26
 
@@ -33,56 +34,65 @@ class KeyboardAnimationCallback(
33
34
  val context: ThemedReactContext?,
34
35
  ) : WindowInsetsAnimationCompat.Callback(dispatchMode), OnApplyWindowInsetsListener {
35
36
  private val surfaceId = UIManagerHelper.getSurfaceId(view)
37
+
38
+ // state variables
36
39
  private var persistentKeyboardHeight = 0.0
37
40
  private var isKeyboardVisible = false
38
41
  private var isTransitioning = false
39
42
  private var duration = 0
40
43
  private var viewTagFocused = -1
41
44
 
45
+ // listeners
46
+ private val focusListener = OnGlobalFocusChangeListener { oldFocus, newFocus ->
47
+ if (newFocus is ReactEditText) {
48
+ viewTagFocused = newFocus.id
49
+
50
+ // keyboard is visible and focus has been changed
51
+ if (this.isKeyboardVisible && oldFocus !== null) {
52
+ // imitate iOS behavior and send two instant start/end events containing an info about new tag
53
+ // 1. onStart/onMove/onEnd can be still dispatched after, if keyboard change size (numeric -> alphabetic type)
54
+ // 2. event should be send only when keyboard is visible, since this event arrives earlier -> `tag` will be
55
+ // 100% included in onStart/onMove/onEnd lifecycles, but triggering onStart/onEnd several time
56
+ // can bring breaking changes
57
+ context.dispatchEvent(
58
+ view.id,
59
+ KeyboardTransitionEvent(
60
+ surfaceId,
61
+ view.id,
62
+ "topKeyboardMoveStart",
63
+ this.persistentKeyboardHeight,
64
+ 1.0,
65
+ 0,
66
+ viewTagFocused,
67
+ ),
68
+ )
69
+ context.dispatchEvent(
70
+ view.id,
71
+ KeyboardTransitionEvent(
72
+ surfaceId,
73
+ view.id,
74
+ "topKeyboardMoveEnd",
75
+ this.persistentKeyboardHeight,
76
+ 1.0,
77
+ 0,
78
+ viewTagFocused,
79
+ ),
80
+ )
81
+ this.emitEvent("KeyboardController::keyboardWillShow", getEventParams(this.persistentKeyboardHeight))
82
+ this.emitEvent("KeyboardController::keyboardDidShow", getEventParams(this.persistentKeyboardHeight))
83
+ }
84
+ }
85
+ }
86
+ private var layoutObserver: FocusedInputLayoutObserver? = null
87
+
42
88
  init {
43
89
  require(persistentInsetTypes and deferredInsetTypes == 0) {
44
90
  "persistentInsetTypes and deferredInsetTypes can not contain any of " +
45
91
  " same WindowInsetsCompat.Type values"
46
92
  }
47
93
 
48
- view.viewTreeObserver.addOnGlobalFocusChangeListener { oldFocus, newFocus ->
49
- if (newFocus is ReactEditText) {
50
- viewTagFocused = newFocus.id
51
-
52
- // keyboard is visible and focus has been changed
53
- if (this.isKeyboardVisible && oldFocus !== null) {
54
- // imitate iOS behavior and send two instant start/end events containing an info about new tag
55
- // 1. onStart/onMove/onEnd can be still dispatched after, if keyboard change size (numeric -> alphabetic type)
56
- // 2. event should be send only when keyboard is visible, since this event arrives earlier -> `tag` will be
57
- // 100% included in onStart/onMove/onEnd lifecycles, but triggering onStart/onEnd several time
58
- // can bring breaking changes
59
- this.sendEventToJS(
60
- KeyboardTransitionEvent(
61
- surfaceId,
62
- view.id,
63
- "topKeyboardMoveStart",
64
- this.persistentKeyboardHeight,
65
- 1.0,
66
- 0,
67
- viewTagFocused,
68
- ),
69
- )
70
- this.sendEventToJS(
71
- KeyboardTransitionEvent(
72
- surfaceId,
73
- view.id,
74
- "topKeyboardMoveEnd",
75
- this.persistentKeyboardHeight,
76
- 1.0,
77
- 0,
78
- viewTagFocused,
79
- ),
80
- )
81
- this.emitEvent("KeyboardController::keyboardWillShow", getEventParams(this.persistentKeyboardHeight))
82
- this.emitEvent("KeyboardController::keyboardDidShow", getEventParams(this.persistentKeyboardHeight))
83
- }
84
- }
85
- }
94
+ layoutObserver = FocusedInputLayoutObserver(view = view, context = context)
95
+ view.viewTreeObserver.addOnGlobalFocusChangeListener(focusListener)
86
96
  }
87
97
 
88
98
  /**
@@ -115,8 +125,10 @@ class KeyboardAnimationCallback(
115
125
  val keyboardHeight = getCurrentKeyboardHeight()
116
126
  val duration = DEFAULT_ANIMATION_TIME.toInt()
117
127
 
128
+ layoutObserver?.syncUpLayout()
118
129
  this.emitEvent("KeyboardController::keyboardWillShow", getEventParams(keyboardHeight))
119
- this.sendEventToJS(
130
+ context.dispatchEvent(
131
+ view.id,
120
132
  KeyboardTransitionEvent(
121
133
  surfaceId,
122
134
  view.id,
@@ -128,10 +140,12 @@ class KeyboardAnimationCallback(
128
140
  ),
129
141
  )
130
142
 
131
- val animation = ValueAnimator.ofFloat(this.persistentKeyboardHeight.toFloat(), keyboardHeight.toFloat())
143
+ val animation =
144
+ ValueAnimator.ofFloat(this.persistentKeyboardHeight.toFloat(), keyboardHeight.toFloat())
132
145
  animation.addUpdateListener { animator ->
133
146
  val toValue = animator.animatedValue as Float
134
- this.sendEventToJS(
147
+ context.dispatchEvent(
148
+ view.id,
135
149
  KeyboardTransitionEvent(
136
150
  surfaceId,
137
151
  view.id,
@@ -145,7 +159,8 @@ class KeyboardAnimationCallback(
145
159
  }
146
160
  animation.doOnEnd {
147
161
  this.emitEvent("KeyboardController::keyboardDidShow", getEventParams(keyboardHeight))
148
- this.sendEventToJS(
162
+ context.dispatchEvent(
163
+ view.id,
149
164
  KeyboardTransitionEvent(
150
165
  surfaceId,
151
166
  view.id,
@@ -180,13 +195,15 @@ class KeyboardAnimationCallback(
180
195
  this.persistentKeyboardHeight = keyboardHeight
181
196
  }
182
197
 
198
+ layoutObserver?.syncUpLayout()
183
199
  this.emitEvent(
184
200
  "KeyboardController::" + if (!isKeyboardVisible) "keyboardWillHide" else "keyboardWillShow",
185
201
  getEventParams(keyboardHeight),
186
202
  )
187
203
 
188
204
  Log.i(TAG, "HEIGHT:: $keyboardHeight TAG:: $viewTagFocused")
189
- this.sendEventToJS(
205
+ context.dispatchEvent(
206
+ view.id,
190
207
  KeyboardTransitionEvent(
191
208
  surfaceId,
192
209
  view.id,
@@ -227,10 +244,24 @@ class KeyboardAnimationCallback(
227
244
  // do nothing, just log an exception send progress as 0
228
245
  Log.w(TAG, "Caught arithmetic exception during `progress` calculation: $e")
229
246
  }
230
- Log.i(TAG, "DiffY: $diffY $height $progress ${InteractiveKeyboardProvider.isInteractive} $viewTagFocused")
247
+ Log.i(
248
+ TAG,
249
+ "DiffY: $diffY $height $progress ${InteractiveKeyboardProvider.isInteractive} $viewTagFocused",
250
+ )
231
251
 
232
252
  val event = if (InteractiveKeyboardProvider.isInteractive) "topKeyboardMoveInteractive" else "topKeyboardMove"
233
- this.sendEventToJS(KeyboardTransitionEvent(surfaceId, view.id, event, height, progress, duration, viewTagFocused))
253
+ context.dispatchEvent(
254
+ view.id,
255
+ KeyboardTransitionEvent(
256
+ surfaceId,
257
+ view.id,
258
+ event,
259
+ height,
260
+ progress,
261
+ duration,
262
+ viewTagFocused,
263
+ ),
264
+ )
234
265
 
235
266
  return insets
236
267
  }
@@ -259,7 +290,8 @@ class KeyboardAnimationCallback(
259
290
  "KeyboardController::" + if (!isKeyboardVisible) "keyboardDidHide" else "keyboardDidShow",
260
291
  getEventParams(keyboardHeight),
261
292
  )
262
- this.sendEventToJS(
293
+ context.dispatchEvent(
294
+ view.id,
263
295
  KeyboardTransitionEvent(
264
296
  surfaceId,
265
297
  view.id,
@@ -275,6 +307,11 @@ class KeyboardAnimationCallback(
275
307
  duration = 0
276
308
  }
277
309
 
310
+ fun destroy() {
311
+ view.viewTreeObserver.removeOnGlobalFocusChangeListener(focusListener)
312
+ layoutObserver?.destroy()
313
+ }
314
+
278
315
  private fun isKeyboardVisible(): Boolean {
279
316
  val insets = ViewCompat.getRootWindowInsets(view)
280
317
 
@@ -290,12 +327,6 @@ class KeyboardAnimationCallback(
290
327
  return (keyboardHeight - navigationBar).toFloat().dp.coerceAtLeast(0.0)
291
328
  }
292
329
 
293
- private fun sendEventToJS(event: Event<*>) {
294
- val eventDispatcher: EventDispatcher? =
295
- UIManagerHelper.getEventDispatcherForReactTag(context, view.id)
296
- eventDispatcher?.dispatchEvent(event)
297
- }
298
-
299
330
  private fun emitEvent(event: String, params: WritableMap) {
300
331
  context?.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter::class.java)?.emit(event, params)
301
332
 
@@ -11,6 +11,10 @@ class KeyboardControllerViewManagerImpl(mReactContext: ReactApplicationContext)
11
11
  return EdgeToEdgeReactViewGroup(reactContext)
12
12
  }
13
13
 
14
+ fun setEnabled(view: EdgeToEdgeReactViewGroup, enabled: Boolean) {
15
+ view.setActive(enabled)
16
+ }
17
+
14
18
  fun setStatusBarTranslucent(view: EdgeToEdgeReactViewGroup, isStatusBarTranslucent: Boolean) {
15
19
  view.setStatusBarTranslucent(isStatusBarTranslucent)
16
20
  }
@@ -29,6 +33,8 @@ class KeyboardControllerViewManagerImpl(mReactContext: ReactApplicationContext)
29
33
  MapBuilder.of("registrationName", "onKeyboardMoveEnd"),
30
34
  "topKeyboardMoveInteractive",
31
35
  MapBuilder.of("registrationName", "onKeyboardMoveInteractive"),
36
+ "topFocusedInputLayoutChanged",
37
+ MapBuilder.of("registrationName", "onFocusedInputLayoutChanged"),
32
38
  )
33
39
 
34
40
  return map
@@ -12,78 +12,47 @@ import androidx.core.view.WindowInsetsAnimationCompat
12
12
  import androidx.core.view.WindowInsetsCompat
13
13
  import com.facebook.react.uimanager.ThemedReactContext
14
14
  import com.facebook.react.views.view.ReactViewGroup
15
- import com.reactnativekeyboardcontroller.KeyboardAnimationCallback
16
15
  import com.reactnativekeyboardcontroller.extensions.removeSelf
17
16
  import com.reactnativekeyboardcontroller.extensions.requestApplyInsetsWhenAttached
18
17
  import com.reactnativekeyboardcontroller.extensions.rootView
18
+ import com.reactnativekeyboardcontroller.listeners.KeyboardAnimationCallback
19
19
 
20
20
  private val TAG = EdgeToEdgeReactViewGroup::class.qualifiedName
21
21
 
22
+ @Suppress("detekt:TooManyFunctions")
22
23
  @SuppressLint("ViewConstructor")
23
24
  class EdgeToEdgeReactViewGroup(private val reactContext: ThemedReactContext) : ReactViewGroup(reactContext) {
25
+ // props
24
26
  private var isStatusBarTranslucent = false
25
27
  private var isNavigationBarTranslucent = false
28
+ private var active = false
29
+
30
+ // internal class members
26
31
  private var eventView: ReactViewGroup? = null
32
+ private var wasMounted = false
33
+ private var callback: KeyboardAnimationCallback? = null
27
34
 
28
35
  // region View lifecycles
29
36
  override fun onAttachedToWindow() {
30
37
  super.onAttachedToWindow()
31
38
 
32
- val activity = reactContext.currentActivity
33
- if (activity == null) {
34
- Log.w(TAG, "Can not setup keyboard animation listener, since `currentActivity` is null")
39
+ if (!wasMounted) {
40
+ // skip logic with callback re-creation if it was first render/mount
41
+ wasMounted = true
35
42
  return
36
43
  }
37
44
 
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()
60
- }
45
+ this.setupKeyboardCallbacks()
61
46
  }
62
47
 
63
48
  override fun onDetachedFromWindow() {
64
49
  super.onDetachedFromWindow()
65
50
 
66
- eventView.removeSelf()
51
+ this.removeKeyboardCallbacks()
67
52
  }
68
53
  // endregion
69
54
 
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
-
55
+ // region State manager helpers
87
56
  private fun setupWindowInsets() {
88
57
  val rootView = reactContext.rootView
89
58
  if (rootView != null) {
@@ -94,16 +63,20 @@ class EdgeToEdgeReactViewGroup(private val reactContext: ThemedReactContext) : R
94
63
  FrameLayout.LayoutParams.MATCH_PARENT,
95
64
  )
96
65
 
66
+ val shouldApplyZeroPaddingTop = !active || this.isStatusBarTranslucent
67
+ val shouldApplyZeroPaddingBottom = !active || this.isNavigationBarTranslucent
97
68
  params.setMargins(
98
69
  0,
99
- if (this.isStatusBarTranslucent) {
70
+ if (shouldApplyZeroPaddingTop) {
100
71
  0
101
72
  } else {
102
- insets?.getInsets(WindowInsetsCompat.Type.systemBars())?.top
103
- ?: 0
73
+ (
74
+ insets?.getInsets(WindowInsetsCompat.Type.systemBars())?.top
75
+ ?: 0
76
+ )
104
77
  },
105
78
  0,
106
- if (this.isNavigationBarTranslucent) {
79
+ if (shouldApplyZeroPaddingBottom) {
107
80
  0
108
81
  } else {
109
82
  insets?.getInsets(WindowInsetsCompat.Type.navigationBars())?.bottom
@@ -113,9 +86,106 @@ class EdgeToEdgeReactViewGroup(private val reactContext: ThemedReactContext) : R
113
86
 
114
87
  content?.layoutParams = params
115
88
 
116
- insets
89
+ val defaultInsets = ViewCompat.onApplyWindowInsets(v, insets)
90
+
91
+ defaultInsets.replaceSystemWindowInsets(
92
+ defaultInsets.systemWindowInsetLeft,
93
+ if (this.isStatusBarTranslucent) 0 else defaultInsets.systemWindowInsetTop,
94
+ defaultInsets.systemWindowInsetRight,
95
+ defaultInsets.systemWindowInsetBottom,
96
+ )
117
97
  }
118
98
  }
119
99
  }
100
+
101
+ private fun goToEdgeToEdge(edgeToEdge: Boolean) {
102
+ reactContext.currentActivity?.let {
103
+ WindowCompat.setDecorFitsSystemWindows(
104
+ it.window,
105
+ !edgeToEdge,
106
+ )
107
+ }
108
+ }
109
+
110
+ private fun setupKeyboardCallbacks() {
111
+ val activity = reactContext.currentActivity
112
+
113
+ if (activity != null) {
114
+ eventView = ReactViewGroup(context)
115
+ val root = this.getContentView()
116
+ root?.addView(eventView)
117
+
118
+ callback = KeyboardAnimationCallback(
119
+ view = this,
120
+ persistentInsetTypes = WindowInsetsCompat.Type.systemBars(),
121
+ deferredInsetTypes = WindowInsetsCompat.Type.ime(),
122
+ dispatchMode = WindowInsetsAnimationCompat.Callback.DISPATCH_MODE_CONTINUE_ON_SUBTREE,
123
+ context = reactContext,
124
+ )
125
+
126
+ eventView?.let {
127
+ ViewCompat.setWindowInsetsAnimationCallback(it, callback)
128
+ ViewCompat.setOnApplyWindowInsetsListener(it, callback)
129
+ it.requestApplyInsetsWhenAttached()
130
+ }
131
+ } else {
132
+ Log.w(TAG, "Can not setup keyboard animation listener, since `currentActivity` is null")
133
+ }
134
+ }
135
+
136
+ private fun removeKeyboardCallbacks() {
137
+ callback?.destroy()
138
+
139
+ // capture view into closure, because if `onDetachedFromWindow` and `onAttachedToWindow`
140
+ // dispatched synchronously after each other (open application on Fabric), then `.post`
141
+ // will destroy just newly created view (if we have a reference via `this`)
142
+ // and we'll have a memory leak or zombie-view
143
+ val view = eventView
144
+ // we need to remove view asynchronously from `onDetachedFromWindow` method
145
+ // otherwise we may face NPE when app is getting opened via universal link
146
+ // see https://github.com/kirillzyusko/react-native-keyboard-controller/issues/242
147
+ // for more details
148
+ Handler(Looper.getMainLooper()).post { view.removeSelf() }
149
+ }
150
+
151
+ private fun getContentView(): FitWindowsLinearLayout? {
152
+ return reactContext.currentActivity?.window?.decorView?.rootView?.findViewById(
153
+ androidx.appcompat.R.id.action_bar_root,
154
+ )
155
+ }
156
+ // endregion
157
+
158
+ // region State managers
159
+ private fun enable() {
160
+ this.goToEdgeToEdge(true)
161
+ this.setupWindowInsets()
162
+ this.setupKeyboardCallbacks()
163
+ }
164
+
165
+ private fun disable() {
166
+ this.goToEdgeToEdge(false)
167
+ this.setupWindowInsets()
168
+ this.removeKeyboardCallbacks()
169
+ }
170
+ // endregion
171
+
172
+ // region Props setters
173
+ fun setStatusBarTranslucent(isStatusBarTranslucent: Boolean) {
174
+ this.isStatusBarTranslucent = isStatusBarTranslucent
175
+ }
176
+
177
+ fun setNavigationBarTranslucent(isNavigationBarTranslucent: Boolean) {
178
+ this.isNavigationBarTranslucent = isNavigationBarTranslucent
179
+ }
180
+
181
+ fun setActive(active: Boolean) {
182
+ this.active = active
183
+
184
+ if (active) {
185
+ this.enable()
186
+ } else {
187
+ this.disable()
188
+ }
189
+ }
120
190
  // endregion
121
191
  }