react-native-keyboard-controller 1.8.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 (114) hide show
  1. package/README.md +2 -1
  2. package/android/gradle.properties +1 -1
  3. package/android/src/main/java/com/reactnativekeyboardcontroller/events/FocusedInputLayoutChangedEvent.kt +41 -0
  4. package/android/src/main/java/com/reactnativekeyboardcontroller/extensions/ThemedReactContext.kt +9 -0
  5. package/android/src/main/java/com/reactnativekeyboardcontroller/extensions/View.kt +7 -0
  6. package/android/src/main/java/com/reactnativekeyboardcontroller/listeners/FocusedInputLayoutObserver.kt +93 -0
  7. package/android/src/main/java/com/reactnativekeyboardcontroller/{KeyboardAnimationCallback.kt → listeners/KeyboardAnimationCallback.kt} +86 -55
  8. package/android/src/main/java/com/reactnativekeyboardcontroller/managers/KeyboardControllerViewManagerImpl.kt +2 -0
  9. package/android/src/main/java/com/reactnativekeyboardcontroller/views/EdgeToEdgeReactViewGroup.kt +16 -8
  10. package/ios/Extensions.swift +7 -0
  11. package/ios/KeyboardController.xcodeproj/project.pbxproj +41 -7
  12. package/ios/events/FocusedInputLayoutChangedEvent.h +16 -0
  13. package/ios/events/FocusedInputLayoutChangedEvent.m +75 -0
  14. package/ios/observers/FocusedInputLayoutObserver.swift +136 -0
  15. package/ios/{KeyboardControllerView.mm → views/KeyboardControllerView.mm} +40 -4
  16. package/ios/{KeyboardControllerViewManager.mm → views/KeyboardControllerViewManager.mm} +3 -0
  17. package/ios/{KeyboardControllerViewManager.swift → views/KeyboardControllerViewManager.swift} +14 -0
  18. package/jest/index.js +16 -0
  19. package/lib/commonjs/animated.js +18 -4
  20. package/lib/commonjs/animated.js.map +1 -1
  21. package/lib/commonjs/bindings.js.map +1 -1
  22. package/lib/commonjs/bindings.native.js.map +1 -1
  23. package/lib/commonjs/components/KeyboardAvoidingView/hooks.js.map +1 -1
  24. package/lib/commonjs/components/KeyboardAvoidingView/index.js.map +1 -1
  25. package/lib/commonjs/components/KeyboardStickyView/index.js +44 -0
  26. package/lib/commonjs/components/KeyboardStickyView/index.js.map +1 -0
  27. package/lib/commonjs/components/index.js +7 -0
  28. package/lib/commonjs/components/index.js.map +1 -1
  29. package/lib/commonjs/constants.js.map +1 -1
  30. package/lib/commonjs/context.js +6 -3
  31. package/lib/commonjs/context.js.map +1 -1
  32. package/lib/commonjs/hooks.js +8 -1
  33. package/lib/commonjs/hooks.js.map +1 -1
  34. package/lib/commonjs/index.js +8 -1
  35. package/lib/commonjs/index.js.map +1 -1
  36. package/lib/commonjs/internal.js.map +1 -1
  37. package/lib/commonjs/reanimated.js +5 -2
  38. package/lib/commonjs/reanimated.js.map +1 -1
  39. package/lib/commonjs/reanimated.native.js +18 -1
  40. package/lib/commonjs/reanimated.native.js.map +1 -1
  41. package/lib/commonjs/replicas.js +1 -1
  42. package/lib/commonjs/replicas.js.map +1 -1
  43. package/lib/commonjs/specs/KeyboardControllerViewNativeComponent.js.map +1 -1
  44. package/lib/commonjs/specs/KeyboardGestureAreaNativeComponent.js.map +1 -1
  45. package/lib/commonjs/specs/NativeKeyboardController.js.map +1 -1
  46. package/lib/commonjs/specs/NativeStatusBarManagerCompat.js.map +1 -1
  47. package/lib/commonjs/types.js.map +1 -1
  48. package/lib/module/animated.js +19 -5
  49. package/lib/module/animated.js.map +1 -1
  50. package/lib/module/bindings.js.map +1 -1
  51. package/lib/module/bindings.native.js +1 -1
  52. package/lib/module/bindings.native.js.map +1 -1
  53. package/lib/module/components/KeyboardAvoidingView/hooks.js.map +1 -1
  54. package/lib/module/components/KeyboardAvoidingView/index.js +2 -2
  55. package/lib/module/components/KeyboardAvoidingView/index.js.map +1 -1
  56. package/lib/module/components/KeyboardStickyView/index.js +35 -0
  57. package/lib/module/components/KeyboardStickyView/index.js.map +1 -0
  58. package/lib/module/components/index.js +1 -0
  59. package/lib/module/components/index.js.map +1 -1
  60. package/lib/module/constants.js.map +1 -1
  61. package/lib/module/context.js +6 -3
  62. package/lib/module/context.js.map +1 -1
  63. package/lib/module/hooks.js +7 -1
  64. package/lib/module/hooks.js.map +1 -1
  65. package/lib/module/index.js +1 -1
  66. package/lib/module/index.js.map +1 -1
  67. package/lib/module/internal.js.map +1 -1
  68. package/lib/module/reanimated.js +3 -1
  69. package/lib/module/reanimated.js.map +1 -1
  70. package/lib/module/reanimated.native.js +16 -0
  71. package/lib/module/reanimated.native.js.map +1 -1
  72. package/lib/module/replicas.js +2 -2
  73. package/lib/module/replicas.js.map +1 -1
  74. package/lib/module/specs/KeyboardControllerViewNativeComponent.js.map +1 -1
  75. package/lib/module/specs/KeyboardGestureAreaNativeComponent.js.map +1 -1
  76. package/lib/module/specs/NativeKeyboardController.js.map +1 -1
  77. package/lib/module/specs/NativeStatusBarManagerCompat.js.map +1 -1
  78. package/lib/module/types.js.map +1 -1
  79. package/lib/typescript/components/KeyboardAvoidingView/index.d.ts +2 -1
  80. package/lib/typescript/components/KeyboardStickyView/index.d.ts +20 -0
  81. package/lib/typescript/components/index.d.ts +1 -0
  82. package/lib/typescript/context.d.ts +3 -2
  83. package/lib/typescript/hooks.d.ts +5 -2
  84. package/lib/typescript/index.d.ts +1 -1
  85. package/lib/typescript/internal.d.ts +1 -1
  86. package/lib/typescript/reanimated.d.ts +2 -1
  87. package/lib/typescript/reanimated.native.d.ts +2 -1
  88. package/lib/typescript/specs/KeyboardControllerViewNativeComponent.d.ts +12 -0
  89. package/lib/typescript/types.d.ts +16 -0
  90. package/package.json +9 -28
  91. package/src/animated.tsx +30 -7
  92. package/src/bindings.native.ts +1 -1
  93. package/src/bindings.ts +3 -1
  94. package/src/components/KeyboardAvoidingView/hooks.ts +1 -0
  95. package/src/components/KeyboardAvoidingView/index.tsx +8 -10
  96. package/src/components/KeyboardStickyView/index.tsx +63 -0
  97. package/src/components/index.ts +1 -0
  98. package/src/constants.ts +2 -0
  99. package/src/context.ts +12 -5
  100. package/src/hooks.ts +10 -6
  101. package/src/index.ts +1 -1
  102. package/src/internal.ts +3 -1
  103. package/src/reanimated.native.ts +30 -1
  104. package/src/reanimated.ts +13 -2
  105. package/src/replicas.ts +2 -2
  106. package/src/specs/KeyboardControllerViewNativeComponent.ts +15 -1
  107. package/src/specs/KeyboardGestureAreaNativeComponent.ts +2 -1
  108. package/src/specs/NativeKeyboardController.ts +2 -1
  109. package/src/specs/NativeStatusBarManagerCompat.ts +2 -1
  110. package/src/types.ts +27 -1
  111. /package/ios/{KeyboardMoveEvent.h → events/KeyboardMoveEvent.h} +0 -0
  112. /package/ios/{KeyboardMoveEvent.m → events/KeyboardMoveEvent.m} +0 -0
  113. /package/ios/{KeyboardMovementObserver.swift → observers/KeyboardMovementObserver.swift} +0 -0
  114. /package/ios/{KeyboardControllerView.h → views/KeyboardControllerView.h} +0 -0
