react-native-keyboard-controller 1.8.0 → 1.9.1

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} +102 -63
  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
  /**
@@ -100,6 +110,7 @@ class KeyboardAnimationCallback(
100
110
  * - we dispatch `keyboardDidShow` (onEnd).
101
111
  */
102
112
  override fun onApplyWindowInsets(v: View, insets: WindowInsetsCompat): WindowInsetsCompat {
113
+ val keyboardHeight = getCurrentKeyboardHeight()
103
114
  // when keyboard appears values will be (false && true)
104
115
  // when keyboard disappears values will be (true && false)
105
116
  val isKeyboardShown = isKeyboardVisible && isKeyboardVisible()
@@ -111,53 +122,65 @@ class KeyboardAnimationCallback(
111
122
  // `InteractiveKeyboardProvider.isInteractive` detect case when keyboard moves
112
123
  // because of the gesture
113
124
  val isMoving = isTransitioning || InteractiveKeyboardProvider.isInteractive
114
- if (isKeyboardShown && !isMoving && Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
115
- val keyboardHeight = getCurrentKeyboardHeight()
116
- val duration = DEFAULT_ANIMATION_TIME.toInt()
125
+ val isKeyboardFullyVisible = isKeyboardShown && !isMoving
126
+ // when keyboard is opened and we trigger a transition from screen A to screen B,
127
+ // then this method is getting called and we start dispatching events, and later original
128
+ // `onStart`/`onProgress`/`onEnd` emitting their events (since keyboard is closing) and we
129
+ // are getting race conditions.
130
+ //
131
+ // but in general this check is a must because we are detecting keyboard size changes
132
+ // in this method
133
+ val isKeyboardSizeEqual = this.persistentKeyboardHeight == keyboardHeight
117
134
 
135
+ if (isKeyboardFullyVisible && !isKeyboardSizeEqual && Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
136
+ layoutObserver?.syncUpLayout()
118
137
  this.emitEvent("KeyboardController::keyboardWillShow", getEventParams(keyboardHeight))
119
- this.sendEventToJS(
138
+ context.dispatchEvent(
139
+ view.id,
120
140
  KeyboardTransitionEvent(
121
141
  surfaceId,
122
142
  view.id,
123
143
  "topKeyboardMoveStart",
124
144
  keyboardHeight,
125
145
  1.0,
126
- duration,
146
+ DEFAULT_ANIMATION_TIME,
127
147
  viewTagFocused,
128
148
  ),
129
149
  )
130
150
 
131
- val animation = ValueAnimator.ofFloat(this.persistentKeyboardHeight.toFloat(), keyboardHeight.toFloat())
151
+ val animation =
152
+ ValueAnimator.ofFloat(this.persistentKeyboardHeight.toFloat(), keyboardHeight.toFloat())
132
153
  animation.addUpdateListener { animator ->
133
154
  val toValue = animator.animatedValue as Float
134
- this.sendEventToJS(
155
+ context.dispatchEvent(
156
+ view.id,
135
157
  KeyboardTransitionEvent(
136
158
  surfaceId,
137
159
  view.id,
138
160
  "topKeyboardMove",
139
161
  toValue.toDouble(),
140
162
  toValue.toDouble() / keyboardHeight,
141
- duration,
163
+ DEFAULT_ANIMATION_TIME,
142
164
  viewTagFocused,
143
165
  ),
144
166
  )
145
167
  }
146
168
  animation.doOnEnd {
147
169
  this.emitEvent("KeyboardController::keyboardDidShow", getEventParams(keyboardHeight))
148
- this.sendEventToJS(
170
+ context.dispatchEvent(
171
+ view.id,
149
172
  KeyboardTransitionEvent(
150
173
  surfaceId,
151
174
  view.id,
152
175
  "topKeyboardMoveEnd",
153
176
  keyboardHeight,
154
177
  1.0,
155
- duration,
178
+ DEFAULT_ANIMATION_TIME,
156
179
  viewTagFocused,
157
180
  ),
158
181
  )
159
182
  }
160
- animation.setDuration(DEFAULT_ANIMATION_TIME).startDelay = 0
183
+ animation.setDuration(DEFAULT_ANIMATION_TIME.toLong()).startDelay = 0
161
184
  animation.start()
162
185
 
163
186
  this.persistentKeyboardHeight = keyboardHeight
@@ -180,13 +203,15 @@ class KeyboardAnimationCallback(
180
203
  this.persistentKeyboardHeight = keyboardHeight
181
204
  }
182
205
 
206
+ layoutObserver?.syncUpLayout()
183
207
  this.emitEvent(
184
208
  "KeyboardController::" + if (!isKeyboardVisible) "keyboardWillHide" else "keyboardWillShow",
185
209
  getEventParams(keyboardHeight),
186
210
  )
187
211
 
188
212
  Log.i(TAG, "HEIGHT:: $keyboardHeight TAG:: $viewTagFocused")
189
- this.sendEventToJS(
213
+ context.dispatchEvent(
214
+ view.id,
190
215
  KeyboardTransitionEvent(
191
216
  surfaceId,
192
217
  view.id,
@@ -227,10 +252,24 @@ class KeyboardAnimationCallback(
227
252
  // do nothing, just log an exception send progress as 0
228
253
  Log.w(TAG, "Caught arithmetic exception during `progress` calculation: $e")
229
254
  }
230
- Log.i(TAG, "DiffY: $diffY $height $progress ${InteractiveKeyboardProvider.isInteractive} $viewTagFocused")
255
+ Log.i(
256
+ TAG,
257
+ "DiffY: $diffY $height $progress ${InteractiveKeyboardProvider.isInteractive} $viewTagFocused",
258
+ )
231
259
 
232
260
  val event = if (InteractiveKeyboardProvider.isInteractive) "topKeyboardMoveInteractive" else "topKeyboardMove"
233
- this.sendEventToJS(KeyboardTransitionEvent(surfaceId, view.id, event, height, progress, duration, viewTagFocused))
261
+ context.dispatchEvent(
262
+ view.id,
263
+ KeyboardTransitionEvent(
264
+ surfaceId,
265
+ view.id,
266
+ event,
267
+ height,
268
+ progress,
269
+ duration,
270
+ viewTagFocused,
271
+ ),
272
+ )
234
273
 
235
274
  return insets
236
275
  }
@@ -259,7 +298,8 @@ class KeyboardAnimationCallback(
259
298
  "KeyboardController::" + if (!isKeyboardVisible) "keyboardDidHide" else "keyboardDidShow",
260
299
  getEventParams(keyboardHeight),
261
300
  )
262
- this.sendEventToJS(
301
+ context.dispatchEvent(
302
+ view.id,
263
303
  KeyboardTransitionEvent(
264
304
  surfaceId,
265
305
  view.id,
@@ -275,6 +315,11 @@ class KeyboardAnimationCallback(
275
315
  duration = 0
276
316
  }
277
317
 
318
+ fun destroy() {
319
+ view.viewTreeObserver.removeOnGlobalFocusChangeListener(focusListener)
320
+ layoutObserver?.destroy()
321
+ }
322
+
278
323
  private fun isKeyboardVisible(): Boolean {
279
324
  val insets = ViewCompat.getRootWindowInsets(view)
280
325
 
@@ -290,12 +335,6 @@ class KeyboardAnimationCallback(
290
335
  return (keyboardHeight - navigationBar).toFloat().dp.coerceAtLeast(0.0)
291
336
  }
292
337
 
293
- private fun sendEventToJS(event: Event<*>) {
294
- val eventDispatcher: EventDispatcher? =
295
- UIManagerHelper.getEventDispatcherForReactTag(context, view.id)
296
- eventDispatcher?.dispatchEvent(event)
297
- }
298
-
299
338
  private fun emitEvent(event: String, params: WritableMap) {
300
339
  context?.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter::class.java)?.emit(event, params)
301
340
 
@@ -313,6 +352,6 @@ class KeyboardAnimationCallback(
313
352
  }
314
353
 
315
354
  companion object {
316
- private const val DEFAULT_ANIMATION_TIME = 250L
355
+ private const val DEFAULT_ANIMATION_TIME = 250
317
356
  }
318
357
  }
@@ -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