react-native-keyboard-controller 1.18.3 → 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/{KeyboardTrackingView.swift → movement/KeyboardTrackingView.swift} +17 -10
- 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/KeyboardExtenderContainerView.swift +24 -20
- package/package.json +1 -1
- package/ios/observers/KeyboardMovementObserver.swift +0 -307
- /package/ios/observers/{KeyboardEventsIgnorer.swift → movement/KeyboardEventsIgnorer.swift} +0 -0
- /package/ios/observers/{KeyboardViewLocator.swift → movement/KeyboardViewLocator.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
|
}
|
|
@@ -66,16 +66,18 @@ final class KeyboardTrackingView: UIView {
|
|
|
66
66
|
|
|
67
67
|
translatesAutoresizingMaskIntoConstraints = false
|
|
68
68
|
|
|
69
|
-
if #available(iOS
|
|
70
|
-
|
|
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
|
+
])
|
|
71
80
|
}
|
|
72
|
-
|
|
73
|
-
NSLayoutConstraint.activate([
|
|
74
|
-
leadingAnchor.constraint(equalTo: rootView.leadingAnchor, constant: 0),
|
|
75
|
-
trailingAnchor.constraint(equalTo: rootView.trailingAnchor, constant: 0),
|
|
76
|
-
bottomAnchor.constraint(equalTo: rootView.keyboardLayoutGuide.topAnchor, constant: 0),
|
|
77
|
-
heightAnchor.constraint(equalToConstant: 0),
|
|
78
|
-
])
|
|
79
81
|
}
|
|
80
82
|
|
|
81
83
|
@objc private func keyboardWillAppear(_ notification: Notification) {
|
|
@@ -111,6 +113,11 @@ final class KeyboardTrackingView: UIView {
|
|
|
111
113
|
|
|
112
114
|
// for `keyboardLayoutGuide` case we can just read keyboard position directly - no interpolation needed
|
|
113
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
|
+
}
|
|
114
121
|
return keyboardPosition
|
|
115
122
|
}
|
|
116
123
|
|
|
@@ -127,6 +134,6 @@ final class KeyboardTrackingView: UIView {
|
|
|
127
134
|
currentValue: keyboardPosition
|
|
128
135
|
)
|
|
129
136
|
|
|
130
|
-
return position
|
|
137
|
+
return position - KeyboardAreaExtender.shared.offset
|
|
131
138
|
}
|
|
132
139
|
}
|
|
@@ -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
|
+
}
|
|
@@ -10,11 +10,13 @@ import UIKit
|
|
|
10
10
|
@objc
|
|
11
11
|
public class KeyboardExtenderContainerView: NSObject {
|
|
12
12
|
@objc public static func create(frame: CGRect, contentView: UIView) -> UIView {
|
|
13
|
-
if
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
13
|
+
#if canImport(UIKit.UIGlassEffect)
|
|
14
|
+
if #available(iOS 26.0, *) {
|
|
15
|
+
return ModernContainerView(frame: frame, contentView: contentView)
|
|
16
|
+
}
|
|
17
|
+
#endif
|
|
18
|
+
|
|
19
|
+
return LegacyContainerView(frame: frame, contentView: contentView)
|
|
18
20
|
}
|
|
19
21
|
}
|
|
20
22
|
|
|
@@ -86,21 +88,23 @@ private class ModernContainerView: BaseContainerView {
|
|
|
86
88
|
}
|
|
87
89
|
|
|
88
90
|
private func setupVisualEffect() {
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
visualEffectView
|
|
102
|
-
|
|
103
|
-
|
|
91
|
+
#if canImport(UIKit.UIGlassEffect)
|
|
92
|
+
let isDark = FocusedInputHolder.shared.get()?.keyboardAppearanceValue == "dark"
|
|
93
|
+
let glassEffect = UIGlassEffect()
|
|
94
|
+
let color =
|
|
95
|
+
isDark ? UIColor.black.withAlphaComponent(0.3) : UIColor.gray.withAlphaComponent(0.3)
|
|
96
|
+
glassEffect.tintColor = color
|
|
97
|
+
glassEffect.isInteractive = true
|
|
98
|
+
|
|
99
|
+
visualEffectView = UIVisualEffectView(effect: glassEffect)
|
|
100
|
+
visualEffectView?.overrideUserInterfaceStyle = isDark ? .dark : .light
|
|
101
|
+
visualEffectView?.cornerConfiguration = .capsule()
|
|
102
|
+
|
|
103
|
+
if let visualEffectView = visualEffectView {
|
|
104
|
+
visualEffectView.contentView.addSubview(contentView)
|
|
105
|
+
addSubview(visualEffectView)
|
|
106
|
+
}
|
|
107
|
+
#endif
|
|
104
108
|
}
|
|
105
109
|
|
|
106
110
|
override func updateContentFrame(desiredHeight: CGFloat) {
|
package/package.json
CHANGED
|
@@ -1,307 +0,0 @@
|
|
|
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
|
-
private var keyboardTrackingView = KeyboardTrackingView()
|
|
22
|
-
|
|
23
|
-
private var prevKeyboardPosition = 0.0
|
|
24
|
-
private var displayLink: CADisplayLink!
|
|
25
|
-
private var interactiveKeyboardObserver: NSKeyValueObservation?
|
|
26
|
-
private var isMounted = false
|
|
27
|
-
// state variables
|
|
28
|
-
private var _keyboardHeight: CGFloat = 0.0
|
|
29
|
-
private var keyboardHeight: CGFloat {
|
|
30
|
-
get { _keyboardHeight - KeyboardAreaExtender.shared.offset }
|
|
31
|
-
set { _keyboardHeight = newValue }
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
private var duration = 0
|
|
35
|
-
private var tag: NSNumber = -1
|
|
36
|
-
private var animation: KeyboardAnimation?
|
|
37
|
-
private 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
|
-
}
|
|
62
|
-
|
|
63
|
-
@objc public func mount() {
|
|
64
|
-
if isMounted {
|
|
65
|
-
return
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
isMounted = true
|
|
69
|
-
|
|
70
|
-
NotificationCenter.default.addObserver(
|
|
71
|
-
self,
|
|
72
|
-
selector: #selector(keyboardWillDisappear),
|
|
73
|
-
name: UIResponder.keyboardWillHideNotification,
|
|
74
|
-
object: nil
|
|
75
|
-
)
|
|
76
|
-
NotificationCenter.default.addObserver(
|
|
77
|
-
self,
|
|
78
|
-
selector: #selector(keyboardWillAppear),
|
|
79
|
-
name: UIResponder.keyboardWillShowNotification,
|
|
80
|
-
object: nil
|
|
81
|
-
)
|
|
82
|
-
NotificationCenter.default.addObserver(
|
|
83
|
-
self,
|
|
84
|
-
selector: #selector(keyboardDidAppear),
|
|
85
|
-
name: UIResponder.keyboardDidShowNotification,
|
|
86
|
-
object: nil
|
|
87
|
-
)
|
|
88
|
-
NotificationCenter.default.addObserver(
|
|
89
|
-
self,
|
|
90
|
-
selector: #selector(keyboardDidDisappear),
|
|
91
|
-
name: UIResponder.keyboardDidHideNotification,
|
|
92
|
-
object: nil
|
|
93
|
-
)
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
private func setupKVObserver() {
|
|
97
|
-
guard interactiveKeyboardObserver == nil, let view = keyboardTrackingView.view else { return }
|
|
98
|
-
|
|
99
|
-
interactiveKeyboardObserver = view.observe(\.center, options: .new) { [weak self] _, change in
|
|
100
|
-
guard let self = self, let changeValue = change.newValue else { return }
|
|
101
|
-
|
|
102
|
-
self.keyboardDidMoveInteractively(changeValue: changeValue)
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
private func removeKVObserver() {
|
|
107
|
-
interactiveKeyboardObserver?.invalidate()
|
|
108
|
-
interactiveKeyboardObserver = nil
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
private func keyboardDidMoveInteractively(changeValue: CGPoint) {
|
|
112
|
-
if UIResponder.isKeyboardPreloading {
|
|
113
|
-
return
|
|
114
|
-
}
|
|
115
|
-
// if we are currently animating keyboard -> we need to ignore values from KVO
|
|
116
|
-
if !displayLink.isPaused {
|
|
117
|
-
return
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
let interactive = keyboardTrackingView.interactive(point: changeValue)
|
|
121
|
-
|
|
122
|
-
if interactive == KeyboardTrackingView.invalidPosition {
|
|
123
|
-
return
|
|
124
|
-
}
|
|
125
|
-
let position = interactive - KeyboardAreaExtender.shared.offset
|
|
126
|
-
|
|
127
|
-
if position == 0 {
|
|
128
|
-
// it will be triggered before `keyboardWillDisappear` and
|
|
129
|
-
// we don't need to trigger `onInteractive` handler for that
|
|
130
|
-
// since it will be handled in `keyboardWillDisappear` function
|
|
131
|
-
return
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
prevKeyboardPosition = position
|
|
135
|
-
|
|
136
|
-
onEvent(
|
|
137
|
-
"onKeyboardMoveInteractive",
|
|
138
|
-
position as NSNumber,
|
|
139
|
-
position / CGFloat(keyboardHeight) as NSNumber,
|
|
140
|
-
-1,
|
|
141
|
-
tag
|
|
142
|
-
)
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
@objc public func unmount() {
|
|
146
|
-
isMounted = false
|
|
147
|
-
// swiftlint:disable:next notification_center_detachment
|
|
148
|
-
NotificationCenter.default.removeObserver(self)
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
@objc func keyboardWillAppear(_ notification: Notification) {
|
|
152
|
-
guard !KeyboardEventsIgnorer.shared.shouldIgnore, !UIResponder.isKeyboardPreloading else { return }
|
|
153
|
-
|
|
154
|
-
let (duration, frame) = notification.keyboardMetaData()
|
|
155
|
-
if let keyboardFrame = frame {
|
|
156
|
-
tag = UIResponder.current.reactViewTag
|
|
157
|
-
let keyboardHeight = keyboardFrame.cgRectValue.size.height
|
|
158
|
-
self.keyboardHeight = keyboardHeight
|
|
159
|
-
self.duration = duration
|
|
160
|
-
didShowDeadline = Date.currentTimeStamp + Int64(duration)
|
|
161
|
-
|
|
162
|
-
onRequestAnimation()
|
|
163
|
-
onEvent("onKeyboardMoveStart", Float(self.keyboardHeight) as NSNumber, 1, duration as NSNumber, tag)
|
|
164
|
-
onNotify("KeyboardController::keyboardWillShow", buildEventParams(self.keyboardHeight, duration, tag))
|
|
165
|
-
|
|
166
|
-
setupKeyboardWatcher()
|
|
167
|
-
initializeAnimation(fromValue: prevKeyboardPosition, toValue: self.keyboardHeight)
|
|
168
|
-
}
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
@objc func keyboardWillDisappear(_ notification: Notification) {
|
|
172
|
-
guard !UIResponder.isKeyboardPreloading else { return }
|
|
173
|
-
let (duration, _) = notification.keyboardMetaData()
|
|
174
|
-
tag = UIResponder.current.reactViewTag
|
|
175
|
-
self.duration = duration
|
|
176
|
-
|
|
177
|
-
onRequestAnimation()
|
|
178
|
-
onEvent("onKeyboardMoveStart", 0, 0, duration as NSNumber, tag)
|
|
179
|
-
onNotify("KeyboardController::keyboardWillHide", buildEventParams(0, duration, tag))
|
|
180
|
-
|
|
181
|
-
setupKeyboardWatcher()
|
|
182
|
-
removeKVObserver()
|
|
183
|
-
initializeAnimation(fromValue: prevKeyboardPosition, toValue: 0)
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
@objc func keyboardDidAppear(_ notification: Notification) {
|
|
187
|
-
guard !UIResponder.isKeyboardPreloading else { return }
|
|
188
|
-
|
|
189
|
-
let timestamp = Date.currentTimeStamp
|
|
190
|
-
let (duration, frame) = notification.keyboardMetaData()
|
|
191
|
-
if let keyboardFrame = frame {
|
|
192
|
-
let (position, _) = keyboardTrackingView.view.frameTransitionInWindow
|
|
193
|
-
let keyboardHeight = keyboardFrame.cgRectValue.size.height
|
|
194
|
-
tag = UIResponder.current.reactViewTag
|
|
195
|
-
self.keyboardHeight = keyboardHeight
|
|
196
|
-
|
|
197
|
-
guard !KeyboardEventsIgnorer.shared.shouldIgnore else {
|
|
198
|
-
KeyboardEventsIgnorer.shared.shouldIgnoreKeyboardEvents = false
|
|
199
|
-
return
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
// if the event is caught in between it's highly likely that it could be a "resize" event
|
|
203
|
-
// so we just read actual keyboard frame value in this case
|
|
204
|
-
let height = timestamp >= didShowDeadline ? self.keyboardHeight : position - KeyboardAreaExtender.shared.offset
|
|
205
|
-
// always limit progress to the maximum possible value
|
|
206
|
-
let progress = min(height / self.keyboardHeight, 1.0)
|
|
207
|
-
|
|
208
|
-
onCancelAnimation()
|
|
209
|
-
onEvent("onKeyboardMoveEnd", height as NSNumber, progress as NSNumber, duration as NSNumber, tag)
|
|
210
|
-
onNotify("KeyboardController::keyboardDidShow", buildEventParams(height, duration, tag))
|
|
211
|
-
|
|
212
|
-
removeKeyboardWatcher()
|
|
213
|
-
setupKVObserver()
|
|
214
|
-
animation = nil
|
|
215
|
-
}
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
@objc func keyboardDidDisappear(_ notification: Notification) {
|
|
219
|
-
guard !UIResponder.isKeyboardPreloading else { return }
|
|
220
|
-
let (duration, _) = notification.keyboardMetaData()
|
|
221
|
-
tag = UIResponder.current.reactViewTag
|
|
222
|
-
|
|
223
|
-
onCancelAnimation()
|
|
224
|
-
onEvent("onKeyboardMoveEnd", 0 as NSNumber, 0, duration as NSNumber, tag)
|
|
225
|
-
onNotify("KeyboardController::keyboardDidHide", buildEventParams(0, duration, tag))
|
|
226
|
-
|
|
227
|
-
removeKeyboardWatcher()
|
|
228
|
-
animation = nil
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
@objc func setupKeyboardWatcher() {
|
|
232
|
-
// sometimes `will` events can be called multiple times.
|
|
233
|
-
// To avoid double re-creation of listener we are adding this condition
|
|
234
|
-
// (if active link is present, then no need to re-setup a listener)
|
|
235
|
-
if !displayLink.isPaused {
|
|
236
|
-
return
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
displayLink.isPaused = false
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
@objc func removeKeyboardWatcher() {
|
|
243
|
-
displayLink.isPaused = true
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
func initializeAnimation(fromValue: Double, toValue: Double) {
|
|
247
|
-
for key in ["position", "opacity"] {
|
|
248
|
-
if let keyboardAnimation = keyboardTrackingView.view?.layer.presentation()?.animation(forKey: key) {
|
|
249
|
-
if let springAnimation = keyboardAnimation as? CASpringAnimation {
|
|
250
|
-
animation = SpringAnimation(animation: springAnimation, fromValue: fromValue, toValue: toValue)
|
|
251
|
-
} else if let basicAnimation = keyboardAnimation as? CABasicAnimation {
|
|
252
|
-
animation = TimingAnimation(animation: basicAnimation, fromValue: fromValue, toValue: toValue)
|
|
253
|
-
}
|
|
254
|
-
return
|
|
255
|
-
}
|
|
256
|
-
}
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
@objc func updateKeyboardFrame(link: CADisplayLink) {
|
|
260
|
-
if keyboardTrackingView.view == nil {
|
|
261
|
-
return
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
let (visibleKeyboardHeight, keyboardFrameY) = keyboardTrackingView.view.frameTransitionInWindow
|
|
265
|
-
var keyboardPosition = visibleKeyboardHeight - KeyboardAreaExtender.shared.offset
|
|
266
|
-
|
|
267
|
-
if keyboardPosition == prevKeyboardPosition || keyboardFrameY == 0 {
|
|
268
|
-
return
|
|
269
|
-
}
|
|
270
|
-
|
|
271
|
-
if animation == nil {
|
|
272
|
-
initializeAnimation(fromValue: prevKeyboardPosition, toValue: keyboardHeight)
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
prevKeyboardPosition = keyboardPosition
|
|
276
|
-
|
|
277
|
-
if let animation = animation {
|
|
278
|
-
let baseDuration = animation.timingAt(value: keyboardPosition)
|
|
279
|
-
|
|
280
|
-
#if targetEnvironment(simulator)
|
|
281
|
-
// on iOS simulator we can not use static interval
|
|
282
|
-
// (from my observation from frame to frame we may have different delays)
|
|
283
|
-
// so for now we use approximation - we add a difference as
|
|
284
|
-
// beginTime - keyboardEventTime (but only in 0..0.016 range)
|
|
285
|
-
// and it gives satisfactory results (better than static delays)
|
|
286
|
-
let duration = baseDuration + animation.diff
|
|
287
|
-
#else
|
|
288
|
-
// 2 frames because we read previous frame, but need to calculate the next frame
|
|
289
|
-
let duration = baseDuration + link.duration * 2
|
|
290
|
-
#endif
|
|
291
|
-
|
|
292
|
-
let position = CGFloat(animation.valueAt(time: duration))
|
|
293
|
-
// handles a case when final frame has final destination (i. e. 0 or 291)
|
|
294
|
-
// but CASpringAnimation can never get to this final destination
|
|
295
|
-
let race: (CGFloat, CGFloat) -> CGFloat = animation.isIncreasing ? max : min
|
|
296
|
-
keyboardPosition = race(position, keyboardPosition)
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
onEvent(
|
|
300
|
-
"onKeyboardMove",
|
|
301
|
-
keyboardPosition as NSNumber,
|
|
302
|
-
keyboardPosition / CGFloat(keyboardHeight) as NSNumber,
|
|
303
|
-
duration as NSNumber,
|
|
304
|
-
tag
|
|
305
|
-
)
|
|
306
|
-
}
|
|
307
|
-
}
|
|
File without changes
|
|
File without changes
|