package/README.md CHANGED
@@ -13,7 +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
- - re-worked prebuilt components (such as `KeyboardAvoidingView`) 📚
16
+ - prebuilt components (`KeyboardStickyView`, re-worked `KeyboardAvoidingView`) 📚
17
+ - easy focused input information retrieval 📝 🔮
17
18
  - works with any navigation library 🧭
18
19
  - and more is coming... Stay tuned! 😊
19
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
@@ -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
 
@@ -33,6 +33,8 @@ class KeyboardControllerViewManagerImpl(mReactContext: ReactApplicationContext)
33
33
  MapBuilder.of("registrationName", "onKeyboardMoveEnd"),
34
34
  "topKeyboardMoveInteractive",
35
35
  MapBuilder.of("registrationName", "onKeyboardMoveInteractive"),
36
+ "topFocusedInputLayoutChanged",
37
+ MapBuilder.of("registrationName", "onFocusedInputLayoutChanged"),
36
38
  )
37
39
 
38
40
  return map
@@ -12,10 +12,10 @@ 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
 
@@ -30,6 +30,7 @@ class EdgeToEdgeReactViewGroup(private val reactContext: ThemedReactContext) : R
30
30
  // internal class members
31
31
  private var eventView: ReactViewGroup? = null
32
32
  private var wasMounted = false
