react-native-keyboard-controller 1.18.2 → 1.18.4
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/modules/statusbar/StatusBarManagerCompatModuleImpl.kt +2 -3
- package/android/src/main/java/com/reactnativekeyboardcontroller/views/EdgeToEdgeReactViewGroup.kt +12 -5
- package/ios/observers/movement/KeyboardTrackingView.swift +139 -0
- package/ios/observers/movement/KeyboardViewLocator.swift +23 -0
- package/ios/observers/movement/observer/KeyboardMovementObserver+Animation.swift +21 -0
- package/ios/observers/movement/observer/KeyboardMovementObserver+Interactive.swift +56 -0
- package/ios/observers/movement/observer/KeyboardMovementObserver+Lifecycle.swift +47 -0
- package/ios/observers/movement/observer/KeyboardMovementObserver+Listeners.swift +88 -0
- package/ios/observers/movement/observer/KeyboardMovementObserver+Watcher.swift +72 -0
- package/ios/observers/movement/observer/KeyboardMovementObserver.swift +63 -0
- package/ios/views/KeyboardControllerView.mm +10 -10
- package/ios/views/KeyboardExtenderContainerView.swift +119 -42
- package/ios/views/KeyboardExtenderManager.mm +12 -9
- package/lib/commonjs/components/KeyboardAwareScrollView/index.js +0 -1
- package/lib/commonjs/components/KeyboardAwareScrollView/index.js.map +1 -1
- package/lib/commonjs/types/views.js.map +1 -1
- package/lib/module/components/KeyboardAwareScrollView/index.js +0 -1
- package/lib/module/components/KeyboardAwareScrollView/index.js.map +1 -1
- package/lib/module/types/views.js.map +1 -1
- package/lib/typescript/types/views.d.ts +1 -1
- package/package.json +1 -1
- package/src/components/KeyboardAwareScrollView/index.tsx +0 -2
- package/src/types/views.ts +1 -1
- package/ios/observers/KeyboardMovementObserver.swift +0 -325
- /package/ios/observers/{KeyboardEventsIgnorer.swift → movement/KeyboardEventsIgnorer.swift} +0 -0
|
@@ -9,9 +9,9 @@ import androidx.core.view.WindowInsetsCompat
|
|
|
9
9
|
import androidx.core.view.WindowInsetsControllerCompat
|
|
10
10
|
import com.facebook.react.bridge.ReactApplicationContext
|
|
11
11
|
import com.facebook.react.bridge.UiThreadUtil
|
|
12
|
-
import com.reactnativekeyboardcontroller.extensions.rootView
|
|
13
12
|
import com.reactnativekeyboardcontroller.log.Logger
|
|
14
13
|
import com.reactnativekeyboardcontroller.views.EdgeToEdgeReactViewGroup
|
|
14
|
+
import com.reactnativekeyboardcontroller.views.EdgeToEdgeViewRegistry
|
|
15
15
|
import java.lang.ref.WeakReference
|
|
16
16
|
|
|
17
17
|
private val TAG = StatusBarManagerCompatModuleImpl::class.qualifiedName
|
|
@@ -117,8 +117,7 @@ class StatusBarManagerCompatModuleImpl(
|
|
|
117
117
|
|
|
118
118
|
private fun isEnabled(): Boolean = view()?.active ?: false
|
|
119
119
|
|
|
120
|
-
private fun view(): EdgeToEdgeReactViewGroup? =
|
|
121
|
-
mReactContext.rootView?.findViewWithTag<EdgeToEdgeReactViewGroup>(EdgeToEdgeReactViewGroup.VIEW_TAG)
|
|
120
|
+
private fun view(): EdgeToEdgeReactViewGroup? = EdgeToEdgeViewRegistry.get()
|
|
122
121
|
|
|
123
122
|
companion object {
|
|
124
123
|
const val NAME = "StatusBarManager"
|
package/android/src/main/java/com/reactnativekeyboardcontroller/views/EdgeToEdgeReactViewGroup.kt
CHANGED
|
@@ -19,9 +19,20 @@ import com.reactnativekeyboardcontroller.listeners.KeyboardAnimationCallback
|
|
|
19
19
|
import com.reactnativekeyboardcontroller.listeners.KeyboardAnimationCallbackConfig
|
|
20
20
|
import com.reactnativekeyboardcontroller.log.Logger
|
|
21
21
|
import com.reactnativekeyboardcontroller.modal.ModalAttachedWatcher
|
|
22
|
+
import java.lang.ref.WeakReference
|
|
22
23
|
|
|
23
24
|
private val TAG = EdgeToEdgeReactViewGroup::class.qualifiedName
|
|
24
25
|
|
|
26
|
+
object EdgeToEdgeViewRegistry {
|
|
27
|
+
private var lastCreatedView: WeakReference<EdgeToEdgeReactViewGroup>? = null
|
|
28
|
+
|
|
29
|
+
fun register(view: EdgeToEdgeReactViewGroup) {
|
|
30
|
+
lastCreatedView = WeakReference(view)
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
fun get(): EdgeToEdgeReactViewGroup? = lastCreatedView?.get()
|
|
34
|
+
}
|
|
35
|
+
|
|
25
36
|
@Suppress("detekt:TooManyFunctions")
|
|
26
37
|
@SuppressLint("ViewConstructor")
|
|
27
38
|
class EdgeToEdgeReactViewGroup(
|
|
@@ -58,7 +69,7 @@ class EdgeToEdgeReactViewGroup(
|
|
|
58
69
|
private val modalAttachedWatcher = ModalAttachedWatcher(this, reactContext, config, ::getKeyboardCallback)
|
|
59
70
|
|
|
60
71
|
init {
|
|
61
|
-
|
|
72
|
+
EdgeToEdgeViewRegistry.register(this)
|
|
62
73
|
}
|
|
63
74
|
|
|
64
75
|
// region View life cycles
|
|
@@ -241,8 +252,4 @@ class EdgeToEdgeReactViewGroup(
|
|
|
241
252
|
}
|
|
242
253
|
}
|
|
243
254
|
// endregion
|
|
244
|
-
|
|
245
|
-
companion object {
|
|
246
|
-
val VIEW_TAG = EdgeToEdgeReactViewGroup::class.simpleName
|
|
247
|
-
}
|
|
248
255
|
}
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
//
|
|
2
|
+
// KeyboardTrackingView.swift
|
|
3
|
+
// Pods
|
|
4
|
+
//
|
|
5
|
+
// Created by Kiryl Ziusko on 25/07/2025.
|
|
6
|
+
//
|
|
7
|
+
|
|
8
|
+
import UIKit
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* A compatibility view that resolves to `KeyboardView` on iOS < 26
|
|
12
|
+
* and uses `keyboardLayoutGuide` on iOS 26+.
|
|
13
|
+
*/
|
|
14
|
+
final class KeyboardTrackingView: UIView {
|
|
15
|
+
private var keyboardView: UIView? { KeyboardViewLocator.shared.resolve() }
|
|
16
|
+
private var keyboardHeight = 0.0
|
|
17
|
+
|
|
18
|
+
static let invalidPosition: CGFloat = -.greatestFiniteMagnitude
|
|
19
|
+
|
|
20
|
+
override init(frame: CGRect) {
|
|
21
|
+
super.init(frame: frame)
|
|
22
|
+
setup()
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
required init?(coder: NSCoder) {
|
|
26
|
+
super.init(coder: coder)
|
|
27
|
+
setup()
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
required init() {
|
|
31
|
+
super.init(frame: .zero)
|
|
32
|
+
setup()
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
deinit {
|
|
36
|
+
NotificationCenter.default.removeObserver(self)
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
private func setup() {
|
|
40
|
+
// for debug purposes
|
|
41
|
+
// self.backgroundColor = .red
|
|
42
|
+
isUserInteractionEnabled = false
|
|
43
|
+
isHidden = true
|
|
44
|
+
|
|
45
|
+
NotificationCenter.default.addObserver(
|
|
46
|
+
self,
|
|
47
|
+
selector: #selector(keyboardWillAppear),
|
|
48
|
+
name: UIResponder.keyboardWillShowNotification,
|
|
49
|
+
object: nil
|
|
50
|
+
)
|
|
51
|
+
NotificationCenter.default.addObserver(
|
|
52
|
+
self,
|
|
53
|
+
selector: #selector(keyboardDidAppear),
|
|
54
|
+
name: UIResponder.keyboardDidShowNotification,
|
|
55
|
+
object: nil
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
guard
|
|
59
|
+
let window = UIApplication.shared.activeWindow,
|
|
60
|
+
let rootView = window.rootViewController?.view
|
|
61
|
+
else {
|
|
62
|
+
return
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
rootView.addSubview(self)
|
|
66
|
+
|
|
67
|
+
translatesAutoresizingMaskIntoConstraints = false
|
|
68
|
+
|
|
69
|
+
if #available(iOS 15.0, *) {
|
|
70
|
+
if #available(iOS 17.0, *) {
|
|
71
|
+
rootView.keyboardLayoutGuide.usesBottomSafeArea = false
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
NSLayoutConstraint.activate([
|
|
75
|
+
leadingAnchor.constraint(equalTo: rootView.leadingAnchor, constant: 0),
|
|
76
|
+
trailingAnchor.constraint(equalTo: rootView.trailingAnchor, constant: 0),
|
|
77
|
+
bottomAnchor.constraint(equalTo: rootView.keyboardLayoutGuide.topAnchor, constant: 0),
|
|
78
|
+
heightAnchor.constraint(equalToConstant: 0),
|
|
79
|
+
])
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
@objc private func keyboardWillAppear(_ notification: Notification) {
|
|
84
|
+
updateHeightFromNotification(notification)
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
@objc private func keyboardDidAppear(_ notification: Notification) {
|
|
88
|
+
updateHeightFromNotification(notification)
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
private func updateHeightFromNotification(_ notification: Notification) {
|
|
92
|
+
let (_, frame) = notification.keyboardMetaData()
|
|
93
|
+
if let keyboardFrame = frame {
|
|
94
|
+
let keyboardHeight = keyboardFrame.cgRectValue.size.height
|
|
95
|
+
self.keyboardHeight = keyboardHeight
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
@objc var view: UIView? {
|
|
100
|
+
if #available(iOS 26.0, *) {
|
|
101
|
+
return self
|
|
102
|
+
} else {
|
|
103
|
+
return keyboardView
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
func interactive(point: CGPoint) -> CGFloat {
|
|
108
|
+
guard let trackedView = view else { return Self.invalidPosition }
|
|
109
|
+
|
|
110
|
+
let keyboardFrameY = point.y
|
|
111
|
+
let keyboardWindowH = trackedView.window?.bounds.size.height ?? 0
|
|
112
|
+
let keyboardPosition = keyboardWindowH - keyboardFrameY
|
|
113
|
+
|
|
114
|
+
// for `keyboardLayoutGuide` case we can just read keyboard position directly - no interpolation needed
|
|
115
|
+
if #available(iOS 26.0, *) {
|
|
116
|
+
// when we are the top position KVO takes `inputAccessoryView` into consideration,
|
|
117
|
+
// so we handle it here
|
|
118
|
+
if keyboardPosition == keyboardHeight {
|
|
119
|
+
return keyboardPosition - KeyboardAreaExtender.shared.offset
|
|
120
|
+
}
|
|
121
|
+
return keyboardPosition
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// if keyboard height is not equal to its bounds - we can ignore
|
|
125
|
+
// values, since they'll be invalid and will cause UI jumps
|
|
126
|
+
// valid only for non-`keyboardLayoutGuide` case
|
|
127
|
+
if floor(trackedView.bounds.size.height) != floor(keyboardHeight) {
|
|
128
|
+
return Self.invalidPosition
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
let position = CGFloat.interpolate(
|
|
132
|
+
inputRange: [keyboardHeight / 2, -keyboardHeight / 2],
|
|
133
|
+
outputRange: [keyboardHeight, 0],
|
|
134
|
+
currentValue: keyboardPosition
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
return position - KeyboardAreaExtender.shared.offset
|
|
138
|
+
}
|
|
139
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
//
|
|
2
|
+
// KeyboardViewLocator.swift
|
|
3
|
+
// Pods
|
|
4
|
+
//
|
|
5
|
+
// Created by Kiryl Ziusko on 25/07/2025.
|
|
6
|
+
//
|
|
7
|
+
|
|
8
|
+
final class KeyboardViewLocator {
|
|
9
|
+
static let shared = KeyboardViewLocator()
|
|
10
|
+
private var cachedKeyboardView: UIView?
|
|
11
|
+
private var cachedWindowsCount: Int = 0
|
|
12
|
+
|
|
13
|
+
func resolve() -> UIView? {
|
|
14
|
+
let currentWindowsCount = UIApplication.shared.windows.count
|
|
15
|
+
|
|
16
|
+
if cachedKeyboardView == nil || currentWindowsCount != cachedWindowsCount {
|
|
17
|
+
cachedKeyboardView = KeyboardView.find()
|
|
18
|
+
cachedWindowsCount = currentWindowsCount
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
return cachedKeyboardView
|
|
22
|
+
}
|
|
23
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
//
|
|
2
|
+
// KeyboardMovementObserver+Animation.swift
|
|
3
|
+
// Pods
|
|
4
|
+
//
|
|
5
|
+
// Created by Kiryl Ziusko on 07/08/2025.
|
|
6
|
+
//
|
|
7
|
+
|
|
8
|
+
extension KeyboardMovementObserver {
|
|
9
|
+
func initializeAnimation(fromValue: Double, toValue: Double) {
|
|
10
|
+
for key in ["position", "opacity"] {
|
|
11
|
+
if let keyboardAnimation = keyboardTrackingView.view?.layer.presentation()?.animation(forKey: key) {
|
|
12
|
+
if let springAnimation = keyboardAnimation as? CASpringAnimation {
|
|
13
|
+
animation = SpringAnimation(animation: springAnimation, fromValue: fromValue, toValue: toValue)
|
|
14
|
+
} else if let basicAnimation = keyboardAnimation as? CABasicAnimation {
|
|
15
|
+
animation = TimingAnimation(animation: basicAnimation, fromValue: fromValue, toValue: toValue)
|
|
16
|
+
}
|
|
17
|
+
return
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
//
|
|
2
|
+
// KeyboardMovementObserver+Interactive.swift
|
|
3
|
+
// Pods
|
|
4
|
+
//
|
|
5
|
+
// Created by Kiryl Ziusko on 07/08/2025.
|
|
6
|
+
//
|
|
7
|
+
|
|
8
|
+
extension KeyboardMovementObserver {
|
|
9
|
+
func setupKVObserver() {
|
|
10
|
+
guard interactiveKeyboardObserver == nil, let view = keyboardTrackingView.view else { return }
|
|
11
|
+
|
|
12
|
+
interactiveKeyboardObserver = view.observe(\.center, options: .new) { [weak self] _, change in
|
|
13
|
+
guard let self = self, let changeValue = change.newValue else { return }
|
|
14
|
+
|
|
15
|
+
self.keyboardDidMoveInteractively(changeValue: changeValue)
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
func removeKVObserver() {
|
|
20
|
+
interactiveKeyboardObserver?.invalidate()
|
|
21
|
+
interactiveKeyboardObserver = nil
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
private func keyboardDidMoveInteractively(changeValue: CGPoint) {
|
|
25
|
+
if UIResponder.isKeyboardPreloading {
|
|
26
|
+
return
|
|
27
|
+
}
|
|
28
|
+
// if we are currently animating keyboard -> we need to ignore values from KVO
|
|
29
|
+
if !displayLink.isPaused {
|
|
30
|
+
return
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
let position = keyboardTrackingView.interactive(point: changeValue)
|
|
34
|
+
|
|
35
|
+
if position == KeyboardTrackingView.invalidPosition {
|
|
36
|
+
return
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if position == 0 {
|
|
40
|
+
// it will be triggered before `keyboardWillDisappear` and
|
|
41
|
+
// we don't need to trigger `onInteractive` handler for that
|
|
42
|
+
// since it will be handled in `keyboardWillDisappear` function
|
|
43
|
+
return
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
prevKeyboardPosition = position
|
|
47
|
+
|
|
48
|
+
onEvent(
|
|
49
|
+
"onKeyboardMoveInteractive",
|
|
50
|
+
position as NSNumber,
|
|
51
|
+
position / CGFloat(keyboardHeight) as NSNumber,
|
|
52
|
+
-1,
|
|
53
|
+
tag
|
|
54
|
+
)
|
|
55
|
+
}
|
|
56
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
//
|
|
2
|
+
// KeyboardMovementObserver+Lifecycle.swift
|
|
3
|
+
// Pods
|
|
4
|
+
//
|
|
5
|
+
// Created by Kiryl Ziusko on 07/08/2025.
|
|
6
|
+
//
|
|
7
|
+
|
|
8
|
+
public extension KeyboardMovementObserver {
|
|
9
|
+
@objc func mount() {
|
|
10
|
+
if isMounted {
|
|
11
|
+
return
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
isMounted = true
|
|
15
|
+
|
|
16
|
+
NotificationCenter.default.addObserver(
|
|
17
|
+
self,
|
|
18
|
+
selector: #selector(keyboardWillDisappear),
|
|
19
|
+
name: UIResponder.keyboardWillHideNotification,
|
|
20
|
+
object: nil
|
|
21
|
+
)
|
|
22
|
+
NotificationCenter.default.addObserver(
|
|
23
|
+
self,
|
|
24
|
+
selector: #selector(keyboardWillAppear),
|
|
25
|
+
name: UIResponder.keyboardWillShowNotification,
|
|
26
|
+
object: nil
|
|
27
|
+
)
|
|
28
|
+
NotificationCenter.default.addObserver(
|
|
29
|
+
self,
|
|
30
|
+
selector: #selector(keyboardDidAppear),
|
|
31
|
+
name: UIResponder.keyboardDidShowNotification,
|
|
32
|
+
object: nil
|
|
33
|
+
)
|
|
34
|
+
NotificationCenter.default.addObserver(
|
|
35
|
+
self,
|
|
36
|
+
selector: #selector(keyboardDidDisappear),
|
|
37
|
+
name: UIResponder.keyboardDidHideNotification,
|
|
38
|
+
object: nil
|
|
39
|
+
)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
@objc func unmount() {
|
|
43
|
+
isMounted = false
|
|
44
|
+
// swiftlint:disable:next notification_center_detachment
|
|
45
|
+
NotificationCenter.default.removeObserver(self)
|
|
46
|
+
}
|
|
47
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
//
|
|
2
|
+
// KeyboardMovementObserver+Listeners.swift
|
|
3
|
+
// Pods
|
|
4
|
+
//
|
|
5
|
+
// Created by Kiryl Ziusko on 07/08/2025.
|
|
6
|
+
//
|
|
7
|
+
|
|
8
|
+
extension KeyboardMovementObserver {
|
|
9
|
+
@objc func keyboardWillAppear(_ notification: Notification) {
|
|
10
|
+
guard !KeyboardEventsIgnorer.shared.shouldIgnore, !UIResponder.isKeyboardPreloading else { return }
|
|
11
|
+
|
|
12
|
+
let (duration, frame) = notification.keyboardMetaData()
|
|
13
|
+
if let keyboardFrame = frame {
|
|
14
|
+
tag = UIResponder.current.reactViewTag
|
|
15
|
+
let keyboardHeight = keyboardFrame.cgRectValue.size.height
|
|
16
|
+
self.keyboardHeight = keyboardHeight
|
|
17
|
+
self.duration = duration
|
|
18
|
+
didShowDeadline = Date.currentTimeStamp + Int64(duration)
|
|
19
|
+
|
|
20
|
+
onRequestAnimation()
|
|
21
|
+
onEvent("onKeyboardMoveStart", Float(self.keyboardHeight) as NSNumber, 1, duration as NSNumber, tag)
|
|
22
|
+
onNotify("KeyboardController::keyboardWillShow", buildEventParams(self.keyboardHeight, duration, tag))
|
|
23
|
+
|
|
24
|
+
setupKeyboardWatcher()
|
|
25
|
+
initializeAnimation(fromValue: prevKeyboardPosition, toValue: self.keyboardHeight)
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
@objc func keyboardWillDisappear(_ notification: Notification) {
|
|
30
|
+
guard !UIResponder.isKeyboardPreloading else { return }
|
|
31
|
+
let (duration, _) = notification.keyboardMetaData()
|
|
32
|
+
tag = UIResponder.current.reactViewTag
|
|
33
|
+
self.duration = duration
|
|
34
|
+
|
|
35
|
+
onRequestAnimation()
|
|
36
|
+
onEvent("onKeyboardMoveStart", 0, 0, duration as NSNumber, tag)
|
|
37
|
+
onNotify("KeyboardController::keyboardWillHide", buildEventParams(0, duration, tag))
|
|
38
|
+
|
|
39
|
+
setupKeyboardWatcher()
|
|
40
|
+
removeKVObserver()
|
|
41
|
+
initializeAnimation(fromValue: prevKeyboardPosition, toValue: 0)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
@objc func keyboardDidAppear(_ notification: Notification) {
|
|
45
|
+
guard !UIResponder.isKeyboardPreloading else { return }
|
|
46
|
+
|
|
47
|
+
let timestamp = Date.currentTimeStamp
|
|
48
|
+
let (duration, frame) = notification.keyboardMetaData()
|
|
49
|
+
if let keyboardFrame = frame {
|
|
50
|
+
let (position, _) = keyboardTrackingView.view.frameTransitionInWindow
|
|
51
|
+
let keyboardHeight = keyboardFrame.cgRectValue.size.height
|
|
52
|
+
tag = UIResponder.current.reactViewTag
|
|
53
|
+
self.keyboardHeight = keyboardHeight
|
|
54
|
+
|
|
55
|
+
guard !KeyboardEventsIgnorer.shared.shouldIgnore else {
|
|
56
|
+
KeyboardEventsIgnorer.shared.shouldIgnoreKeyboardEvents = false
|
|
57
|
+
return
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// if the event is caught in between it's highly likely that it could be a "resize" event
|
|
61
|
+
// so we just read actual keyboard frame value in this case
|
|
62
|
+
let height = timestamp >= didShowDeadline ? self.keyboardHeight : position - KeyboardAreaExtender.shared.offset
|
|
63
|
+
// always limit progress to the maximum possible value
|
|
64
|
+
let progress = min(height / self.keyboardHeight, 1.0)
|
|
65
|
+
|
|
66
|
+
onCancelAnimation()
|
|
67
|
+
onEvent("onKeyboardMoveEnd", height as NSNumber, progress as NSNumber, duration as NSNumber, tag)
|
|
68
|
+
onNotify("KeyboardController::keyboardDidShow", buildEventParams(height, duration, tag))
|
|
69
|
+
|
|
70
|
+
removeKeyboardWatcher()
|
|
71
|
+
setupKVObserver()
|
|
72
|
+
animation = nil
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
@objc func keyboardDidDisappear(_ notification: Notification) {
|
|
77
|
+
guard !UIResponder.isKeyboardPreloading else { return }
|
|
78
|
+
let (duration, _) = notification.keyboardMetaData()
|
|
79
|
+
tag = UIResponder.current.reactViewTag
|
|
80
|
+
|
|
81
|
+
onCancelAnimation()
|
|
82
|
+
onEvent("onKeyboardMoveEnd", 0 as NSNumber, 0, duration as NSNumber, tag)
|
|
83
|
+
onNotify("KeyboardController::keyboardDidHide", buildEventParams(0, duration, tag))
|
|
84
|
+
|
|
85
|
+
removeKeyboardWatcher()
|
|
86
|
+
animation = nil
|
|
87
|
+
}
|
|
88
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
//
|
|
2
|
+
// KeyboardMovementObserver+Watcher.swift
|
|
3
|
+
// Pods
|
|
4
|
+
//
|
|
5
|
+
// Created by Kiryl Ziusko on 07/08/2025.
|
|
6
|
+
//
|
|
7
|
+
|
|
8
|
+
extension KeyboardMovementObserver {
|
|
9
|
+
@objc func setupKeyboardWatcher() {
|
|
10
|
+
// sometimes `will` events can be called multiple times.
|
|
11
|
+
// To avoid double re-creation of listener we are adding this condition
|
|
12
|
+
// (if active link is present, then no need to re-setup a listener)
|
|
13
|
+
if !displayLink.isPaused {
|
|
14
|
+
return
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
displayLink.isPaused = false
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
@objc func removeKeyboardWatcher() {
|
|
21
|
+
displayLink.isPaused = true
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
@objc func updateKeyboardFrame(link: CADisplayLink) {
|
|
25
|
+
if keyboardTrackingView.view == nil {
|
|
26
|
+
return
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
let (visibleKeyboardHeight, keyboardFrameY) = keyboardTrackingView.view.frameTransitionInWindow
|
|
30
|
+
var keyboardPosition = visibleKeyboardHeight - KeyboardAreaExtender.shared.offset
|
|
31
|
+
|
|
32
|
+
if keyboardPosition == prevKeyboardPosition || keyboardFrameY == 0 {
|
|
33
|
+
return
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if animation == nil {
|
|
37
|
+
initializeAnimation(fromValue: prevKeyboardPosition, toValue: keyboardHeight)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
prevKeyboardPosition = keyboardPosition
|
|
41
|
+
|
|
42
|
+
if let animation = animation {
|
|
43
|
+
let baseDuration = animation.timingAt(value: keyboardPosition)
|
|
44
|
+
|
|
45
|
+
#if targetEnvironment(simulator)
|
|
46
|
+
// on iOS simulator we can not use static interval
|
|
47
|
+
// (from my observation from frame to frame we may have different delays)
|
|
48
|
+
// so for now we use approximation - we add a difference as
|
|
49
|
+
// beginTime - keyboardEventTime (but only in 0..0.016 range)
|
|
50
|
+
// and it gives satisfactory results (better than static delays)
|
|
51
|
+
let duration = baseDuration + animation.diff
|
|
52
|
+
#else
|
|
53
|
+
// 2 frames because we read previous frame, but need to calculate the next frame
|
|
54
|
+
let duration = baseDuration + link.duration * 2
|
|
55
|
+
#endif
|
|
56
|
+
|
|
57
|
+
let position = CGFloat(animation.valueAt(time: duration))
|
|
58
|
+
// handles a case when final frame has final destination (i. e. 0 or 291)
|
|
59
|
+
// but CASpringAnimation can never get to this final destination
|
|
60
|
+
let race: (CGFloat, CGFloat) -> CGFloat = animation.isIncreasing ? max : min
|
|
61
|
+
keyboardPosition = race(position, keyboardPosition)
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
onEvent(
|
|
65
|
+
"onKeyboardMove",
|
|
66
|
+
keyboardPosition as NSNumber,
|
|
67
|
+
keyboardPosition / CGFloat(keyboardHeight) as NSNumber,
|
|
68
|
+
duration as NSNumber,
|
|
69
|
+
tag
|
|
70
|
+
)
|
|
71
|
+
}
|
|
72
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
//
|
|
2
|
+
// KeyboardMovementObserver.swift
|
|
3
|
+
// KeyboardController
|
|
4
|
+
//
|
|
5
|
+
// Created by Kiryl Ziusko on 2.08.22.
|
|
6
|
+
// Copyright © 2022 Facebook. All rights reserved.
|
|
7
|
+
//
|
|
8
|
+
|
|
9
|
+
import Foundation
|
|
10
|
+
import UIKit
|
|
11
|
+
|
|
12
|
+
@objc(KeyboardMovementObserver)
|
|
13
|
+
public class KeyboardMovementObserver: NSObject {
|
|
14
|
+
// class members
|
|
15
|
+
var onEvent: (NSString, NSNumber, NSNumber, NSNumber, NSNumber) -> Void
|
|
16
|
+
var onNotify: (String, Any) -> Void
|
|
17
|
+
// animation
|
|
18
|
+
var onRequestAnimation: () -> Void
|
|
19
|
+
var onCancelAnimation: () -> Void
|
|
20
|
+
// progress tracker
|
|
21
|
+
var keyboardTrackingView = KeyboardTrackingView()
|
|
22
|
+
var animation: KeyboardAnimation?
|
|
23
|
+
|
|
24
|
+
var prevKeyboardPosition = 0.0
|
|
25
|
+
var displayLink: CADisplayLink!
|
|
26
|
+
var interactiveKeyboardObserver: NSKeyValueObservation?
|
|
27
|
+
var isMounted = false
|
|
28
|
+
// state variables
|
|
29
|
+
private var _keyboardHeight: CGFloat = 0.0
|
|
30
|
+
var keyboardHeight: CGFloat {
|
|
31
|
+
get { _keyboardHeight - KeyboardAreaExtender.shared.offset }
|
|
32
|
+
set { _keyboardHeight = newValue }
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
var duration = 0
|
|
36
|
+
var tag: NSNumber = -1
|
|
37
|
+
var didShowDeadline: Int64 = 0
|
|
38
|
+
|
|
39
|
+
@objc public init(
|
|
40
|
+
handler: @escaping (NSString, NSNumber, NSNumber, NSNumber, NSNumber) -> Void,
|
|
41
|
+
onNotify: @escaping (String, Any) -> Void,
|
|
42
|
+
onRequestAnimation: @escaping () -> Void,
|
|
43
|
+
onCancelAnimation: @escaping () -> Void
|
|
44
|
+
) {
|
|
45
|
+
onEvent = handler
|
|
46
|
+
self.onNotify = onNotify
|
|
47
|
+
self.onRequestAnimation = onRequestAnimation
|
|
48
|
+
self.onCancelAnimation = onCancelAnimation
|
|
49
|
+
|
|
50
|
+
super.init()
|
|
51
|
+
|
|
52
|
+
displayLink = CADisplayLink(target: self, selector: #selector(updateKeyboardFrame))
|
|
53
|
+
displayLink.preferredFramesPerSecond = 120 // will fallback to 60 fps for devices without Pro Motion display
|
|
54
|
+
displayLink.add(to: .main, forMode: .common)
|
|
55
|
+
displayLink.isPaused = true
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
deinit {
|
|
59
|
+
displayLink.invalidate()
|
|
60
|
+
displayLink = nil
|
|
61
|
+
NotificationCenter.default.removeObserver(self)
|
|
62
|
+
}
|
|
63
|
+
}
|
|
@@ -178,16 +178,16 @@ using namespace facebook::react;
|
|
|
178
178
|
.duration = [duration intValue],
|
|
179
179
|
.target = [target intValue]});
|
|
180
180
|
}
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
181
|
+
if ([event isEqualToString:@"onKeyboardMoveInteractive"]) {
|
|
182
|
+
std::dynamic_pointer_cast<const facebook::react::KeyboardControllerViewEventEmitter>(
|
|
183
|
+
self->_eventEmitter)
|
|
184
|
+
->onKeyboardMoveInteractive(facebook::react::KeyboardControllerViewEventEmitter::
|
|
185
|
+
OnKeyboardMoveInteractive{
|
|
186
|
+
.height = [height doubleValue],
|
|
187
|
+
.progress = [progress doubleValue],
|
|
188
|
+
.duration = [duration intValue],
|
|
189
|
+
.target = [target intValue]});
|
|
190
|
+
}
|
|
191
191
|
}
|
|
192
192
|
|
|
193
193
|
KeyboardMoveEvent *keyboardMoveEvent =
|