react-native-keyboard-controller 1.15.2 → 1.16.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/fabric/java/com/reactnativekeyboardcontroller/KeyboardControllerViewManager.kt +6 -0
- package/android/src/fabric/java/com/reactnativekeyboardcontroller/KeyboardGestureAreaViewManager.kt +8 -0
- package/android/src/main/java/com/reactnativekeyboardcontroller/extensions/EditText.kt +77 -51
- package/android/src/main/java/com/reactnativekeyboardcontroller/listeners/FocusedInputObserver.kt +3 -3
- package/android/src/main/java/com/reactnativekeyboardcontroller/listeners/KeyboardAnimationCallback.kt +2 -2
- package/android/src/main/java/com/reactnativekeyboardcontroller/managers/KeyboardControllerViewManagerImpl.kt +7 -0
- package/android/src/main/java/com/reactnativekeyboardcontroller/views/EdgeToEdgeReactViewGroup.kt +9 -1
- package/android/src/paper/java/com/reactnativekeyboardcontroller/KeyboardControllerViewManager.kt +8 -0
- package/android/src/paper/java/com/reactnativekeyboardcontroller/KeyboardGestureAreaViewManager.kt +9 -0
- package/ios/animations/KeyboardAnimation.swift +4 -4
- package/ios/animations/SpringAnimation.swift +28 -5
- package/ios/animations/TimingAnimation.swift +77 -32
- package/ios/delegates/KCTextInputCompositeDelegate.swift +73 -1
- package/ios/extensions/Notification.swift +4 -0
- package/ios/extensions/UIResponder.swift +10 -0
- package/ios/interactive/InvisibleInputAccessoryView.swift +59 -0
- package/ios/interactive/KeyboardAreaExtender.swift +74 -0
- package/ios/interactive/KeyboardOffsetProvider.swift +39 -0
- package/ios/observers/FocusedInputObserver.swift +81 -25
- package/ios/observers/KeyboardEventsIgnorer.swift +39 -0
- package/ios/observers/KeyboardMovementObserver.swift +24 -10
- package/ios/protocols/TextInput.swift +2 -0
- package/ios/swizzling/UIResponderSwizzle.swift +60 -0
- package/ios/traversal/ViewHierarchyNavigator.swift +1 -1
- package/ios/views/KeyboardGestureAreaManager.h +31 -0
- package/ios/views/KeyboardGestureAreaManager.mm +174 -0
- package/lib/commonjs/animated.js +4 -1
- package/lib/commonjs/animated.js.map +1 -1
- package/lib/commonjs/bindings.native.js +1 -1
- package/lib/commonjs/bindings.native.js.map +1 -1
- package/lib/commonjs/specs/KeyboardControllerViewNativeComponent.js.map +1 -1
- package/lib/commonjs/specs/KeyboardGestureAreaNativeComponent.js +1 -3
- package/lib/commonjs/specs/KeyboardGestureAreaNativeComponent.js.map +1 -1
- package/lib/commonjs/types.js.map +1 -1
- package/lib/module/animated.js +4 -1
- package/lib/module/animated.js.map +1 -1
- package/lib/module/bindings.native.js +1 -1
- package/lib/module/bindings.native.js.map +1 -1
- package/lib/module/specs/KeyboardControllerViewNativeComponent.js.map +1 -1
- package/lib/module/specs/KeyboardGestureAreaNativeComponent.js +1 -3
- package/lib/module/specs/KeyboardGestureAreaNativeComponent.js.map +1 -1
- package/lib/module/types.js.map +1 -1
- package/lib/typescript/animated.d.ts +9 -1
- package/lib/typescript/specs/KeyboardControllerViewNativeComponent.d.ts +1 -0
- package/lib/typescript/specs/KeyboardGestureAreaNativeComponent.d.ts +1 -0
- package/lib/typescript/types.d.ts +6 -1
- package/package.json +1 -1
- package/src/animated.tsx +15 -1
- package/src/bindings.native.ts +1 -1
- package/src/specs/KeyboardControllerViewNativeComponent.ts +1 -0
- package/src/specs/KeyboardGestureAreaNativeComponent.ts +4 -3
- package/src/types.ts +6 -1
- package/android/src/main/java/com/reactnativekeyboardcontroller/ui/FrameScheduler.kt +0 -43
|
@@ -44,6 +44,16 @@ public extension Optional where Wrapped == UIResponder {
|
|
|
44
44
|
return (self as? UIView)?.superview?.reactTag ?? -1
|
|
45
45
|
#endif
|
|
46
46
|
}
|
|
47
|
+
|
|
48
|
+
var nativeID: String? {
|
|
49
|
+
guard let superview = (self as? UIView)?.superview else { return nil }
|
|
50
|
+
|
|
51
|
+
#if KEYBOARD_CONTROLLER_NEW_ARCH_ENABLED
|
|
52
|
+
return (superview as NSObject).value(forKey: "nativeId") as? String
|
|
53
|
+
#else
|
|
54
|
+
return superview.nativeID
|
|
55
|
+
#endif
|
|
56
|
+
}
|
|
47
57
|
}
|
|
48
58
|
|
|
49
59
|
public extension Optional where Wrapped: UIResponder {
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
//
|
|
2
|
+
// InvisibleInputAccessoryView.swift
|
|
3
|
+
// Pods
|
|
4
|
+
//
|
|
5
|
+
// Created by Kiryl Ziusko on 01/11/2024.
|
|
6
|
+
//
|
|
7
|
+
|
|
8
|
+
import Foundation
|
|
9
|
+
import UIKit
|
|
10
|
+
|
|
11
|
+
public class InvisibleInputAccessoryView: UIView {
|
|
12
|
+
var isShown = true
|
|
13
|
+
|
|
14
|
+
override init(frame: CGRect) {
|
|
15
|
+
super.init(frame: frame)
|
|
16
|
+
setupView()
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
public convenience init(height: CGFloat) {
|
|
20
|
+
self.init(frame: CGRect(x: 0, y: 0, width: 0, height: height))
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
required init?(coder aDecoder: NSCoder) {
|
|
24
|
+
super.init(coder: aDecoder)
|
|
25
|
+
setupView()
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
override public func point(inside _: CGPoint, with _: UIEvent?) -> Bool {
|
|
29
|
+
// Return false to allow touch events to pass through
|
|
30
|
+
return false
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
public func updateHeight(to newHeight: CGFloat) {
|
|
34
|
+
frame = CGRect(x: 0, y: 0, width: 0, height: newHeight)
|
|
35
|
+
|
|
36
|
+
// Invalidate intrinsic content size to trigger a layout update
|
|
37
|
+
invalidateIntrinsicContentSize()
|
|
38
|
+
layoutIfNeeded()
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
public func hide() {
|
|
42
|
+
guard isShown else { return }
|
|
43
|
+
isShown = false
|
|
44
|
+
updateHeight(to: 0.0)
|
|
45
|
+
superview?.layoutIfNeeded()
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
override public var intrinsicContentSize: CGSize {
|
|
49
|
+
return CGSize(width: UIView.noIntrinsicMetric, height: frame.height)
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
private func setupView() {
|
|
53
|
+
isUserInteractionEnabled = false
|
|
54
|
+
// for debug purposes
|
|
55
|
+
// backgroundColor = UIColor.red.withAlphaComponent(0.2)
|
|
56
|
+
backgroundColor = .clear
|
|
57
|
+
autoresizingMask = .flexibleHeight
|
|
58
|
+
}
|
|
59
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
//
|
|
2
|
+
// KeyboardAreaExtender.swift
|
|
3
|
+
// Pods
|
|
4
|
+
//
|
|
5
|
+
// Created by Kiryl Ziusko on 02/11/2024.
|
|
6
|
+
//
|
|
7
|
+
|
|
8
|
+
class KeyboardAreaExtender: NSObject {
|
|
9
|
+
private var currentInputAccessoryView: InvisibleInputAccessoryView?
|
|
10
|
+
|
|
11
|
+
@objc public static let shared = KeyboardAreaExtender()
|
|
12
|
+
|
|
13
|
+
override private init() {
|
|
14
|
+
super.init()
|
|
15
|
+
NotificationCenter.default.addObserver(
|
|
16
|
+
self,
|
|
17
|
+
selector: #selector(keyboardDidAppear),
|
|
18
|
+
name: UIResponder.keyboardDidShowNotification,
|
|
19
|
+
object: nil
|
|
20
|
+
)
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
deinit {
|
|
24
|
+
NotificationCenter.default.removeObserver(self)
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
public var offset: CGFloat {
|
|
28
|
+
return currentInputAccessoryView?.frame.height ?? 0
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
public func hide() {
|
|
32
|
+
if isVisible {
|
|
33
|
+
NotificationCenter.default.post(
|
|
34
|
+
name: .shouldIgnoreKeyboardEvents, object: nil, userInfo: ["ignore": true]
|
|
35
|
+
)
|
|
36
|
+
currentInputAccessoryView?.hide()
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
public func remove() {
|
|
41
|
+
currentInputAccessoryView = nil
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
public func updateHeight(to newHeight: CGFloat, for nativeID: String) {
|
|
45
|
+
if UIResponder.current.nativeID == nativeID {
|
|
46
|
+
currentInputAccessoryView?.updateHeight(to: newHeight)
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
private var isVisible: Bool {
|
|
51
|
+
return currentInputAccessoryView?.isShown ?? false
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
@objc private func keyboardDidAppear(_: Notification) {
|
|
55
|
+
let responder = UIResponder.current
|
|
56
|
+
if let activeTextInput = responder as? TextInput,
|
|
57
|
+
let offset = KeyboardOffsetProvider.shared.getOffset(
|
|
58
|
+
forTextInputNativeID: responder.nativeID),
|
|
59
|
+
responder?.inputAccessoryView == nil
|
|
60
|
+
{
|
|
61
|
+
currentInputAccessoryView = InvisibleInputAccessoryView(height: CGFloat(truncating: offset))
|
|
62
|
+
|
|
63
|
+
// we need to send this event before we actually attach the IAV
|
|
64
|
+
// since on some OS versions (iOS 15 for example), `reloadInputViews`
|
|
65
|
+
// will trigger `keyboardDidAppear` listener immediately
|
|
66
|
+
NotificationCenter.default.post(
|
|
67
|
+
name: .shouldIgnoreKeyboardEvents, object: nil, userInfo: ["ignore": true]
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
activeTextInput.inputAccessoryView = currentInputAccessoryView
|
|
71
|
+
activeTextInput.reloadInputViews()
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
//
|
|
2
|
+
// KeyboardOffsetProvider.swift
|
|
3
|
+
// Pods
|
|
4
|
+
//
|
|
5
|
+
// Created by Kiryl Ziusko on 01/11/2024.
|
|
6
|
+
//
|
|
7
|
+
|
|
8
|
+
import Foundation
|
|
9
|
+
|
|
10
|
+
@objc(KeyboardOffsetProvider)
|
|
11
|
+
public class KeyboardOffsetProvider: NSObject {
|
|
12
|
+
@objc public static let shared = KeyboardOffsetProvider()
|
|
13
|
+
|
|
14
|
+
override private init() {}
|
|
15
|
+
|
|
16
|
+
private var offsetMap: [String: (offset: NSNumber, tag: NSNumber)] = [:]
|
|
17
|
+
|
|
18
|
+
@objc public func setOffset(forTextInputNativeID nativeID: String, offset: NSNumber, withTag tag: NSNumber) {
|
|
19
|
+
KeyboardAreaExtender.shared.updateHeight(to: CGFloat(offset), for: nativeID)
|
|
20
|
+
offsetMap[nativeID] = (offset: offset, tag: tag)
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
@objc public func getOffset(forTextInputNativeID nativeID: String?) -> NSNumber? {
|
|
24
|
+
guard let unwrappedNativeID = nativeID, let pair = offsetMap[unwrappedNativeID] else { return nil }
|
|
25
|
+
return pair.offset
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
@objc public func hasOffset(forTextInputNativeID nativeID: String?) -> Bool {
|
|
29
|
+
guard let unwrappedNativeID = nativeID else { return false }
|
|
30
|
+
return offsetMap[unwrappedNativeID] != nil
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
@objc public func removeOffset(forTextInputNativeID nativeID: String, withTag tag: NSNumber) {
|
|
34
|
+
// Ensure the tag matches before removing the entry
|
|
35
|
+
if let currentPair = offsetMap[nativeID], currentPair.tag == tag {
|
|
36
|
+
offsetMap.removeValue(forKey: nativeID)
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
@@ -37,7 +37,7 @@ public class FocusedInputObserver: NSObject {
|
|
|
37
37
|
// input tracking
|
|
38
38
|
private var currentInput: UIView?
|
|
39
39
|
private var currentResponder: UIView?
|
|
40
|
-
private var
|
|
40
|
+
private var observation: NSKeyValueObservation?
|
|
41
41
|
private var lastEventDispatched: [AnyHashable: Any] = noFocusedInputEvent
|
|
42
42
|
|
|
43
43
|
@objc public init(
|
|
@@ -76,7 +76,7 @@ public class FocusedInputObserver: NSObject {
|
|
|
76
76
|
|
|
77
77
|
NotificationCenter.default.addObserver(
|
|
78
78
|
self,
|
|
79
|
-
selector: #selector(
|
|
79
|
+
selector: #selector(didReceiveFocus),
|
|
80
80
|
name: UIResponder.keyboardWillShowNotification,
|
|
81
81
|
object: nil
|
|
82
82
|
)
|
|
@@ -86,6 +86,30 @@ public class FocusedInputObserver: NSObject {
|
|
|
86
86
|
name: UIResponder.keyboardWillHideNotification,
|
|
87
87
|
object: nil
|
|
88
88
|
)
|
|
89
|
+
NotificationCenter.default.addObserver(
|
|
90
|
+
self,
|
|
91
|
+
selector: #selector(didReceiveFocus),
|
|
92
|
+
name: UITextField.textDidBeginEditingNotification,
|
|
93
|
+
object: nil
|
|
94
|
+
)
|
|
95
|
+
NotificationCenter.default.addObserver(
|
|
96
|
+
self,
|
|
97
|
+
selector: #selector(didReceiveFocus),
|
|
98
|
+
name: UITextView.textDidBeginEditingNotification,
|
|
99
|
+
object: nil
|
|
100
|
+
)
|
|
101
|
+
NotificationCenter.default.addObserver(
|
|
102
|
+
self,
|
|
103
|
+
selector: #selector(didReceiveBlur),
|
|
104
|
+
name: UITextField.textDidEndEditingNotification,
|
|
105
|
+
object: nil
|
|
106
|
+
)
|
|
107
|
+
NotificationCenter.default.addObserver(
|
|
108
|
+
self,
|
|
109
|
+
selector: #selector(didReceiveBlur),
|
|
110
|
+
name: UITextView.textDidEndEditingNotification,
|
|
111
|
+
object: nil
|
|
112
|
+
)
|
|
89
113
|
}
|
|
90
114
|
|
|
91
115
|
@objc public func unmount() {
|
|
@@ -94,7 +118,41 @@ public class FocusedInputObserver: NSObject {
|
|
|
94
118
|
NotificationCenter.default.removeObserver(self)
|
|
95
119
|
}
|
|
96
120
|
|
|
97
|
-
@objc func
|
|
121
|
+
@objc func keyboardWillHide(_: Notification) {
|
|
122
|
+
onBlur()
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
@objc func didReceiveFocus(_: Notification) {
|
|
126
|
+
if UIResponder.current == currentResponder {
|
|
127
|
+
// focus was already handled by keyboard event
|
|
128
|
+
return
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
onFocus()
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* We handle blur events in `keyboardWillHide` handler
|
|
136
|
+
* And this additional handler is needed only to have
|
|
137
|
+
* a consistent state when keyboard is not shown
|
|
138
|
+
*/
|
|
139
|
+
@objc func didReceiveBlur(_: Notification) {
|
|
140
|
+
if currentResponder == nil {
|
|
141
|
+
// blur was already handled by keyboard event
|
|
142
|
+
return
|
|
143
|
+
}
|
|
144
|
+
// blur gets triggered on endEditing, so we check if no upcoming
|
|
145
|
+
// didReceiveFocus events are coming to exclude `noFocusedInput`
|
|
146
|
+
// event when user switches between inputs
|
|
147
|
+
DispatchQueue.main.async {
|
|
148
|
+
// check that it wasn't a switch between inputs
|
|
149
|
+
if self.currentResponder == nil {
|
|
150
|
+
self.onBlur()
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
func onFocus() {
|
|
98
156
|
guard let responder = UIResponder.current as? UIView else { return }
|
|
99
157
|
removeObservers(newResponder: responder)
|
|
100
158
|
currentResponder = responder
|
|
@@ -106,7 +164,7 @@ public class FocusedInputObserver: NSObject {
|
|
|
106
164
|
FocusedInputHolder.shared.set(currentResponder as? TextInput)
|
|
107
165
|
|
|
108
166
|
let allInputFields = ViewHierarchyNavigator.getAllInputFields()
|
|
109
|
-
let currentIndex = allInputFields.firstIndex(where: { $0
|
|
167
|
+
let currentIndex = allInputFields.firstIndex(where: { $0 == currentResponder }) ?? -1
|
|
110
168
|
|
|
111
169
|
onFocusDidSet([
|
|
112
170
|
"current": currentIndex,
|
|
@@ -114,7 +172,11 @@ public class FocusedInputObserver: NSObject {
|
|
|
114
172
|
])
|
|
115
173
|
}
|
|
116
174
|
|
|
117
|
-
|
|
175
|
+
func onBlur() {
|
|
176
|
+
// when we switch to next input, but `showSoftInput={false}`
|
|
177
|
+
if UIResponder.current is TextInput {
|
|
178
|
+
return
|
|
179
|
+
}
|
|
118
180
|
removeObservers(newResponder: nil)
|
|
119
181
|
currentInput = nil
|
|
120
182
|
currentResponder = nil
|
|
@@ -162,25 +224,27 @@ public class FocusedInputObserver: NSObject {
|
|
|
162
224
|
}
|
|
163
225
|
|
|
164
226
|
private func setupObservers() {
|
|
165
|
-
if
|
|
227
|
+
if observation != nil {
|
|
166
228
|
return
|
|
167
229
|
}
|
|
168
230
|
|
|
169
231
|
if currentInput != nil {
|
|
170
|
-
|
|
171
|
-
|
|
232
|
+
observation = currentInput?.observe(\.center, options: .new) { [weak self] view, change in
|
|
233
|
+
guard let self = self else { return }
|
|
234
|
+
self.onLayoutChange(view: view, change: change)
|
|
235
|
+
}
|
|
172
236
|
|
|
173
237
|
substituteDelegate(currentResponder)
|
|
174
238
|
}
|
|
175
239
|
}
|
|
176
240
|
|
|
177
241
|
private func removeObservers(newResponder: UIResponder?) {
|
|
178
|
-
if newResponder == currentResponder ||
|
|
242
|
+
if newResponder == currentResponder || observation == nil {
|
|
179
243
|
return
|
|
180
244
|
}
|
|
181
245
|
|
|
182
|
-
|
|
183
|
-
|
|
246
|
+
observation?.invalidate()
|
|
247
|
+
observation = nil
|
|
184
248
|
|
|
185
249
|
substituteDelegateBack(currentResponder)
|
|
186
250
|
}
|
|
@@ -188,7 +252,7 @@ public class FocusedInputObserver: NSObject {
|
|
|
188
252
|
private func substituteDelegate(_ input: UIResponder?) {
|
|
189
253
|
if let textField = input as? UITextField {
|
|
190
254
|
if !(textField.delegate is KCTextInputCompositeDelegate) {
|
|
191
|
-
delegate.setTextFieldDelegate(delegate: textField.delegate)
|
|
255
|
+
delegate.setTextFieldDelegate(delegate: textField.delegate, textField: textField)
|
|
192
256
|
textField.delegate = delegate
|
|
193
257
|
}
|
|
194
258
|
} else if let textView = input as? UITextView {
|
|
@@ -211,19 +275,11 @@ public class FocusedInputObserver: NSObject {
|
|
|
211
275
|
}
|
|
212
276
|
}
|
|
213
277
|
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
context _: UnsafeMutableRawPointer?
|
|
220
|
-
) {
|
|
221
|
-
if keyPath == "center", object as? NSObject == currentInput {
|
|
222
|
-
// we need to read layout in next frame, otherwise we'll get old
|
|
223
|
-
// layout values
|
|
224
|
-
DispatchQueue.main.async {
|
|
225
|
-
self.syncUpLayout()
|
|
226
|
-
}
|
|
278
|
+
private func onLayoutChange(view _: UIView, change _: NSKeyValueObservedChange<CGPoint>) {
|
|
279
|
+
// we need to read layout in next frame, otherwise we'll get old
|
|
280
|
+
// layout values
|
|
281
|
+
DispatchQueue.main.async {
|
|
282
|
+
self.syncUpLayout()
|
|
227
283
|
}
|
|
228
284
|
}
|
|
229
285
|
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
//
|
|
2
|
+
// KeyboardEventsIgnorer.swift
|
|
3
|
+
// Pods
|
|
4
|
+
//
|
|
5
|
+
// Created by Kiryl Ziusko on 24/11/2024.
|
|
6
|
+
//
|
|
7
|
+
|
|
8
|
+
import Foundation
|
|
9
|
+
|
|
10
|
+
@objc(KeyboardEventsIgnorer)
|
|
11
|
+
public class KeyboardEventsIgnorer: NSObject {
|
|
12
|
+
@objc public static let shared = KeyboardEventsIgnorer()
|
|
13
|
+
|
|
14
|
+
var shouldIgnoreKeyboardEvents = false
|
|
15
|
+
|
|
16
|
+
public var shouldIgnore: Bool {
|
|
17
|
+
return shouldIgnoreKeyboardEvents
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
override init() {
|
|
21
|
+
super.init()
|
|
22
|
+
NotificationCenter.default.addObserver(
|
|
23
|
+
self,
|
|
24
|
+
selector: #selector(handleIgnoreKeyboardEventsNotification),
|
|
25
|
+
name: .shouldIgnoreKeyboardEvents,
|
|
26
|
+
object: nil
|
|
27
|
+
)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
@objc private func handleIgnoreKeyboardEventsNotification(_ notification: Notification) {
|
|
31
|
+
if let userInfo = notification.userInfo, let value = userInfo["ignore"] as? Bool {
|
|
32
|
+
shouldIgnoreKeyboardEvents = value
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
deinit {
|
|
37
|
+
NotificationCenter.default.removeObserver(self)
|
|
38
|
+
}
|
|
39
|
+
}
|
|
@@ -36,7 +36,12 @@ public class KeyboardMovementObserver: NSObject {
|
|
|
36
36
|
private var hasKVObserver = false
|
|
37
37
|
private var isMounted = false
|
|
38
38
|
// state variables
|
|
39
|
-
private var
|
|
39
|
+
private var _keyboardHeight: CGFloat = 0.0
|
|
40
|
+
private var keyboardHeight: CGFloat {
|
|
41
|
+
get { _keyboardHeight - KeyboardAreaExtender.shared.offset }
|
|
42
|
+
set { _keyboardHeight = newValue }
|
|
43
|
+
}
|
|
44
|
+
|
|
40
45
|
private var duration = 0
|
|
41
46
|
private var tag: NSNumber = -1
|
|
42
47
|
private var animation: KeyboardAnimation?
|
|
@@ -121,7 +126,7 @@ public class KeyboardMovementObserver: NSObject {
|
|
|
121
126
|
}
|
|
122
127
|
// if keyboard height is not equal to its bounds - we can ignore
|
|
123
128
|
// values, since they'll be invalid and will cause UI jumps
|
|
124
|
-
if keyboardView?.bounds.size.height !=
|
|
129
|
+
if floor(keyboardView?.bounds.size.height ?? 0) != floor(_keyboardHeight) {
|
|
125
130
|
return
|
|
126
131
|
}
|
|
127
132
|
|
|
@@ -131,11 +136,12 @@ public class KeyboardMovementObserver: NSObject {
|
|
|
131
136
|
let keyboardFrameY = changeValue.cgPointValue.y
|
|
132
137
|
let keyboardWindowH = keyboardView?.window?.bounds.size.height ?? 0
|
|
133
138
|
let keyboardPosition = keyboardWindowH - keyboardFrameY
|
|
139
|
+
|
|
134
140
|
let position = CGFloat.interpolate(
|
|
135
|
-
inputRange: [
|
|
136
|
-
outputRange: [
|
|
141
|
+
inputRange: [_keyboardHeight / 2, -_keyboardHeight / 2],
|
|
142
|
+
outputRange: [_keyboardHeight, 0],
|
|
137
143
|
currentValue: keyboardPosition
|
|
138
|
-
)
|
|
144
|
+
) - KeyboardAreaExtender.shared.offset
|
|
139
145
|
|
|
140
146
|
if position == 0 {
|
|
141
147
|
// it will be triggered before `keyboardWillDisappear` and
|
|
@@ -163,6 +169,8 @@ public class KeyboardMovementObserver: NSObject {
|
|
|
163
169
|
}
|
|
164
170
|
|
|
165
171
|
@objc func keyboardWillAppear(_ notification: Notification) {
|
|
172
|
+
guard !KeyboardEventsIgnorer.shared.shouldIgnore else { return }
|
|
173
|
+
|
|
166
174
|
let (duration, frame) = notification.keyboardMetaData()
|
|
167
175
|
if let keyboardFrame = frame {
|
|
168
176
|
tag = UIResponder.current.reactViewTag
|
|
@@ -172,11 +180,11 @@ public class KeyboardMovementObserver: NSObject {
|
|
|
172
180
|
didShowDeadline = Date.currentTimeStamp + Int64(duration)
|
|
173
181
|
|
|
174
182
|
onRequestAnimation()
|
|
175
|
-
onEvent("onKeyboardMoveStart", Float(keyboardHeight) as NSNumber, 1, duration as NSNumber, tag)
|
|
176
|
-
onNotify("KeyboardController::keyboardWillShow", buildEventParams(keyboardHeight, duration, tag))
|
|
183
|
+
onEvent("onKeyboardMoveStart", Float(self.keyboardHeight) as NSNumber, 1, duration as NSNumber, tag)
|
|
184
|
+
onNotify("KeyboardController::keyboardWillShow", buildEventParams(self.keyboardHeight, duration, tag))
|
|
177
185
|
|
|
178
186
|
setupKeyboardWatcher()
|
|
179
|
-
initializeAnimation(fromValue: prevKeyboardPosition, toValue: keyboardHeight)
|
|
187
|
+
initializeAnimation(fromValue: prevKeyboardPosition, toValue: self.keyboardHeight)
|
|
180
188
|
}
|
|
181
189
|
}
|
|
182
190
|
|
|
@@ -202,9 +210,15 @@ public class KeyboardMovementObserver: NSObject {
|
|
|
202
210
|
let keyboardHeight = keyboardFrame.cgRectValue.size.height
|
|
203
211
|
tag = UIResponder.current.reactViewTag
|
|
204
212
|
self.keyboardHeight = keyboardHeight
|
|
213
|
+
|
|
214
|
+
guard !KeyboardEventsIgnorer.shared.shouldIgnore else {
|
|
215
|
+
KeyboardEventsIgnorer.shared.shouldIgnoreKeyboardEvents = false
|
|
216
|
+
return
|
|
217
|
+
}
|
|
218
|
+
|
|
205
219
|
// if the event is caught in between it's highly likely that it could be a "resize" event
|
|
206
220
|
// so we just read actual keyboard frame value in this case
|
|
207
|
-
let height = timestamp >= didShowDeadline ? keyboardHeight : position
|
|
221
|
+
let height = timestamp >= didShowDeadline ? self.keyboardHeight : position - KeyboardAreaExtender.shared.offset
|
|
208
222
|
// always limit progress to the maximum possible value
|
|
209
223
|
let progress = min(height / self.keyboardHeight, 1.0)
|
|
210
224
|
|
|
@@ -267,7 +281,7 @@ public class KeyboardMovementObserver: NSObject {
|
|
|
267
281
|
}
|
|
268
282
|
|
|
269
283
|
let (visibleKeyboardHeight, keyboardFrameY) = keyboardView.frameTransitionInWindow
|
|
270
|
-
var keyboardPosition = visibleKeyboardHeight
|
|
284
|
+
var keyboardPosition = visibleKeyboardHeight - KeyboardAreaExtender.shared.offset
|
|
271
285
|
|
|
272
286
|
if keyboardPosition == prevKeyboardPosition || keyboardFrameY == 0 {
|
|
273
287
|
return
|
|
@@ -11,9 +11,11 @@ import UIKit
|
|
|
11
11
|
|
|
12
12
|
public protocol TextInput: UIView {
|
|
13
13
|
// default common methods/properties
|
|
14
|
+
var inputAccessoryView: UIView? { get set }
|
|
14
15
|
var inputView: UIView? { get set }
|
|
15
16
|
var keyboardType: UIKeyboardType { get }
|
|
16
17
|
var keyboardAppearance: UIKeyboardAppearance { get }
|
|
18
|
+
// custom methods/properties
|
|
17
19
|
func focus()
|
|
18
20
|
}
|
|
19
21
|
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
//
|
|
2
|
+
// UIResponderSwizzle.swift
|
|
3
|
+
// Pods
|
|
4
|
+
//
|
|
5
|
+
// Created by Kiryl Ziusko on 01/11/2024.
|
|
6
|
+
//
|
|
7
|
+
|
|
8
|
+
import Foundation
|
|
9
|
+
import UIKit
|
|
10
|
+
|
|
11
|
+
private var originalResignFirstResponder: IMP?
|
|
12
|
+
|
|
13
|
+
@objc
|
|
14
|
+
extension UIResponder {
|
|
15
|
+
public static func swizzleResignFirstResponder() {
|
|
16
|
+
let originalSelector = #selector(resignFirstResponder)
|
|
17
|
+
|
|
18
|
+
guard let originalMethod = class_getInstanceMethod(UIResponder.self, originalSelector) else {
|
|
19
|
+
return
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
originalResignFirstResponder = method_getImplementation(originalMethod)
|
|
23
|
+
|
|
24
|
+
let swizzledImplementation: @convention(block) (UIResponder) -> Bool = { (self) in
|
|
25
|
+
// Check the type of responder
|
|
26
|
+
if let textField = self as? TextInput {
|
|
27
|
+
// check inputAccessoryView and call original method immediately if not InvisibleInputAccessoryView
|
|
28
|
+
if !(textField.inputAccessoryView is InvisibleInputAccessoryView) {
|
|
29
|
+
return self.callOriginalResignFirstResponder(originalSelector)
|
|
30
|
+
}
|
|
31
|
+
} else {
|
|
32
|
+
// If casting to TextInput fails
|
|
33
|
+
return self.callOriginalResignFirstResponder(originalSelector)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
KeyboardAreaExtender.shared.hide()
|
|
37
|
+
|
|
38
|
+
// Postpone execution of the original resignFirstResponder
|
|
39
|
+
DispatchQueue.main.asyncAfter(deadline: .now() + UIUtils.nextFrame) {
|
|
40
|
+
(self as? TextInput)?.inputAccessoryView = nil
|
|
41
|
+
KeyboardAreaExtender.shared.remove()
|
|
42
|
+
_ = self.callOriginalResignFirstResponder(originalSelector)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// We need to return a value immediately, even though the actual action is delayed
|
|
46
|
+
return false
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
let implementation = imp_implementationWithBlock(swizzledImplementation)
|
|
50
|
+
method_setImplementation(originalMethod, implementation)
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
private func callOriginalResignFirstResponder(_ selector: Selector) -> Bool {
|
|
54
|
+
guard let originalResignFirstResponder = originalResignFirstResponder else { return false }
|
|
55
|
+
typealias Function = @convention(c) (AnyObject, Selector) -> Bool
|
|
56
|
+
let castOriginalResignFirstResponder = unsafeBitCast(originalResignFirstResponder, to: Function.self)
|
|
57
|
+
let result = castOriginalResignFirstResponder(self, selector)
|
|
58
|
+
return result
|
|
59
|
+
}
|
|
60
|
+
}
|
|
@@ -21,7 +21,7 @@ public class ViewHierarchyNavigator: NSObject {
|
|
|
21
21
|
return
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
-
let input =
|
|
24
|
+
let input = FocusedInputHolder.shared.get() ?? (UIResponder.current as? UIView) ?? nil
|
|
25
25
|
guard let view = input else { return }
|
|
26
26
|
|
|
27
27
|
let textField = findTextInputInDirection(currentFocus: view, direction: direction)
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
//
|
|
2
|
+
// KeyboardGestureAreaManager.h
|
|
3
|
+
// Pods
|
|
4
|
+
//
|
|
5
|
+
// Created by Kiryl Ziusko on 01/11/2024.
|
|
6
|
+
//
|
|
7
|
+
|
|
8
|
+
#ifdef RCT_NEW_ARCH_ENABLED
|
|
9
|
+
#import <React/RCTViewComponentView.h>
|
|
10
|
+
#else
|
|
11
|
+
#import <React/RCTBridge.h>
|
|
12
|
+
#endif
|
|
13
|
+
#import <React/RCTViewManager.h>
|
|
14
|
+
#import <UIKit/UIKit.h>
|
|
15
|
+
|
|
16
|
+
@interface KeyboardGestureAreaManager : RCTViewManager
|
|
17
|
+
@end
|
|
18
|
+
|
|
19
|
+
@interface KeyboardGestureArea :
|
|
20
|
+
#ifdef RCT_NEW_ARCH_ENABLED
|
|
21
|
+
RCTViewComponentView
|
|
22
|
+
#else
|
|
23
|
+
UIView
|
|
24
|
+
|
|
25
|
+
- (instancetype)initWithBridge:(RCTBridge *)bridge;
|
|
26
|
+
|
|
27
|
+
#endif
|
|
28
|
+
|
|
29
|
+
@property (nonatomic, strong) NSNumber *offset;
|
|
30
|
+
@property (nonatomic, strong) NSString *textInputNativeID;
|
|
31
|
+
@end
|