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.
- package/android/src/main/java/com/reactnativekeyboardcontroller/events/FocusedInputLayoutChangedEvent.kt +1 -1
- package/android/src/main/java/com/reactnativekeyboardcontroller/events/FocusedInputSelectionChangedEvent.kt +51 -0
- package/android/src/main/java/com/reactnativekeyboardcontroller/events/FocusedInputTextChangedEvent.kt +1 -1
- package/android/src/main/java/com/reactnativekeyboardcontroller/extensions/EditText.kt +79 -0
- package/android/src/main/java/com/reactnativekeyboardcontroller/extensions/ThemedReactContext.kt +1 -2
- package/android/src/main/java/com/reactnativekeyboardcontroller/extensions/View.kt +1 -1
- package/android/src/main/java/com/reactnativekeyboardcontroller/interactive/interpolators/IosInterpolator.kt +1 -1
- package/android/src/main/java/com/reactnativekeyboardcontroller/listeners/FocusedInputObserver.kt +32 -0
- package/android/src/main/java/com/reactnativekeyboardcontroller/listeners/KeyboardAnimationCallback.kt +1 -1
- package/android/src/main/java/com/reactnativekeyboardcontroller/managers/KeyboardControllerViewManagerImpl.kt +2 -0
- package/android/src/main/java/com/reactnativekeyboardcontroller/ui/FrameScheduler.kt +40 -0
- package/android/src/main/java/com/reactnativekeyboardcontroller/views/EdgeToEdgeReactViewGroup.kt +1 -1
- package/ios/KeyboardController-Bridging-Header.h +1 -0
- package/ios/core/KeyboardAnimation.swift +77 -0
- package/ios/core/SpringAnimation.swift +60 -0
- package/ios/core/UIUtils.swift +11 -0
- package/ios/delegates/KCTextInputCompositeDelegate.swift +148 -0
- package/ios/events/FocusedInputSelectionChangedEvent.h +15 -0
- package/ios/events/FocusedInputSelectionChangedEvent.m +76 -0
- package/ios/objc/RCTUITextView+DelegateManager.h +14 -0
- package/ios/objc/RCTUITextView+DelegateManager.m +26 -0
- package/ios/observers/FocusedInputObserver.swift +74 -13
- package/ios/observers/KeyboardMovementObserver.swift +39 -2
- package/ios/views/KeyboardControllerView.mm +45 -8
- package/ios/views/KeyboardControllerViewManager.mm +1 -0
- package/ios/views/KeyboardControllerViewManager.swift +10 -2
- package/lib/commonjs/animated.js +8 -0
- package/lib/commonjs/animated.js.map +1 -1
- package/lib/commonjs/components/KeyboardAvoidingView/index.js.map +1 -1
- package/lib/commonjs/components/KeyboardAwareScrollView/index.js +5 -5
- package/lib/commonjs/components/KeyboardAwareScrollView/index.js.map +1 -1
- package/lib/commonjs/components/KeyboardStickyView/index.js.map +1 -1
- package/lib/commonjs/components/KeyboardToolbar/index.js +25 -7
- package/lib/commonjs/components/KeyboardToolbar/index.js.map +1 -1
- package/lib/commonjs/components/KeyboardToolbar/types.js.map +1 -1
- package/lib/commonjs/components/index.js.map +1 -1
- package/lib/commonjs/index.js.map +1 -1
- package/lib/commonjs/reanimated.js +2 -1
- package/lib/commonjs/reanimated.js.map +1 -1
- package/lib/commonjs/reanimated.native.js +18 -1
- package/lib/commonjs/reanimated.native.js.map +1 -1
- package/lib/commonjs/specs/KeyboardControllerViewNativeComponent.js.map +1 -1
- package/lib/commonjs/types.js.map +1 -1
- package/lib/module/animated.js +9 -1
- package/lib/module/animated.js.map +1 -1
- package/lib/module/components/KeyboardAvoidingView/index.js.map +1 -1
- package/lib/module/components/KeyboardAwareScrollView/index.js +5 -5
- package/lib/module/components/KeyboardAwareScrollView/index.js.map +1 -1
- package/lib/module/components/KeyboardStickyView/index.js.map +1 -1
- package/lib/module/components/KeyboardToolbar/index.js +26 -8
- package/lib/module/components/KeyboardToolbar/index.js.map +1 -1
- package/lib/module/components/KeyboardToolbar/types.js.map +1 -1
- package/lib/module/components/index.js.map +1 -1
- package/lib/module/index.js.map +1 -1
- package/lib/module/reanimated.js +1 -0
- package/lib/module/reanimated.js.map +1 -1
- package/lib/module/reanimated.native.js +16 -0
- package/lib/module/reanimated.native.js.map +1 -1
- package/lib/module/specs/KeyboardControllerViewNativeComponent.js.map +1 -1
- package/lib/module/types.js.map +1 -1
- package/lib/typescript/components/KeyboardAvoidingView/index.d.ts +20 -0
- package/lib/typescript/components/KeyboardAwareScrollView/index.d.ts +8 -0
- package/lib/typescript/components/KeyboardStickyView/index.d.ts +15 -0
- package/lib/typescript/components/KeyboardToolbar/index.d.ts +21 -1
- package/lib/typescript/components/KeyboardToolbar/types.d.ts +3 -1
- package/lib/typescript/components/index.d.ts +3 -0
- package/lib/typescript/index.d.ts +1 -1
- package/lib/typescript/reanimated.d.ts +2 -1
- package/lib/typescript/reanimated.native.d.ts +7 -4
- package/lib/typescript/specs/KeyboardControllerViewNativeComponent.d.ts +16 -0
- package/lib/typescript/types.d.ts +21 -0
- package/package.json +4 -3
- package/src/animated.tsx +12 -0
- package/src/components/KeyboardAvoidingView/index.tsx +5 -2
- package/src/components/KeyboardAwareScrollView/index.tsx +81 -81
- package/src/components/KeyboardStickyView/index.tsx +1 -1
- package/src/components/KeyboardToolbar/index.tsx +47 -7
- package/src/components/KeyboardToolbar/types.ts +25 -1
- package/src/components/index.ts +3 -0
- package/src/index.ts +6 -1
- package/src/reanimated.native.ts +30 -3
- package/src/reanimated.ts +6 -0
- package/src/specs/KeyboardControllerViewNativeComponent.ts +17 -0
- package/src/types.ts +31 -0
- package/ios/KeyboardController.xcodeproj/project.pbxproj +0 -387
- 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<
|
|
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<
|
|
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
|
+
}
|
package/android/src/main/java/com/reactnativekeyboardcontroller/extensions/ThemedReactContext.kt
CHANGED
|
@@ -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?.
|
|
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
|
|
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
|
|
10
|
+
absoluteFingerPosition <= keyboardPosition || // user over scrolled keyboard
|
|
11
11
|
dy <= 0 // user scrolls up
|
|
12
12
|
) {
|
|
13
13
|
return dy
|
package/android/src/main/java/com/reactnativekeyboardcontroller/listeners/FocusedInputObserver.kt
CHANGED
|
@@ -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
|
|
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
|
+
}
|
package/android/src/main/java/com/reactnativekeyboardcontroller/views/EdgeToEdgeReactViewGroup.kt
CHANGED
|
@@ -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
|
|
35
|
+
// region View life cycles
|
|
36
36
|
override fun onAttachedToWindow() {
|
|
37
37
|
super.onAttachedToWindow()
|
|
38
38
|
|
|
@@ -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,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
|