33
+ private var callback: KeyboardAnimationCallback? = null
33
34
 
34
35
  // region View lifecycles
35
36
  override fun onAttachedToWindow() {
@@ -47,11 +48,7 @@ class EdgeToEdgeReactViewGroup(private val reactContext: ThemedReactContext) : R
47
48
  override fun onDetachedFromWindow() {
48
49
  super.onDetachedFromWindow()
49
50
 
50
- // we need to remove view asynchronously from `onDetachedFromWindow` method
51
- // otherwise we may face NPE when app is getting opened via universal link
52
- // see https://github.com/kirillzyusko/react-native-keyboard-controller/issues/242
53
- // for more details
54
- Handler(Looper.getMainLooper()).post { this.removeKeyboardCallbacks() }
51
+ this.removeKeyboardCallbacks()
55
52
  }
56
53
  // endregion
57
54
 
@@ -118,7 +115,7 @@ class EdgeToEdgeReactViewGroup(private val reactContext: ThemedReactContext) : R
118
115
  val root = this.getContentView()
119
116
  root?.addView(eventView)
120
117
 
121
- val callback = KeyboardAnimationCallback(
118
+ callback = KeyboardAnimationCallback(
122
119
  view = this,
123
120
  persistentInsetTypes = WindowInsetsCompat.Type.systemBars(),
124
121
  deferredInsetTypes = WindowInsetsCompat.Type.ime(),
@@ -137,7 +134,18 @@ class EdgeToEdgeReactViewGroup(private val reactContext: ThemedReactContext) : R
137
134
  }
138
135
 
139
136
  private fun removeKeyboardCallbacks() {
140
- eventView.removeSelf()
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() }
141
149
  }
142
150
 
143
151
  private fun getContentView(): FitWindowsLinearLayout? {
@@ -52,3 +52,10 @@ public extension Optional where Wrapped == UIResponder {
52
52
  #endif
53
53
  }
54
54
  }
55
+
56
+ public extension UIView {
57
+ var globalFrame: CGRect? {
58
+ let rootView = UIApplication.shared.keyWindow?.rootViewController?.view
59
+ return superview?.convert(frame, to: rootView)
60
+ }
61
+ }
@@ -8,6 +8,8 @@
8
8
 
9
9
  /* Begin PBXBuildFile section */
10
10
  0807071E2A34807B00C05A19 /* Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0807071D2A34807B00C05A19 /* Extensions.swift */; };
