react-native-keyboard-controller 1.11.6 → 1.12.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 (86) hide show
  1. package/android/src/main/java/com/reactnativekeyboardcontroller/events/FocusedInputLayoutChangedEvent.kt +1 -1
  2. package/android/src/main/java/com/reactnativekeyboardcontroller/events/FocusedInputSelectionChangedEvent.kt +51 -0
  3. package/android/src/main/java/com/reactnativekeyboardcontroller/events/FocusedInputTextChangedEvent.kt +1 -1
  4. package/android/src/main/java/com/reactnativekeyboardcontroller/extensions/EditText.kt +79 -0
  5. package/android/src/main/java/com/reactnativekeyboardcontroller/extensions/ThemedReactContext.kt +1 -2
  6. package/android/src/main/java/com/reactnativekeyboardcontroller/extensions/View.kt +1 -1
  7. package/android/src/main/java/com/reactnativekeyboardcontroller/interactive/interpolators/IosInterpolator.kt +1 -1
  8. package/android/src/main/java/com/reactnativekeyboardcontroller/listeners/FocusedInputObserver.kt +32 -0
  9. package/android/src/main/java/com/reactnativekeyboardcontroller/listeners/KeyboardAnimationCallback.kt +1 -1
  10. package/android/src/main/java/com/reactnativekeyboardcontroller/managers/KeyboardControllerViewManagerImpl.kt +2 -0
  11. package/android/src/main/java/com/reactnativekeyboardcontroller/ui/FrameScheduler.kt +40 -0
  12. package/android/src/main/java/com/reactnativekeyboardcontroller/views/EdgeToEdgeReactViewGroup.kt +1 -1
  13. package/ios/KeyboardController-Bridging-Header.h +1 -0
  14. package/ios/core/KeyboardAnimation.swift +77 -0
  15. package/ios/core/SpringAnimation.swift +60 -0
  16. package/ios/core/UIUtils.swift +11 -0
  17. package/ios/delegates/KCTextInputCompositeDelegate.swift +148 -0
  18. package/ios/events/FocusedInputSelectionChangedEvent.h +15 -0
  19. package/ios/events/FocusedInputSelectionChangedEvent.m +76 -0
  20. package/ios/objc/RCTUITextView+DelegateManager.h +14 -0
  21. package/ios/objc/RCTUITextView+DelegateManager.m +26 -0
  22. package/ios/observers/FocusedInputObserver.swift +74 -13
  23. package/ios/observers/KeyboardMovementObserver.swift +39 -2
  24. package/ios/views/KeyboardControllerView.mm +45 -8
  25. package/ios/views/KeyboardControllerViewManager.mm +1 -0
  26. package/ios/views/KeyboardControllerViewManager.swift +10 -2
  27. package/lib/commonjs/animated.js +8 -0
  28. package/lib/commonjs/animated.js.map +1 -1
  29. package/lib/commonjs/components/KeyboardAvoidingView/index.js.map +1 -1
  30. package/lib/commonjs/components/KeyboardAwareScrollView/index.js +5 -5
  31. package/lib/commonjs/components/KeyboardAwareScrollView/index.js.map +1 -1
  32. package/lib/commonjs/components/KeyboardStickyView/index.js.map +1 -1
  33. package/lib/commonjs/components/KeyboardToolbar/index.js +25 -7
  34. package/lib/commonjs/components/KeyboardToolbar/index.js.map +1 -1
  35. package/lib/commonjs/components/KeyboardToolbar/types.js.map +1 -1
  36. package/lib/commonjs/components/index.js.map +1 -1
  37. package/lib/commonjs/index.js.map +1 -1
  38. package/lib/commonjs/reanimated.js +2 -1
  39. package/lib/commonjs/reanimated.js.map +1 -1
  40. package/lib/commonjs/reanimated.native.js +18 -1
  41. package/lib/commonjs/reanimated.native.js.map +1 -1
  42. package/lib/commonjs/specs/KeyboardControllerViewNativeComponent.js.map +1 -1
  43. package/lib/commonjs/types.js.map +1 -1
  44. package/lib/module/animated.js +9 -1
  45. package/lib/module/animated.js.map +1 -1
  46. package/lib/module/components/KeyboardAvoidingView/index.js.map +1 -1
  47. package/lib/module/components/KeyboardAwareScrollView/index.js +5 -5
  48. package/lib/module/components/KeyboardAwareScrollView/index.js.map +1 -1
  49. package/lib/module/components/KeyboardStickyView/index.js.map +1 -1
  50. package/lib/module/components/KeyboardToolbar/index.js +26 -8
  51. package/lib/module/components/KeyboardToolbar/index.js.map +1 -1
  52. package/lib/module/components/KeyboardToolbar/types.js.map +1 -1
  53. package/lib/module/components/index.js.map +1 -1
  54. package/lib/module/index.js.map +1 -1
  55. package/lib/module/reanimated.js +1 -0
  56. package/lib/module/reanimated.js.map +1 -1
  57. package/lib/module/reanimated.native.js +16 -0
  58. package/lib/module/reanimated.native.js.map +1 -1
  59. package/lib/module/specs/KeyboardControllerViewNativeComponent.js.map +1 -1
  60. package/lib/module/types.js.map +1 -1
  61. package/lib/typescript/components/KeyboardAvoidingView/index.d.ts +20 -0
  62. package/lib/typescript/components/KeyboardAwareScrollView/index.d.ts +8 -0
  63. package/lib/typescript/components/KeyboardStickyView/index.d.ts +15 -0
  64. package/lib/typescript/components/KeyboardToolbar/index.d.ts +21 -1
  65. package/lib/typescript/components/KeyboardToolbar/types.d.ts +3 -1
  66. package/lib/typescript/components/index.d.ts +3 -0
  67. package/lib/typescript/index.d.ts +1 -1
  68. package/lib/typescript/reanimated.d.ts +2 -1
  69. package/lib/typescript/reanimated.native.d.ts +7 -4
  70. package/lib/typescript/specs/KeyboardControllerViewNativeComponent.d.ts +16 -0
  71. package/lib/typescript/types.d.ts +21 -0
  72. package/package.json +4 -3
  73. package/src/animated.tsx +12 -0
  74. package/src/components/KeyboardAvoidingView/index.tsx +5 -2
  75. package/src/components/KeyboardAwareScrollView/index.tsx +81 -81
  76. package/src/components/KeyboardStickyView/index.tsx +1 -1
  77. package/src/components/KeyboardToolbar/index.tsx +47 -7
  78. package/src/components/KeyboardToolbar/types.ts +25 -1
  79. package/src/components/index.ts +3 -0
  80. package/src/index.ts +6 -1
  81. package/src/reanimated.native.ts +30 -3
  82. package/src/reanimated.ts +6 -0
  83. package/src/specs/KeyboardControllerViewNativeComponent.ts +17 -0
  84. package/src/types.ts +31 -0
  85. package/ios/KeyboardController.xcodeproj/project.pbxproj +0 -387
  86. package/ios/observers/TextChangeObserver.swift +0 -41