11
+ 084AEEC62ACF49A8001A3069 /* FocusedInputLayoutChangedEvent.m in Sources */ = {isa = PBXBuildFile; fileRef = 084AEEC52ACF49A8001A3069 /* FocusedInputLayoutChangedEvent.m */; };
12
+ 084AEEC82ACF4AB2001A3069 /* FocusedInputLayoutObserver.swift in Sources */ = {isa = PBXBuildFile; fileRef = 084AEEC72ACF4AB2001A3069 /* FocusedInputLayoutObserver.swift */; };
11
13
  F333F8D428996B8D0015B05F /* KeyboardControllerView.mm in Sources */ = {isa = PBXBuildFile; fileRef = F333F8D228996B8D0015B05F /* KeyboardControllerView.mm */; };
12
14
  F359D34F28133C26000B6AFE /* KeyboardControllerModule.mm in Sources */ = {isa = PBXBuildFile; fileRef = F359D34E28133C26000B6AFE /* KeyboardControllerModule.mm */; };
13
15
  F3626A0728B3FE760021B2D9 /* KeyboardMovementObserver.swift in Sources */ = {isa = PBXBuildFile; fileRef = F3626A0628B3FE760021B2D9 /* KeyboardMovementObserver.swift */; };
@@ -29,6 +31,9 @@
29
31
 
30
32
  /* Begin PBXFileReference section */
31
33
  0807071D2A34807B00C05A19 /* Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Extensions.swift; sourceTree = "<group>"; };
34
+ 084AEEC22ACF479A001A3069 /* FocusedInputLayoutChangedEvent.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = FocusedInputLayoutChangedEvent.h; sourceTree = "<group>"; };
35
+ 084AEEC52ACF49A8001A3069 /* FocusedInputLayoutChangedEvent.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FocusedInputLayoutChangedEvent.m; sourceTree = "<group>"; };
36
+ 084AEEC72ACF4AB2001A3069 /* FocusedInputLayoutObserver.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FocusedInputLayoutObserver.swift; sourceTree = "<group>"; };
32
37
  134814201AA4EA6300B7C361 /* libKeyboardController.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = libKeyboardController.a; sourceTree = BUILT_PRODUCTS_DIR; };
33
38
  B3E7B5891CC2AC0600A0062D /* KeyboardControllerViewManager.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = KeyboardControllerViewManager.mm; sourceTree = "<group>"; };
34
39
  F333F8D128996B1C0015B05F /* KeyboardControllerView.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = KeyboardControllerView.h; sourceTree = "<group>"; };
@@ -53,6 +58,37 @@
53
58
  /* End PBXFrameworksBuildPhase section */
54
59
 
55
60
  /* Begin PBXGroup section */