@@ -19,7 +19,7 @@ class FocusedInputLayoutChangedEvent(
19
19
  surfaceId: Int,
20
20
  viewId: Int,
21
21
  private val event: FocusedInputLayoutChangedEventData,
22
- ) : Event<KeyboardTransitionEvent>(surfaceId, viewId) {
22
+ ) : Event<FocusedInputLayoutChangedEvent>(surfaceId, viewId) {
23
23
  override fun getEventName() = "topFocusedInputLayoutChanged"
24
24
 
25
25
  // All events for a given view can be coalesced
@@ -0,0 +1,51 @@
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 FocusedInputSelectionChangedEventData(
8
+ val target: Int,
9
+ val startX: Double,
10
+ val startY: Double,
11
+ val endX: Double,
12
+ val endY: Double,
13
+ val start: Int,
14
+ val end: Int,
15
+ )
16
+
17
+ class FocusedInputSelectionChangedEvent(
18
+ surfaceId: Int,
19
+ viewId: Int,
20
+ private val event: FocusedInputSelectionChangedEventData,
21
+ ) : Event<FocusedInputSelectionChangedEvent>(surfaceId, viewId) {
22
+ override fun getEventName() = "topFocusedInputSelectionChanged"
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
+ "selection",
31
+ Arguments.createMap().apply {
32
+ putMap(
33
+ "start",
34
+ Arguments.createMap().apply {
35
+ putDouble("x", event.startX)
36
+ putDouble("y", event.startY)
37
+ putInt("position", event.start)
38
+ },
39
+ )
40
+ putMap(
41
+ "end",
42
+ Arguments.createMap().apply {
43
+ putDouble("x", event.endX)
44
+ putDouble("y", event.endY)
45
+ putInt("position", event.end)
46
+ },
47
+ )
48
+ },
49
+ )
50
+ }
51
+ }
@@ -8,7 +8,7 @@ class FocusedInputTextChangedEvent(
8
8
  surfaceId: Int,
9
9
  viewId: Int,
10
10
  private val text: String,
11
- ) : Event<KeyboardTransitionEvent>(surfaceId, viewId) {
11
+ ) : Event<FocusedInputTextChangedEvent>(surfaceId, viewId) {
12
12
  override fun getEventName() = "topFocusedInputTextChanged"
13
13
 
14
14
  // All events for a given view can be coalesced
@@ -1,5 +1,6 @@
1
1
  package com.reactnativekeyboardcontroller.extensions
2
2
 
3
+ import android.os.Build
3
4
  import android.text.Editable
4
5
  import android.text.TextWatcher
5
6
  import android.util.Log
@@ -7,7 +8,10 @@ import android.view.View
7
8
  import android.widget.EditText
8
9
  import android.widget.ScrollView
9
10
  import com.facebook.react.views.textinput.ReactEditText
11
+ import com.reactnativekeyboardcontroller.ui.FrameScheduler
10
12
  import java.lang.reflect.Field
13
+ import kotlin.math.max
14
+ import kotlin.math.min
11
15
 
12
16
  /**
13
17
  * Adds a listener that will be fired only once for each unique value.
@@ -94,3 +98,78 @@ fun EditText?.focus() {
94
98
  this?.requestFocus()
95
99
  }
96
100
  }
101
+
102
+ class KeyboardControllerSelectionWatcher(
103
+ private val editText: ReactEditText,
104
+ private val action: (start: Int, end: Int, startX: Double, startY: Double, endX: Double, endY: Double) -> Unit,
105
+ ) {
106
+ private var lastSelectionStart: Int = -1
107
+ private var lastSelectionEnd: Int = -1
108
+
109
+ private val frameScheduler = FrameScheduler {
110
+ val start = editText.selectionStart
111
+ val end = editText.selectionEnd
112
+
113
+ if (lastSelectionStart != start || lastSelectionEnd != end) {
114
+ lastSelectionStart = start
115
+ lastSelectionEnd = end
116
+
117
+ val view = editText
118
+ val layout = view.layout
119
+
120
+ if (layout === null) {
121
+ return@FrameScheduler
122
+ }
123
+
124
+ val cursorPositionStartX: Float
125
+ val cursorPositionStartY: Float
126
+ val cursorPositionEndX: Float
127
+ val cursorPositionEndY: Float
128
+
129
+ val realStart = min(start, end)
130
+ val realEnd = max(start, end)
131
+
132
+ val lineStart = layout.getLineForOffset(realStart)
133
+ val baselineStart = layout.getLineBaseline(lineStart)
134
+ val ascentStart = layout.getLineAscent(lineStart)
135
+
136
+ cursorPositionStartX = layout.getPrimaryHorizontal(realStart)
137
+ cursorPositionStartY = (baselineStart + ascentStart).toFloat()
138
+
139
+ val lineEnd = layout.getLineForOffset(realEnd)
140
+
141
+ val right = layout.getPrimaryHorizontal(realEnd)
142
+ val bottom = layout.getLineBottom(lineEnd) + layout.getLineAscent(lineEnd)
143
+ val cursorWidth = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
144
+ view.textCursorDrawable?.intrinsicWidth ?: 0
145
+ } else {
146
+ 0
147
+ }
148
+
149
+ cursorPositionEndX = right + cursorWidth
150
+ cursorPositionEndY = bottom.toFloat()
151
+
152
+ action(start, end, cursorPositionStartX.dp, cursorPositionStartY.dp, cursorPositionEndX.dp, cursorPositionEndY.dp)
153
+ }
154
+ }
155
+
156
+ fun setup() {
157
+ frameScheduler.start()
158
+ }
159
+
160
+ fun destroy() {
161
+ frameScheduler.stop()
162
+ }
163
+ }
164
+
165
+ fun ReactEditText.addOnSelectionChangedListener(
166
+ action: (start: Int, end: Int, startX: Double, startY: Double, endX: Double, endY: Double) -> Unit,
167
+ ): () -> Unit {
168
+ val listener = KeyboardControllerSelectionWatcher(this, action)
169
+
170
+ listener.setup()
171
+
172
+ return {
173
+ listener.destroy()
174
+ }
175
+ }
@@ -3,7 +3,6 @@ package com.reactnativekeyboardcontroller.extensions
3
3
  import android.util.Log
4
4
  import android.view.View
5
5
  import com.facebook.react.bridge.WritableMap
6
- import com.facebook.react.modules.core.DeviceEventManagerModule
7
6
  import com.facebook.react.uimanager.ThemedReactContext
8
7
  import com.facebook.react.uimanager.UIManagerHelper
9
8
  import com.facebook.react.uimanager.events.Event
@@ -19,7 +18,7 @@ fun ThemedReactContext?.dispatchEvent(viewId: Int, event: Event<*>) {
19
18
  }
20
19
 
21
20
  fun ThemedReactContext?.emitEvent(event: String, params: WritableMap) {
22
- this?.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter::class.java)?.emit(event, params)
21
+ this?.reactApplicationContext?.emitDeviceEvent(event, params)
23
22
 
24
23
  Log.i("ThemedReactContext", event)
25
24
  }
@@ -7,7 +7,7 @@ import android.view.View
7
7
  import androidx.annotation.RequiresApi
8
8
 
9
9
  /**
10
- * Call this everytime when using [ViewCompat.setOnApplyWindowInsetsListener]
10
+ * Call this every time when using [ViewCompat.setOnApplyWindowInsetsListener]
11
11
  * to ensure that insets are always received.
12
12
  * @see https://stackoverflow.com/a/61909205/9272042
13
13
  */
@@ -7,7 +7,7 @@ class IosInterpolator : Interpolator {
7
7
  keyboardPosition: Int,
8
8
  ): Int {
9
9
  if (
10
- absoluteFingerPosition <= keyboardPosition || // user overscrolled keyboard
10
+ absoluteFingerPosition <= keyboardPosition || // user over scrolled keyboard
11
11
  dy <= 0 // user scrolls up
12
12
  ) {
13
13
  return dy
@@ -10,7 +10,10 @@ import com.facebook.react.views.textinput.ReactEditText
10
10
  import com.facebook.react.views.view.ReactViewGroup
11
11
  import com.reactnativekeyboardcontroller.events.FocusedInputLayoutChangedEvent
12
12
  import com.reactnativekeyboardcontroller.events.FocusedInputLayoutChangedEventData
13
+ import com.reactnativekeyboardcontroller.events.FocusedInputSelectionChangedEvent
14
+ import com.reactnativekeyboardcontroller.events.FocusedInputSelectionChangedEventData
13
15
  import com.reactnativekeyboardcontroller.events.FocusedInputTextChangedEvent
16
+ import com.reactnativekeyboardcontroller.extensions.addOnSelectionChangedListener
14
17
  import com.reactnativekeyboardcontroller.extensions.addOnTextChangedListener
15
18
  import com.reactnativekeyboardcontroller.extensions.dispatchEvent
16
19
  import com.reactnativekeyboardcontroller.extensions.dp
@@ -40,6 +43,7 @@ class FocusedInputObserver(val view: ReactViewGroup, private val context: Themed
40
43
  private var lastFocusedInput: ReactEditText? = null
41
44
  private var lastEventDispatched: FocusedInputLayoutChangedEventData = noFocusedInputEvent
42
45
  private var textWatcher: TextWatcher? = null
46
+ private var selectionSubscription: (() -> Unit)? = null
43
47
 
44
48
  // listeners
45
49
  private val layoutListener =
@@ -57,11 +61,38 @@ class FocusedInputObserver(val view: ReactViewGroup, private val context: Themed
57
61
  ),
58
62
  )
59
63
  }
64
+ private val selectionListener: (
65
+ start: Int,
66
+ end: Int,
67
+ startX: Double,
68
+ startY: Double,
69
+ endX: Double,
70
+ endY: Double,
71
+ ) -> Unit = listener@{ start, end, startX, startY, endX, endY ->
72
+ val input = lastFocusedInput ?: return@listener
73
+ context.dispatchEvent(
74
+ view.id,
75
+ FocusedInputSelectionChangedEvent(
76
+ surfaceId,
77
+ view.id,
78
+ event = FocusedInputSelectionChangedEventData(
79
+ target = input.id,
80
+ start = start,
81
+ end = end,
82
+ startX = startX,
83
+ startY = startY,
84
+ endX = endX,
85
+ endY = endY,
86
+ ),
87
+ ),
88
+ )
89
+ }
60
90
  private val focusListener = OnGlobalFocusChangeListener { oldFocus, newFocus ->
61
91
  // unfocused or focused was changed
62
92
  if (newFocus == null || oldFocus != null) {
63
93
  lastFocusedInput?.removeOnLayoutChangeListener(layoutListener)
64
94
  lastFocusedInput?.removeTextChangedListener(textWatcher)
95
+ selectionSubscription?.invoke()
65
96
  lastFocusedInput = null
66
97
  }
67
98
  if (newFocus is ReactEditText) {
@@ -69,6 +100,7 @@ class FocusedInputObserver(val view: ReactViewGroup, private val context: Themed
69
100
  newFocus.addOnLayoutChangeListener(layoutListener)
70
101
  this.syncUpLayout()
71
102
  textWatcher = newFocus.addOnTextChangedListener(textListener)
103
+ selectionSubscription = newFocus.addOnSelectionChangedListener(selectionListener)
72
104
  FocusedInputHolder.set(newFocus)
73
105
 
74
106
  val allInputFields = ViewHierarchyNavigator.getAllInputFields(context?.rootView)
@@ -54,7 +54,7 @@ class KeyboardAnimationCallback(
54
54
  // imitate iOS behavior and send two instant start/end events containing an info about new tag
55
55
  // 1. onStart/onMove/onEnd can be still dispatched after, if keyboard change size (numeric -> alphabetic type)
56
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
57
+ // 100% included in onStart/onMove/onEnd life cycles, but triggering onStart/onEnd several time
58
58
  // can bring breaking changes
59
59
  context.dispatchEvent(
60
60
  view.id,
@@ -37,6 +37,8 @@ class KeyboardControllerViewManagerImpl(mReactContext: ReactApplicationContext)
37
37
  MapBuilder.of("registrationName", "onFocusedInputLayoutChanged"),
38
38
  "topFocusedInputTextChanged",
39
39
  MapBuilder.of("registrationName", "onFocusedInputTextChanged"),
40
+ "topFocusedInputSelectionChanged",
41
+ MapBuilder.of("registrationName", "onFocusedInputSelectionChanged"),
40
42
  )
41
43
 
42
44
  return map
@@ -0,0 +1,40 @@
1
+ package com.reactnativekeyboardcontroller.ui
2
+
3
+ import android.view.Choreographer
4
+
5
+ /**
6
+ * A class that schedules a callback to be executed on each frame using the Android Choreographer framework.
7
+ *
8
+ * @property callback The function to be executed on each frame.
9
+ */
10
+ class FrameScheduler(private val callback: () -> Unit) {
11
+ /**
12
+ * A FrameCallback instance responsible for running the provided callback
13
+ * on each frame and rescheduling itself for the next frame.
14
+ */
15
+ private val frameCallback = object : Choreographer.FrameCallback {
16
+ override fun doFrame(frameTimeNanoSeconds: Long) {
17
+ // Execute the callback
18
+ callback()
19
+
20
+ // Re-post the callback to the next frame
21
+ Choreographer.getInstance().postFrameCallback(this)
22
+ }
23
+ }
24
+
25
+ /**
26
+ * Starts the frame callback, which will continuously call the provided function
27
+ * on every frame until `stop()` is called.
28
+ */
29
+ fun start() {
30
+ Choreographer.getInstance().postFrameCallback(frameCallback)
31
+ }
32
+
33
+ /**
34
+ * Stops the frame callback from being invoked further. The function will no longer
35
+ * be called on each frame until `start()` is invoked again.
36
+ */
37
+ fun stop() {
38
+ Choreographer.getInstance().removeFrameCallback(frameCallback)
39
+ }
40
+ }
@@ -32,7 +32,7 @@ class EdgeToEdgeReactViewGroup(private val reactContext: ThemedReactContext) : R
32
32
  private var wasMounted = false
33
33
  private var callback: KeyboardAnimationCallback? = null
34
34
 
35
- // region View lifecycles
35
+ // region View life cycles
36
36
  override fun onAttachedToWindow() {
37
37
  super.onAttachedToWindow()
38
38
 
@@ -1,2 +1,3 @@
1
1
  #import <React/RCTUIManager.h>
2
+ #import <React/RCTUITextView.h>
2
3
  #import <React/RCTViewManager.h>
@@ -0,0 +1,77 @@
1
+ //
2
+ // KeyboardAnimation.swift
3
+ // react-native-keyboard-controller
4
+ //
5
+ // Created by Kiryl Ziusko on 04/05/2024.
6
+ //
7
+
8
+ import Foundation
9
+
10
+ protocol KeyboardAnimationProtocol {
11
+ var startTime: CFTimeInterval { get }
12
+ var diff: Double { get }
13
+ var isIncreasing: Bool { get }
14
+
15
+ func valueAt(time: Double) -> Double
16
+ func timingAt(value: Double) -> Double
17
+ }
18
+
19
+ public class KeyboardAnimation: KeyboardAnimationProtocol {
20
+ private weak var animation: CAMediaTiming?
21
+
22
+ // constructor variables
23
+ let fromValue: Double
24
+ let toValue: Double
25
+ let speed: Float
26
+ let timestamp: CFTimeInterval
27
+
28
+ init(fromValue: Double, toValue: Double, animation: CAMediaTiming) {
29
+ self.fromValue = fromValue
30
+ self.toValue = toValue
31
+ self.animation = animation
32
+ speed = animation.speed
33
+ timestamp = CACurrentMediaTime()
34
+ }
35
+
36
+ // public getters
37
+ var startTime: CFTimeInterval {
38
+ // when concurrent animation happens, then `.beginTime` remains the same
39
+ return max(animation?.beginTime ?? timestamp, timestamp)
40
+ }
41
+
42
+ var diff: Double {
43
+ return ((animation?.beginTime ?? timestamp) - timestamp).truncatingRemainder(dividingBy: UIUtils.nextFrame)
44
+ }
45
+
46
+ var isIncreasing: Bool {
47
+ return fromValue < toValue
48
+ }
49
+
50
+ func valueAt(time _: Double) -> Double {
51
+ fatalError("Method is not implemented in abstract class!")
52
+ }
53
+
54
+ func timingAt(value: Double) -> Double {
55
+ var lowerBound = 0.0
56
+ var upperBound = 1.0 // Assuming 1 second is the max duration for simplicity
57
+ let tolerance = 0.00001 // Define how precise you want to be
58
+ var tGuess = 0.0
59
+
60
+ // Check the direction of the animation
61
+ let isIncreasing = isIncreasing
62
+
63
+ while (upperBound - lowerBound) > tolerance {
64
+ tGuess = (lowerBound + upperBound) / 2
65
+ let currentValue = valueAt(time: tGuess / Double(speed))
66
+
67
+ // Adjust the condition to account for the direction of animation
68
+ if (currentValue < value && isIncreasing) || (currentValue > value && !isIncreasing) {
69
+ lowerBound = tGuess
70
+ } else {
71
+ upperBound = tGuess
72
+ }
73
+ }
74
+
75
+ return tGuess / Double(speed)
76
+ }
77
+ }
@@ -0,0 +1,60 @@
1
+ //
2
+ // SpringAnimation.swift
3
+ // KeyboardController
4
+ //
5
+ // Created by Kiryl Ziusko on 18/03/2024.
6
+ // Copyright © 2024 Facebook. All rights reserved.
7
+ //
8
+
9
+ import Foundation
10
+ import QuartzCore
11
+
12
+ // swiftlint:disable identifier_name
13
+ public class SpringAnimation: KeyboardAnimation {
14
+ // internal variables
15
+ private let zeta: Double // Damping ratio
16
+ private let omega0: Double // Undamped angular frequency of the oscillator
17
+ private let omega1: Double // Exponential decay
18
+ private let v0: Double // Initial velocity
19
+
20
+ // constructor variables
21
+ private let stiffness: Double
22
+ private let damping: Double
23
+ private let mass: Double
24
+ private let initialVelocity: Double
25
+
26
+ init(animation: CASpringAnimation, fromValue: Double, toValue: Double) {
27
+ stiffness = animation.stiffness
28
+ damping = animation.damping
29
+ mass = animation.mass
30
+ initialVelocity = animation.initialVelocity
31
+
32
+ zeta = damping / (2 * sqrt(stiffness * mass)) // Damping ratio
33
+ omega0 = sqrt(stiffness / mass) // Undamped angular frequency of the oscillator
34
+ omega1 = omega0 * sqrt(1.0 - zeta * zeta) // Exponential decay
35
+ v0 = -initialVelocity
36
+
37
+ super.init(fromValue: fromValue, toValue: toValue, animation: animation)
38
+ }
39
+
40
+ // public functions
41
+ override func valueAt(time: Double) -> Double {
42
+ let t = time * Double(speed)
43
+ let x0 = toValue - fromValue
44
+
45
+ var y: Double
46
+ if zeta < 1 {
47
+ // Under damped
48
+ let envelope = exp(-zeta * omega0 * t)
49
+ y = toValue - envelope * (((v0 + zeta * omega0 * x0) / omega1) * sin(omega1 * t) + x0 * cos(omega1 * t))
50
+ } else {
51
+ // Critically damped
52
+ let envelope = exp(-omega0 * t)
53
+ y = toValue - envelope * (x0 + (v0 + omega0 * x0) * t)
54
+ }
55
+
56
+ return y
57
+ }
58
+ }
59
+
60
+ // swiftlint:enable identifier_name
@@ -0,0 +1,11 @@
1
+ //
2
+ // UIUtils.swift
3
+ // KeyboardController
4
+ //
5
+ // Created by Kiryl Ziusko on 14/04/2024.
6
+ // Copyright © 2024 Facebook. All rights reserved.
7
+ //
8
+
9
+ enum UIUtils {
10
+ static let nextFrame = 1.0 / 60
11
+ }
@@ -0,0 +1,148 @@
1
+ //
2
+ // KCTextInputCompositeDelegate.swift
3
+ // react-native-keyboard-controller
4
+ //
5
+ // Created by Kiryl Ziusko on 24/04/2024.
6
+ //
7
+
8
+ import Foundation
9
+
10
+ struct Selection {
11
+ var start: Int
12
+ var startX: CGFloat
13
+ var startY: CGFloat
14
+ var end: Int
15
+ var endX: CGFloat
16
+ var endY: CGFloat
17
+ }
18
+
19
+ func textSelection(in textInput: UITextInput) -> Selection? {
20
+ if let selectedRange = textInput.selectedTextRange {
21
+ let caretRectStart = textInput.caretRect(for: selectedRange.start)
22
+ let caretRectEnd = textInput.caretRect(for: selectedRange.end)
23
+
24
+ let coordinates = Selection(
25
+ start: textInput.offset(from: textInput.beginningOfDocument, to: selectedRange.start),
26
+ startX: caretRectStart.origin.x,
27
+ startY: caretRectStart.origin.y,
28
+ end: textInput.offset(from: textInput.beginningOfDocument, to: selectedRange.end),
29
+ endX: caretRectEnd.origin.x + caretRectEnd.size.width,
30
+ endY: caretRectEnd.origin.y + caretRectEnd.size.height
31
+ )
32
+
33
+ return coordinates
34
+ }
35
+
36
+ return nil
37
+ }
38
+
39
+ /**
40
+ * A delegate that is being set to any focused input
41
+ * and intercepts some specific events that needs to be handled
42
+ * for universal keyboard avoiding solutions (such as text selection etc.)
43
+ * and forward all calls to underlying delegate
44
+ */
45
+ class KCTextInputCompositeDelegate: NSObject, UITextViewDelegate, UITextFieldDelegate {
46
+ // constructor members
47
+ var onSelectionChange: (_ event: NSDictionary) -> Void
48
+ var onTextChange: (_ text: String?) -> Void
49
+ // delegates
50
+ weak var textViewDelegate: UITextViewDelegate?
51
+ weak var textFieldDelegate: UITextFieldDelegate?
52
+
53
+ public init(
54
+ onSelectionChange: @escaping (_ event: NSDictionary) -> Void,
55
+ onTextChange: @escaping (_ text: String?) -> Void
56
+ ) {
57
+ self.onSelectionChange = onSelectionChange
58
+ self.onTextChange = onTextChange
59
+ }
60
+
61
+ // MARK: setters/getters
62
+
63
+ public func setTextViewDelegate(delegate: UITextViewDelegate?) {
64
+ textViewDelegate = delegate
65
+ textFieldDelegate = nil
66
+ }
67
+
68
+ public func setTextFieldDelegate(delegate: UITextFieldDelegate?) {
69
+ textFieldDelegate = delegate
70
+ textViewDelegate = nil
71
+ }
72
+
73
+ // Getter for the active delegate
74
+ public var activeDelegate: AnyObject? {
75
+ return textViewDelegate ?? textFieldDelegate
76
+ }
77
+
78
+ // MARK: UITextViewDelegate
79
+
80
+ func textViewDidChangeSelection(_ textView: UITextView) {
81
+ textViewDelegate?.textViewDidChangeSelection?(textView)
82
+ updateSelectionPosition(textInput: textView)
83
+ }
84
+
85
+ func textViewDidChange(_ textView: UITextView) {
86
+ defer {
87
+ self.onTextChange(textView.text)
88
+ }
89
+
90
+ textViewDelegate?.textViewDidChange?(textView)
91
+ }
92
+
93
+ // MARK: UITextFieldDelegate
94
+
95
+ func textFieldDidChangeSelection(_ textField: UITextField) {
96
+ textFieldDelegate?.textFieldDidChangeSelection?(textField)
97
+ updateSelectionPosition(textInput: textField)
98
+ }
99
+
100
+ func textField(
101
+ _ textField: UITextField,
102
+ shouldChangeCharactersIn range: NSRange,
103
+ replacementString string: String
104
+ ) -> Bool {
105
+ defer {
106
+ self.onTextChange(textField.text)
107
+ }
108
+
109
+ return textFieldDelegate?.textField?(textField, shouldChangeCharactersIn: range, replacementString: string) ?? true
110
+ }
111
+
112
+ // MARK: call forwarding
113
+
114
+ override func responds(to aSelector: Selector!) -> Bool {
115
+ if super.responds(to: aSelector) {
116
+ return true
117
+ }
118
+ return activeDelegate?.responds(to: aSelector) ?? false
119
+ }
120
+
121
+ override func forwardingTarget(for aSelector: Selector!) -> Any? {
122
+ if activeDelegate?.responds(to: aSelector) ?? false {
123
+ return activeDelegate
124
+ }
125
+ return super.forwardingTarget(for: aSelector)
126
+ }
127
+
128
+ // MARK: Private functions
129
+
130
+ private func updateSelectionPosition(textInput: UITextInput) {
131
+ if let selection = textSelection(in: textInput) {
132
+ onSelectionChange([
133
+ "selection": [
134
+ "start": [
135
+ "x": selection.startX,
136
+ "y": selection.startY,
137
+ "position": selection.start,
138
+ ],
139
+ "end": [
140
+ "x": selection.endX,
141
+ "y": selection.endY,
142
+ "position": selection.end,
143
+ ],
144
+ ],
145
+ ])
146
+ }
147
+ }
148
+ }
@@ -0,0 +1,15 @@
1
+ //
2
+ // FocusedInputSelectionChangedEvent.h
3
+ // KeyboardController
4
+ //
5
+ // Created by Kiryl Ziusko on 23/04/2024.
6
+ //
7
+
8
+ #import <Foundation/Foundation.h>
9
+ #import <React/RCTEventDispatcherProtocol.h>
10
+
11
+ @interface FocusedInputSelectionChangedEvent : NSObject <RCTEvent>
12
+
13
+ - (instancetype)initWithReactTag:(NSNumber *)reactTag event:(NSObject *)event;
14
+
15
+ @end