61
+ 084AEEBF2ACDFBF1001A3069 /* events */ = {
62
+ isa = PBXGroup;
63
+ children = (
64
+ F3F50667289E653B003091D6 /* KeyboardMoveEvent.h */,
65
+ F3F50668289E653B003091D6 /* KeyboardMoveEvent.m */,
66
+ 084AEEC22ACF479A001A3069 /* FocusedInputLayoutChangedEvent.h */,
67
+ 084AEEC52ACF49A8001A3069 /* FocusedInputLayoutChangedEvent.m */,
68
+ );
69
+ path = events;
70
+ sourceTree = "<group>";
71
+ };
72
+ 084AEEC02ACDFC2A001A3069 /* observers */ = {
73
+ isa = PBXGroup;
74
+ children = (
75
+ F3626A0628B3FE760021B2D9 /* KeyboardMovementObserver.swift */,
76
+ 084AEEC72ACF4AB2001A3069 /* FocusedInputLayoutObserver.swift */,
77
+ );
78
+ path = observers;
79
+ sourceTree = "<group>";
80
+ };
81
+ 084AEEC12ACF405C001A3069 /* views */ = {
82
+ isa = PBXGroup;
83
+ children = (
84
+ F333F8D228996B8D0015B05F /* KeyboardControllerView.mm */,
85
+ F333F8D128996B1C0015B05F /* KeyboardControllerView.h */,
86
+ F4FF95D6245B92E800C19C63 /* KeyboardControllerViewManager.swift */,
87
+ B3E7B5891CC2AC0600A0062D /* KeyboardControllerViewManager.mm */,
88
+ );
89
+ path = views;
90
+ sourceTree = "<group>";
91
+ };
56
92
  134814211AA4EA7D00B7C361 /* Products */ = {
57
93
  isa = PBXGroup;
58
94
  children = (
@@ -64,16 +100,12 @@
64
100
  58B511D21A9E6C8500147676 = {
65
101
  isa = PBXGroup;
66
102
  children = (
103
+ 084AEEC12ACF405C001A3069 /* views */,
104
+ 084AEEC02ACDFC2A001A3069 /* observers */,
105
+ 084AEEBF2ACDFBF1001A3069 /* events */,
67
106
  0807071D2A34807B00C05A19 /* Extensions.swift */,
68
- F3F50667289E653B003091D6 /* KeyboardMoveEvent.h */,
69
- F3F50668289E653B003091D6 /* KeyboardMoveEvent.m */,
70
- F3626A0628B3FE760021B2D9 /* KeyboardMovementObserver.swift */,
71
- F333F8D228996B8D0015B05F /* KeyboardControllerView.mm */,
72
- F333F8D128996B1C0015B05F /* KeyboardControllerView.h */,
73
107
  F359D35028133C6F000B6AFE /* KeyboardControllerModule-Header.h */,
74
108
  F359D34E28133C26000B6AFE /* KeyboardControllerModule.mm */,
75
- F4FF95D6245B92E800C19C63 /* KeyboardControllerViewManager.swift */,
76
- B3E7B5891CC2AC0600A0062D /* KeyboardControllerViewManager.mm */,
77
109
  F4FF95D5245B92E700C19C63 /* KeyboardController-Bridging-Header.h */,
78
110
  134814211AA4EA7D00B7C361 /* Products */,
79
111
  );
@@ -136,9 +168,11 @@
136
168
  isa = PBXSourcesBuildPhase;
137
169
  buildActionMask = 2147483647;
138
170
  files = (
171
+ 084AEEC62ACF49A8001A3069 /* FocusedInputLayoutChangedEvent.m in Sources */,
139
172
  F3626A0728B3FE760021B2D9 /* KeyboardMovementObserver.swift in Sources */,
140
173
  0807071E2A34807B00C05A19 /* Extensions.swift in Sources */,
141
174
  F359D34F28133C26000B6AFE /* KeyboardControllerModule.mm in Sources */,
175
+ 084AEEC82ACF4AB2001A3069 /* FocusedInputLayoutObserver.swift in Sources */,
142
176
  F4FF95D7245B92E800C19C63 /* KeyboardControllerViewManager.swift in Sources */,
143
177
  F333F8D428996B8D0015B05F /* KeyboardControllerView.mm in Sources */,
144
178
  F3F50669289E653B003091D6 /* KeyboardMoveEvent.m in Sources */,
@@ -0,0 +1,16 @@
1
+ //
2
+ // FocusedInputLayoutChangedEvent.h
3
+ // KeyboardController
4
+ //
5
+ // Created by Kiryl Ziusko on 05/10/2023.
6
+ // Copyright © 2023 Facebook. All rights reserved.
7
+ //
8
+
9
+ #import <Foundation/Foundation.h>
10
+ #import <React/RCTEventDispatcherProtocol.h>
11
+
12
+ @interface FocusedInputLayoutChangedEvent : NSObject <RCTEvent>
13
+
14
+ - (instancetype)initWithReactTag:(NSNumber *)reactTag event:(NSObject *)event;
15
+
16
+ @end