react-native-keyboard-controller 1.7.0 → 1.9.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/README.md +2 -0
- package/android/gradle.properties +1 -1
- package/android/src/fabric/java/com/reactnativekeyboardcontroller/KeyboardControllerViewManager.kt +5 -0
- package/android/src/main/java/com/reactnativekeyboardcontroller/events/FocusedInputLayoutChangedEvent.kt +41 -0
- package/android/src/main/java/com/reactnativekeyboardcontroller/extensions/ThemedReactContext.kt +9 -0
- package/android/src/main/java/com/reactnativekeyboardcontroller/extensions/View.kt +7 -0
- package/android/src/main/java/com/reactnativekeyboardcontroller/listeners/FocusedInputLayoutObserver.kt +93 -0
- package/android/src/main/java/com/reactnativekeyboardcontroller/{KeyboardAnimationCallback.kt → listeners/KeyboardAnimationCallback.kt} +86 -55
- package/android/src/main/java/com/reactnativekeyboardcontroller/managers/KeyboardControllerViewManagerImpl.kt +6 -0
- package/android/src/main/java/com/reactnativekeyboardcontroller/views/EdgeToEdgeReactViewGroup.kt +120 -50
- package/android/src/paper/java/com/reactnativekeyboardcontroller/KeyboardControllerViewManager.kt +5 -0
- package/ios/Extensions.swift +7 -0
- package/ios/KeyboardController.xcodeproj/project.pbxproj +41 -7
- package/ios/events/FocusedInputLayoutChangedEvent.h +16 -0
- package/ios/events/FocusedInputLayoutChangedEvent.m +75 -0
- package/ios/observers/FocusedInputLayoutObserver.swift +136 -0
- package/ios/{KeyboardControllerView.mm → views/KeyboardControllerView.mm} +54 -3
- package/ios/{KeyboardControllerViewManager.mm → views/KeyboardControllerViewManager.mm} +7 -0
- package/ios/{KeyboardControllerViewManager.swift → views/KeyboardControllerViewManager.swift} +25 -1
- package/jest/index.js +16 -0
- package/lib/commonjs/animated.js +34 -6
- package/lib/commonjs/animated.js.map +1 -1
- package/lib/commonjs/bindings.js.map +1 -1
- package/lib/commonjs/bindings.native.js.map +1 -1
- package/lib/commonjs/components/KeyboardAvoidingView/hooks.js +9 -1
- package/lib/commonjs/components/KeyboardAvoidingView/hooks.js.map +1 -1
- package/lib/commonjs/components/KeyboardAvoidingView/index.js +7 -4
- package/lib/commonjs/components/KeyboardAvoidingView/index.js.map +1 -1
- package/lib/commonjs/components/KeyboardStickyView/index.js +44 -0
- package/lib/commonjs/components/KeyboardStickyView/index.js.map +1 -0
- package/lib/commonjs/components/index.js +7 -0
- package/lib/commonjs/components/index.js.map +1 -1
- package/lib/commonjs/constants.js.map +1 -1
- package/lib/commonjs/context.js +9 -4
- package/lib/commonjs/context.js.map +1 -1
- package/lib/commonjs/hooks.js +16 -1
- package/lib/commonjs/hooks.js.map +1 -1
- package/lib/commonjs/index.js +8 -2
- package/lib/commonjs/index.js.map +1 -1
- package/lib/commonjs/internal.js.map +1 -1
- package/lib/commonjs/monkey-patch.android.js +32 -20
- package/lib/commonjs/monkey-patch.android.js.map +1 -1
- package/lib/commonjs/monkey-patch.js +11 -1
- package/lib/commonjs/monkey-patch.js.map +1 -1
- package/lib/commonjs/reanimated.js +5 -2
- 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/replicas.js +1 -1
- package/lib/commonjs/replicas.js.map +1 -1
- package/lib/commonjs/specs/KeyboardControllerViewNativeComponent.js.map +1 -1
- package/lib/commonjs/specs/KeyboardGestureAreaNativeComponent.js.map +1 -1
- package/lib/commonjs/specs/NativeKeyboardController.js.map +1 -1
- package/lib/commonjs/specs/NativeStatusBarManagerCompat.js.map +1 -1
- package/lib/commonjs/types.js.map +1 -1
- package/lib/module/animated.js +37 -9
- package/lib/module/animated.js.map +1 -1
- package/lib/module/bindings.js.map +1 -1
- package/lib/module/bindings.native.js +1 -1
- package/lib/module/bindings.native.js.map +1 -1
- package/lib/module/components/KeyboardAvoidingView/hooks.js +9 -1
- package/lib/module/components/KeyboardAvoidingView/hooks.js.map +1 -1
- package/lib/module/components/KeyboardAvoidingView/index.js +9 -6
- package/lib/module/components/KeyboardAvoidingView/index.js.map +1 -1
- package/lib/module/components/KeyboardStickyView/index.js +35 -0
- package/lib/module/components/KeyboardStickyView/index.js.map +1 -0
- package/lib/module/components/index.js +1 -0
- package/lib/module/components/index.js.map +1 -1
- package/lib/module/constants.js.map +1 -1
- package/lib/module/context.js +9 -4
- package/lib/module/context.js.map +1 -1
- package/lib/module/hooks.js +14 -1
- package/lib/module/hooks.js.map +1 -1
- package/lib/module/index.js +1 -2
- package/lib/module/index.js.map +1 -1
- package/lib/module/internal.js.map +1 -1
- package/lib/module/monkey-patch.android.js +26 -20
- package/lib/module/monkey-patch.android.js.map +1 -1
- package/lib/module/monkey-patch.js +3 -0
- package/lib/module/monkey-patch.js.map +1 -1
- package/lib/module/reanimated.js +3 -1
- 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/replicas.js +2 -2
- package/lib/module/replicas.js.map +1 -1
- package/lib/module/specs/KeyboardControllerViewNativeComponent.js.map +1 -1
- package/lib/module/specs/KeyboardGestureAreaNativeComponent.js.map +1 -1
- package/lib/module/specs/NativeKeyboardController.js.map +1 -1
- package/lib/module/specs/NativeStatusBarManagerCompat.js.map +1 -1
- package/lib/module/types.js.map +1 -1
- package/lib/typescript/animated.d.ts +8 -1
- package/lib/typescript/components/KeyboardAvoidingView/hooks.d.ts +1 -0
- package/lib/typescript/components/KeyboardAvoidingView/index.d.ts +2 -1
- package/lib/typescript/components/KeyboardStickyView/index.d.ts +20 -0
- package/lib/typescript/components/index.d.ts +1 -0
- package/lib/typescript/context.d.ts +6 -3
- package/lib/typescript/hooks.d.ts +9 -2
- package/lib/typescript/index.d.ts +1 -2
- package/lib/typescript/internal.d.ts +1 -1
- package/lib/typescript/monkey-patch.android.d.ts +2 -1
- package/lib/typescript/monkey-patch.d.ts +2 -0
- package/lib/typescript/reanimated.d.ts +2 -1
- package/lib/typescript/reanimated.native.d.ts +2 -1
- package/lib/typescript/specs/KeyboardControllerViewNativeComponent.d.ts +13 -0
- package/lib/typescript/types.d.ts +17 -0
- package/package.json +9 -28
- package/src/animated.tsx +53 -9
- package/src/bindings.native.ts +1 -1
- package/src/bindings.ts +3 -1
- package/src/components/KeyboardAvoidingView/hooks.ts +9 -1
- package/src/components/KeyboardAvoidingView/index.tsx +15 -13
- package/src/components/KeyboardStickyView/index.tsx +63 -0
- package/src/components/index.ts +1 -0
- package/src/constants.ts +2 -0
- package/src/context.ts +16 -5
- package/src/hooks.ts +16 -6
- package/src/index.ts +1 -3
- package/src/internal.ts +3 -1
- package/src/monkey-patch.android.ts +27 -21
- package/src/monkey-patch.ts +3 -0
- package/src/reanimated.native.ts +30 -1
- package/src/reanimated.ts +13 -2
- package/src/replicas.ts +2 -2
- package/src/specs/KeyboardControllerViewNativeComponent.ts +16 -1
- package/src/specs/KeyboardGestureAreaNativeComponent.ts +2 -1
- package/src/specs/NativeKeyboardController.ts +2 -1
- package/src/specs/NativeStatusBarManagerCompat.ts +2 -1
- package/src/types.ts +28 -1
- /package/ios/{KeyboardMoveEvent.h → events/KeyboardMoveEvent.h} +0 -0
- /package/ios/{KeyboardMoveEvent.m → events/KeyboardMoveEvent.m} +0 -0
- /package/ios/{KeyboardMovementObserver.swift → observers/KeyboardMovementObserver.swift} +0 -0
- /package/ios/{KeyboardControllerView.h → views/KeyboardControllerView.h} +0 -0
package/README.md
CHANGED
|
@@ -13,6 +13,8 @@ Keyboard manager which works in identical way on both iOS and Android.
|
|
|
13
13
|
- module for changing soft input mode on Android 🤔
|
|
14
14
|
- reanimated support 🚀
|
|
15
15
|
- interactive keyboard dismissing 👆📱
|
|
16
|
+
- prebuilt components (`KeyboardStickyView`, re-worked `KeyboardAvoidingView`) 📚
|
|
17
|
+
- easy focused input information retrieval 📝 🔮
|
|
16
18
|
- works with any navigation library 🧭
|
|
17
19
|
- and more is coming... Stay tuned! 😊
|
|
18
20
|
|
package/android/src/fabric/java/com/reactnativekeyboardcontroller/KeyboardControllerViewManager.kt
CHANGED
|
@@ -37,6 +37,11 @@ class KeyboardControllerViewManager(mReactContext: ReactApplicationContext) :
|
|
|
37
37
|
return manager.setNavigationBarTranslucent(view as EdgeToEdgeReactViewGroup, value)
|
|
38
38
|
}
|
|
39
39
|
|
|
40
|
+
@ReactProp(name = "enabled")
|
|
41
|
+
override fun setEnabled(view: ReactViewGroup, value: Boolean) {
|
|
42
|
+
return manager.setEnabled(view as EdgeToEdgeReactViewGroup, value)
|
|
43
|
+
}
|
|
44
|
+
|
|
40
45
|
override fun getExportedCustomDirectEventTypeConstants(): MutableMap<String, Any> {
|
|
41
46
|
return manager.getExportedCustomDirectEventTypeConstants()
|
|
42
47
|
}
|
|
@@ -0,0 +1,41 @@
|
|
|
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 FocusedInputLayoutChangedEventData(
|
|
8
|
+
val x: Double,
|
|
9
|
+
val y: Double,
|
|
10
|
+
val width: Double,
|
|
11
|
+
val height: Double,
|
|
12
|
+
val absoluteX: Double,
|
|
13
|
+
val absoluteY: Double,
|
|
14
|
+
val target: Int,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
class FocusedInputLayoutChangedEvent(
|
|
18
|
+
surfaceId: Int,
|
|
19
|
+
viewId: Int,
|
|
20
|
+
private val event: FocusedInputLayoutChangedEventData,
|
|
21
|
+
) : Event<KeyboardTransitionEvent>(surfaceId, viewId) {
|
|
22
|
+
override fun getEventName() = "topFocusedInputLayoutChanged"
|
|
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
|
+
"layout",
|
|
31
|
+
Arguments.createMap().apply {
|
|
32
|
+
putDouble("x", event.x)
|
|
33
|
+
putDouble("y", event.y)
|
|
34
|
+
putDouble("width", event.width)
|
|
35
|
+
putDouble("height", event.height)
|
|
36
|
+
putDouble("absoluteX", event.absoluteX)
|
|
37
|
+
putDouble("absoluteY", event.absoluteY)
|
|
38
|
+
},
|
|
39
|
+
)
|
|
40
|
+
}
|
|
41
|
+
}
|
package/android/src/main/java/com/reactnativekeyboardcontroller/extensions/ThemedReactContext.kt
CHANGED
|
@@ -2,6 +2,15 @@ package com.reactnativekeyboardcontroller.extensions
|
|
|
2
2
|
|
|
3
3
|
import android.view.View
|
|
4
4
|
import com.facebook.react.uimanager.ThemedReactContext
|
|
5
|
+
import com.facebook.react.uimanager.UIManagerHelper
|
|
6
|
+
import com.facebook.react.uimanager.events.Event
|
|
7
|
+
import com.facebook.react.uimanager.events.EventDispatcher
|
|
5
8
|
|
|
6
9
|
val ThemedReactContext.rootView: View?
|
|
7
10
|
get() = this.currentActivity?.window?.decorView?.rootView
|
|
11
|
+
|
|
12
|
+
fun ThemedReactContext?.dispatchEvent(viewId: Int, event: Event<*>) {
|
|
13
|
+
val eventDispatcher: EventDispatcher? =
|
|
14
|
+
UIManagerHelper.getEventDispatcherForReactTag(this, viewId)
|
|
15
|
+
eventDispatcher?.dispatchEvent(event)
|
|
16
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
package com.reactnativekeyboardcontroller.listeners
|
|
2
|
+
|
|
3
|
+
import android.view.View.OnLayoutChangeListener
|
|
4
|
+
import android.view.ViewTreeObserver.OnGlobalFocusChangeListener
|
|
5
|
+
import com.facebook.react.uimanager.ThemedReactContext
|
|
6
|
+
import com.facebook.react.uimanager.UIManagerHelper
|
|
7
|
+
import com.facebook.react.views.textinput.ReactEditText
|
|
8
|
+
import com.facebook.react.views.view.ReactViewGroup
|
|
9
|
+
import com.reactnativekeyboardcontroller.events.FocusedInputLayoutChangedEvent
|
|
10
|
+
import com.reactnativekeyboardcontroller.events.FocusedInputLayoutChangedEventData
|
|
11
|
+
import com.reactnativekeyboardcontroller.extensions.dispatchEvent
|
|
12
|
+
import com.reactnativekeyboardcontroller.extensions.dp
|
|
13
|
+
import com.reactnativekeyboardcontroller.extensions.screenLocation
|
|
14
|
+
|
|
15
|
+
val noFocusedInputEvent = FocusedInputLayoutChangedEventData(
|
|
16
|
+
x = 0.0,
|
|
17
|
+
y = 0.0,
|
|
18
|
+
width = 0.0,
|
|
19
|
+
height = 0.0,
|
|
20
|
+
absoluteX = 0.0,
|
|
21
|
+
absoluteY = 0.0,
|
|
22
|
+
target = -1,
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
class FocusedInputLayoutObserver(val view: ReactViewGroup, private val context: ThemedReactContext?) {
|
|
26
|
+
// constructor variables
|
|
27
|
+
private val surfaceId = UIManagerHelper.getSurfaceId(view)
|
|
28
|
+
|
|
29
|
+
// state variables
|
|
30
|
+
private var lastFocusedInput: ReactEditText? = null
|
|
31
|
+
private var lastEventDispatched: FocusedInputLayoutChangedEventData = noFocusedInputEvent
|
|
32
|
+
|
|
33
|
+
// listeners
|
|
34
|
+
private val layoutListener =
|
|
35
|
+
OnLayoutChangeListener { v, left, top, right, bottom, oldLeft, oldTop, oldRight, oldBottom ->
|
|
36
|
+
this.syncUpLayout()
|
|
37
|
+
}
|
|
38
|
+
private val focusListener = OnGlobalFocusChangeListener { oldFocus, newFocus ->
|
|
39
|
+
// unfocused or focused was changed
|
|
40
|
+
if (newFocus == null || oldFocus != null) {
|
|
41
|
+
lastFocusedInput?.removeOnLayoutChangeListener(layoutListener)
|
|
42
|
+
lastFocusedInput = null
|
|
43
|
+
}
|
|
44
|
+
if (newFocus is ReactEditText) {
|
|
45
|
+
lastFocusedInput = newFocus
|
|
46
|
+
newFocus.addOnLayoutChangeListener(layoutListener)
|
|
47
|
+
this.syncUpLayout()
|
|
48
|
+
}
|
|
49
|
+
// unfocused
|
|
50
|
+
if (newFocus == null) {
|
|
51
|
+
dispatchEventToJS(noFocusedInputEvent)
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
init {
|
|
56
|
+
view.viewTreeObserver.addOnGlobalFocusChangeListener(focusListener)
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
fun syncUpLayout() {
|
|
60
|
+
val input = lastFocusedInput ?: return
|
|
61
|
+
|
|
62
|
+
val (x, y) = input.screenLocation
|
|
63
|
+
val event = FocusedInputLayoutChangedEventData(
|
|
64
|
+
x = input.x.dp,
|
|
65
|
+
y = input.y.dp,
|
|
66
|
+
width = input.width.toFloat().dp,
|
|
67
|
+
height = input.height.toFloat().dp,
|
|
68
|
+
absoluteX = x.toFloat().dp,
|
|
69
|
+
absoluteY = y.toFloat().dp,
|
|
70
|
+
target = input.id,
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
dispatchEventToJS(event)
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
fun destroy() {
|
|
77
|
+
view.viewTreeObserver.removeOnGlobalFocusChangeListener(focusListener)
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
private fun dispatchEventToJS(event: FocusedInputLayoutChangedEventData) {
|
|
81
|
+
if (event != lastEventDispatched) {
|
|
82
|
+
lastEventDispatched = event
|
|
83
|
+
context.dispatchEvent(
|
|
84
|
+
view.id,
|
|
85
|
+
FocusedInputLayoutChangedEvent(
|
|
86
|
+
surfaceId,
|
|
87
|
+
view.id,
|
|
88
|
+
event = event,
|
|
89
|
+
),
|
|
90
|
+
)
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
@@ -1,9 +1,10 @@
|
|
|
1
|
-
package com.reactnativekeyboardcontroller
|
|
1
|
+
package com.reactnativekeyboardcontroller.listeners
|
|
2
2
|
|
|
3
3
|
import android.animation.ValueAnimator
|
|
4
4
|
import android.os.Build
|
|
5
5
|
import android.util.Log
|
|
6
6
|
import android.view.View
|
|
7
|
+
import android.view.ViewTreeObserver.OnGlobalFocusChangeListener
|
|
7
8
|
import androidx.core.animation.doOnEnd
|
|
8
9
|
import androidx.core.graphics.Insets
|
|
9
10
|
import androidx.core.view.OnApplyWindowInsetsListener
|
|
@@ -15,11 +16,11 @@ import com.facebook.react.bridge.WritableMap
|
|
|
15
16
|
import com.facebook.react.modules.core.DeviceEventManagerModule
|
|
16
17
|
import com.facebook.react.uimanager.ThemedReactContext
|
|
17
18
|
import com.facebook.react.uimanager.UIManagerHelper
|
|
18
|
-
import com.facebook.react.uimanager.events.Event
|
|
19
|
-
import com.facebook.react.uimanager.events.EventDispatcher
|
|
20
19
|
import com.facebook.react.views.textinput.ReactEditText
|
|
21
20
|
import com.facebook.react.views.view.ReactViewGroup
|
|
21
|
+
import com.reactnativekeyboardcontroller.InteractiveKeyboardProvider
|
|
22
22
|
import com.reactnativekeyboardcontroller.events.KeyboardTransitionEvent
|
|
23
|
+
import com.reactnativekeyboardcontroller.extensions.dispatchEvent
|
|
23
24
|
import com.reactnativekeyboardcontroller.extensions.dp
|
|
24
25
|
import kotlin.math.abs
|
|
25
26
|
|
|
@@ -33,56 +34,65 @@ class KeyboardAnimationCallback(
|
|
|
33
34
|
val context: ThemedReactContext?,
|
|
34
35
|
) : WindowInsetsAnimationCompat.Callback(dispatchMode), OnApplyWindowInsetsListener {
|
|
35
36
|
private val surfaceId = UIManagerHelper.getSurfaceId(view)
|
|
37
|
+
|
|
38
|
+
// state variables
|
|
36
39
|
private var persistentKeyboardHeight = 0.0
|
|
37
40
|
private var isKeyboardVisible = false
|
|
38
41
|
private var isTransitioning = false
|
|
39
42
|
private var duration = 0
|
|
40
43
|
private var viewTagFocused = -1
|
|
41
44
|
|
|
45
|
+
// listeners
|
|
46
|
+
private val focusListener = OnGlobalFocusChangeListener { oldFocus, newFocus ->
|
|
47
|
+
if (newFocus is ReactEditText) {
|
|
48
|
+
viewTagFocused = newFocus.id
|
|
49
|
+
|
|
50
|
+
// keyboard is visible and focus has been changed
|
|
51
|
+
if (this.isKeyboardVisible && oldFocus !== null) {
|
|
52
|
+
// imitate iOS behavior and send two instant start/end events containing an info about new tag
|
|
53
|
+
// 1. onStart/onMove/onEnd can be still dispatched after, if keyboard change size (numeric -> alphabetic type)
|
|
54
|
+
// 2. event should be send only when keyboard is visible, since this event arrives earlier -> `tag` will be
|
|
55
|
+
// 100% included in onStart/onMove/onEnd lifecycles, but triggering onStart/onEnd several time
|
|
56
|
+
// can bring breaking changes
|
|
57
|
+
context.dispatchEvent(
|
|
58
|
+
view.id,
|
|
59
|
+
KeyboardTransitionEvent(
|
|
60
|
+
surfaceId,
|
|
61
|
+
view.id,
|
|
62
|
+
"topKeyboardMoveStart",
|
|
63
|
+
this.persistentKeyboardHeight,
|
|
64
|
+
1.0,
|
|
65
|
+
0,
|
|
66
|
+
viewTagFocused,
|
|
67
|
+
),
|
|
68
|
+
)
|
|
69
|
+
context.dispatchEvent(
|
|
70
|
+
view.id,
|
|
71
|
+
KeyboardTransitionEvent(
|
|
72
|
+
surfaceId,
|
|
73
|
+
view.id,
|
|
74
|
+
"topKeyboardMoveEnd",
|
|
75
|
+
this.persistentKeyboardHeight,
|
|
76
|
+
1.0,
|
|
77
|
+
0,
|
|
78
|
+
viewTagFocused,
|
|
79
|
+
),
|
|
80
|
+
)
|
|
81
|
+
this.emitEvent("KeyboardController::keyboardWillShow", getEventParams(this.persistentKeyboardHeight))
|
|
82
|
+
this.emitEvent("KeyboardController::keyboardDidShow", getEventParams(this.persistentKeyboardHeight))
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
private var layoutObserver: FocusedInputLayoutObserver? = null
|
|
87
|
+
|
|
42
88
|
init {
|
|
43
89
|
require(persistentInsetTypes and deferredInsetTypes == 0) {
|
|
44
90
|
"persistentInsetTypes and deferredInsetTypes can not contain any of " +
|
|
45
91
|
" same WindowInsetsCompat.Type values"
|
|
46
92
|
}
|
|
47
93
|
|
|
48
|
-
view
|
|
49
|
-
|
|
50
|
-
viewTagFocused = newFocus.id
|
|
51
|
-
|
|
52
|
-
// keyboard is visible and focus has been changed
|
|
53
|
-
if (this.isKeyboardVisible && oldFocus !== null) {
|
|
54
|
-
// imitate iOS behavior and send two instant start/end events containing an info about new tag
|
|
55
|
-
// 1. onStart/onMove/onEnd can be still dispatched after, if keyboard change size (numeric -> alphabetic type)
|
|
56
|
-
// 2. event should be send only when keyboard is visible, since this event arrives earlier -> `tag` will be
|
|
57
|
-
// 100% included in onStart/onMove/onEnd lifecycles, but triggering onStart/onEnd several time
|
|
58
|
-
// can bring breaking changes
|
|
59
|
-
this.sendEventToJS(
|
|
60
|
-
KeyboardTransitionEvent(
|
|
61
|
-
surfaceId,
|
|
62
|
-
view.id,
|
|
63
|
-
"topKeyboardMoveStart",
|
|
64
|
-
this.persistentKeyboardHeight,
|
|
65
|
-
1.0,
|
|
66
|
-
0,
|
|
67
|
-
viewTagFocused,
|
|
68
|
-
),
|
|
69
|
-
)
|
|
70
|
-
this.sendEventToJS(
|
|
71
|
-
KeyboardTransitionEvent(
|
|
72
|
-
surfaceId,
|
|
73
|
-
view.id,
|
|
74
|
-
"topKeyboardMoveEnd",
|
|
75
|
-
this.persistentKeyboardHeight,
|
|
76
|
-
1.0,
|
|
77
|
-
0,
|
|
78
|
-
viewTagFocused,
|
|
79
|
-
),
|
|
80
|
-
)
|
|
81
|
-
this.emitEvent("KeyboardController::keyboardWillShow", getEventParams(this.persistentKeyboardHeight))
|
|
82
|
-
this.emitEvent("KeyboardController::keyboardDidShow", getEventParams(this.persistentKeyboardHeight))
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
}
|
|
94
|
+
layoutObserver = FocusedInputLayoutObserver(view = view, context = context)
|
|
95
|
+
view.viewTreeObserver.addOnGlobalFocusChangeListener(focusListener)
|
|
86
96
|
}
|
|
87
97
|
|
|
88
98
|
/**
|
|
@@ -115,8 +125,10 @@ class KeyboardAnimationCallback(
|
|
|
115
125
|
val keyboardHeight = getCurrentKeyboardHeight()
|
|
116
126
|
val duration = DEFAULT_ANIMATION_TIME.toInt()
|
|
117
127
|
|
|
128
|
+
layoutObserver?.syncUpLayout()
|
|
118
129
|
this.emitEvent("KeyboardController::keyboardWillShow", getEventParams(keyboardHeight))
|
|
119
|
-
|
|
130
|
+
context.dispatchEvent(
|
|
131
|
+
view.id,
|
|
120
132
|
KeyboardTransitionEvent(
|
|
121
133
|
surfaceId,
|
|
122
134
|
view.id,
|
|
@@ -128,10 +140,12 @@ class KeyboardAnimationCallback(
|
|
|
128
140
|
),
|
|
129
141
|
)
|
|
130
142
|
|
|
131
|
-
val animation =
|
|
143
|
+
val animation =
|
|
144
|
+
ValueAnimator.ofFloat(this.persistentKeyboardHeight.toFloat(), keyboardHeight.toFloat())
|
|
132
145
|
animation.addUpdateListener { animator ->
|
|
133
146
|
val toValue = animator.animatedValue as Float
|
|
134
|
-
|
|
147
|
+
context.dispatchEvent(
|
|
148
|
+
view.id,
|
|
135
149
|
KeyboardTransitionEvent(
|
|
136
150
|
surfaceId,
|
|
137
151
|
view.id,
|
|
@@ -145,7 +159,8 @@ class KeyboardAnimationCallback(
|
|
|
145
159
|
}
|
|
146
160
|
animation.doOnEnd {
|
|
147
161
|
this.emitEvent("KeyboardController::keyboardDidShow", getEventParams(keyboardHeight))
|
|
148
|
-
|
|
162
|
+
context.dispatchEvent(
|
|
163
|
+
view.id,
|
|
149
164
|
KeyboardTransitionEvent(
|
|
150
165
|
surfaceId,
|
|
151
166
|
view.id,
|
|
@@ -180,13 +195,15 @@ class KeyboardAnimationCallback(
|
|
|
180
195
|
this.persistentKeyboardHeight = keyboardHeight
|
|
181
196
|
}
|
|
182
197
|
|
|
198
|
+
layoutObserver?.syncUpLayout()
|
|
183
199
|
this.emitEvent(
|
|
184
200
|
"KeyboardController::" + if (!isKeyboardVisible) "keyboardWillHide" else "keyboardWillShow",
|
|
185
201
|
getEventParams(keyboardHeight),
|
|
186
202
|
)
|
|
187
203
|
|
|
188
204
|
Log.i(TAG, "HEIGHT:: $keyboardHeight TAG:: $viewTagFocused")
|
|
189
|
-
|
|
205
|
+
context.dispatchEvent(
|
|
206
|
+
view.id,
|
|
190
207
|
KeyboardTransitionEvent(
|
|
191
208
|
surfaceId,
|
|
192
209
|
view.id,
|
|
@@ -227,10 +244,24 @@ class KeyboardAnimationCallback(
|
|
|
227
244
|
// do nothing, just log an exception send progress as 0
|
|
228
245
|
Log.w(TAG, "Caught arithmetic exception during `progress` calculation: $e")
|
|
229
246
|
}
|
|
230
|
-
Log.i(
|
|
247
|
+
Log.i(
|
|
248
|
+
TAG,
|
|
249
|
+
"DiffY: $diffY $height $progress ${InteractiveKeyboardProvider.isInteractive} $viewTagFocused",
|
|
250
|
+
)
|
|
231
251
|
|
|
232
252
|
val event = if (InteractiveKeyboardProvider.isInteractive) "topKeyboardMoveInteractive" else "topKeyboardMove"
|
|
233
|
-
|
|
253
|
+
context.dispatchEvent(
|
|
254
|
+
view.id,
|
|
255
|
+
KeyboardTransitionEvent(
|
|
256
|
+
surfaceId,
|
|
257
|
+
view.id,
|
|
258
|
+
event,
|
|
259
|
+
height,
|
|
260
|
+
progress,
|
|
261
|
+
duration,
|
|
262
|
+
viewTagFocused,
|
|
263
|
+
),
|
|
264
|
+
)
|
|
234
265
|
|
|
235
266
|
return insets
|
|
236
267
|
}
|
|
@@ -259,7 +290,8 @@ class KeyboardAnimationCallback(
|
|
|
259
290
|
"KeyboardController::" + if (!isKeyboardVisible) "keyboardDidHide" else "keyboardDidShow",
|
|
260
291
|
getEventParams(keyboardHeight),
|
|
261
292
|
)
|
|
262
|
-
|
|
293
|
+
context.dispatchEvent(
|
|
294
|
+
view.id,
|
|
263
295
|
KeyboardTransitionEvent(
|
|
264
296
|
surfaceId,
|
|
265
297
|
view.id,
|
|
@@ -275,6 +307,11 @@ class KeyboardAnimationCallback(
|
|
|
275
307
|
duration = 0
|
|
276
308
|
}
|
|
277
309
|
|
|
310
|
+
fun destroy() {
|
|
311
|
+
view.viewTreeObserver.removeOnGlobalFocusChangeListener(focusListener)
|
|
312
|
+
layoutObserver?.destroy()
|
|
313
|
+
}
|
|
314
|
+
|
|
278
315
|
private fun isKeyboardVisible(): Boolean {
|
|
279
316
|
val insets = ViewCompat.getRootWindowInsets(view)
|
|
280
317
|
|
|
@@ -290,12 +327,6 @@ class KeyboardAnimationCallback(
|
|
|
290
327
|
return (keyboardHeight - navigationBar).toFloat().dp.coerceAtLeast(0.0)
|
|
291
328
|
}
|
|
292
329
|
|
|
293
|
-
private fun sendEventToJS(event: Event<*>) {
|
|
294
|
-
val eventDispatcher: EventDispatcher? =
|
|
295
|
-
UIManagerHelper.getEventDispatcherForReactTag(context, view.id)
|
|
296
|
-
eventDispatcher?.dispatchEvent(event)
|
|
297
|
-
}
|
|
298
|
-
|
|
299
330
|
private fun emitEvent(event: String, params: WritableMap) {
|
|
300
331
|
context?.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter::class.java)?.emit(event, params)
|
|
301
332
|
|
|
@@ -11,6 +11,10 @@ class KeyboardControllerViewManagerImpl(mReactContext: ReactApplicationContext)
|
|
|
11
11
|
return EdgeToEdgeReactViewGroup(reactContext)
|
|
12
12
|
}
|
|
13
13
|
|
|
14
|
+
fun setEnabled(view: EdgeToEdgeReactViewGroup, enabled: Boolean) {
|
|
15
|
+
view.setActive(enabled)
|
|
16
|
+
}
|
|
17
|
+
|
|
14
18
|
fun setStatusBarTranslucent(view: EdgeToEdgeReactViewGroup, isStatusBarTranslucent: Boolean) {
|
|
15
19
|
view.setStatusBarTranslucent(isStatusBarTranslucent)
|
|
16
20
|
}
|
|
@@ -29,6 +33,8 @@ class KeyboardControllerViewManagerImpl(mReactContext: ReactApplicationContext)
|
|
|
29
33
|
MapBuilder.of("registrationName", "onKeyboardMoveEnd"),
|
|
30
34
|
"topKeyboardMoveInteractive",
|
|
31
35
|
MapBuilder.of("registrationName", "onKeyboardMoveInteractive"),
|
|
36
|
+
"topFocusedInputLayoutChanged",
|
|
37
|
+
MapBuilder.of("registrationName", "onFocusedInputLayoutChanged"),
|
|
32
38
|
)
|
|
33
39
|
|
|
34
40
|
return map
|
package/android/src/main/java/com/reactnativekeyboardcontroller/views/EdgeToEdgeReactViewGroup.kt
CHANGED
|
@@ -12,78 +12,47 @@ import androidx.core.view.WindowInsetsAnimationCompat
|
|
|
12
12
|
import androidx.core.view.WindowInsetsCompat
|
|
13
13
|
import com.facebook.react.uimanager.ThemedReactContext
|
|
14
14
|
import com.facebook.react.views.view.ReactViewGroup
|
|
15
|
-
import com.reactnativekeyboardcontroller.KeyboardAnimationCallback
|
|
16
15
|
import com.reactnativekeyboardcontroller.extensions.removeSelf
|
|
17
16
|
import com.reactnativekeyboardcontroller.extensions.requestApplyInsetsWhenAttached
|
|
18
17
|
import com.reactnativekeyboardcontroller.extensions.rootView
|
|
18
|
+
import com.reactnativekeyboardcontroller.listeners.KeyboardAnimationCallback
|
|
19
19
|
|
|
20
20
|
private val TAG = EdgeToEdgeReactViewGroup::class.qualifiedName
|
|
21
21
|
|
|
22
|
+
@Suppress("detekt:TooManyFunctions")
|
|
22
23
|
@SuppressLint("ViewConstructor")
|
|
23
24
|
class EdgeToEdgeReactViewGroup(private val reactContext: ThemedReactContext) : ReactViewGroup(reactContext) {
|
|
25
|
+
// props
|
|
24
26
|
private var isStatusBarTranslucent = false
|
|
25
27
|
private var isNavigationBarTranslucent = false
|
|
28
|
+
private var active = false
|
|
29
|
+
|
|
30
|
+
// internal class members
|
|
26
31
|
private var eventView: ReactViewGroup? = null
|
|
32
|
+
private var wasMounted = false
|
|
33
|
+
private var callback: KeyboardAnimationCallback? = null
|
|
27
34
|
|
|
28
35
|
// region View lifecycles
|
|
29
36
|
override fun onAttachedToWindow() {
|
|
30
37
|
super.onAttachedToWindow()
|
|
31
38
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
39
|
+
if (!wasMounted) {
|
|
40
|
+
// skip logic with callback re-creation if it was first render/mount
|
|
41
|
+
wasMounted = true
|
|
35
42
|
return
|
|
36
43
|
}
|
|
37
44
|
|
|
38
|
-
|
|
39
|
-
WindowCompat.setDecorFitsSystemWindows(
|
|
40
|
-
activity.window,
|
|
41
|
-
false,
|
|
42
|
-
)
|
|
43
|
-
|
|
44
|
-
eventView = ReactViewGroup(context)
|
|
45
|
-
val root = this.getContentView()
|
|
46
|
-
root?.addView(eventView)
|
|
47
|
-
|
|
48
|
-
val callback = KeyboardAnimationCallback(
|
|
49
|
-
view = this,
|
|
50
|
-
persistentInsetTypes = WindowInsetsCompat.Type.systemBars(),
|
|
51
|
-
deferredInsetTypes = WindowInsetsCompat.Type.ime(),
|
|
52
|
-
dispatchMode = WindowInsetsAnimationCompat.Callback.DISPATCH_MODE_CONTINUE_ON_SUBTREE,
|
|
53
|
-
context = reactContext,
|
|
54
|
-
)
|
|
55
|
-
|
|
56
|
-
eventView?.let {
|
|
57
|
-
ViewCompat.setWindowInsetsAnimationCallback(it, callback)
|
|
58
|
-
ViewCompat.setOnApplyWindowInsetsListener(it, callback)
|
|
59
|
-
it.requestApplyInsetsWhenAttached()
|
|
60
|
-
}
|
|
45
|
+
this.setupKeyboardCallbacks()
|
|
61
46
|
}
|
|
62
47
|
|
|
63
48
|
override fun onDetachedFromWindow() {
|
|
64
49
|
super.onDetachedFromWindow()
|
|
65
50
|
|
|
66
|
-
|
|
51
|
+
this.removeKeyboardCallbacks()
|
|
67
52
|
}
|
|
68
53
|
// endregion
|
|
69
54
|
|
|
70
|
-
// region
|
|
71
|
-
fun setStatusBarTranslucent(isStatusBarTranslucent: Boolean) {
|
|
72
|
-
this.isStatusBarTranslucent = isStatusBarTranslucent
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
fun setNavigationBarTranslucent(isNavigationBarTranslucent: Boolean) {
|
|
76
|
-
this.isNavigationBarTranslucent = isNavigationBarTranslucent
|
|
77
|
-
}
|
|
78
|
-
// endregion
|
|
79
|
-
|
|
80
|
-
// region Private functions/class helpers
|
|
81
|
-
private fun getContentView(): FitWindowsLinearLayout? {
|
|
82
|
-
return reactContext.currentActivity?.window?.decorView?.rootView?.findViewById(
|
|
83
|
-
androidx.appcompat.R.id.action_bar_root,
|
|
84
|
-
)
|
|
85
|
-
}
|
|
86
|
-
|
|
55
|
+
// region State manager helpers
|
|
87
56
|
private fun setupWindowInsets() {
|
|
88
57
|
val rootView = reactContext.rootView
|
|
89
58
|
if (rootView != null) {
|
|
@@ -94,16 +63,20 @@ class EdgeToEdgeReactViewGroup(private val reactContext: ThemedReactContext) : R
|
|
|
94
63
|
FrameLayout.LayoutParams.MATCH_PARENT,
|
|
95
64
|
)
|
|
96
65
|
|
|
66
|
+
val shouldApplyZeroPaddingTop = !active || this.isStatusBarTranslucent
|
|
67
|
+
val shouldApplyZeroPaddingBottom = !active || this.isNavigationBarTranslucent
|
|
97
68
|
params.setMargins(
|
|
98
69
|
0,
|
|
99
|
-
if (
|
|
70
|
+
if (shouldApplyZeroPaddingTop) {
|
|
100
71
|
0
|
|
101
72
|
} else {
|
|
102
|
-
|
|
103
|
-
|
|
73
|
+
(
|
|
74
|
+
insets?.getInsets(WindowInsetsCompat.Type.systemBars())?.top
|
|
75
|
+
?: 0
|
|
76
|
+
)
|
|
104
77
|
},
|
|
105
78
|
0,
|
|
106
|
-
if (
|
|
79
|
+
if (shouldApplyZeroPaddingBottom) {
|
|
107
80
|
0
|
|
108
81
|
} else {
|
|
109
82
|
insets?.getInsets(WindowInsetsCompat.Type.navigationBars())?.bottom
|
|
@@ -113,9 +86,106 @@ class EdgeToEdgeReactViewGroup(private val reactContext: ThemedReactContext) : R
|
|
|
113
86
|
|
|
114
87
|
content?.layoutParams = params
|
|
115
88
|
|
|
116
|
-
insets
|
|
89
|
+
val defaultInsets = ViewCompat.onApplyWindowInsets(v, insets)
|
|
90
|
+
|
|
91
|
+
defaultInsets.replaceSystemWindowInsets(
|
|
92
|
+
defaultInsets.systemWindowInsetLeft,
|
|
93
|
+
if (this.isStatusBarTranslucent) 0 else defaultInsets.systemWindowInsetTop,
|
|
94
|
+
defaultInsets.systemWindowInsetRight,
|
|
95
|
+
defaultInsets.systemWindowInsetBottom,
|
|
96
|
+
)
|
|
117
97
|
}
|
|
118
98
|
}
|
|
119
99
|
}
|
|
100
|
+
|
|
101
|
+
private fun goToEdgeToEdge(edgeToEdge: Boolean) {
|
|
102
|
+
reactContext.currentActivity?.let {
|
|
103
|
+
WindowCompat.setDecorFitsSystemWindows(
|
|
104
|
+
it.window,
|
|
105
|
+
!edgeToEdge,
|
|
106
|
+
)
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
private fun setupKeyboardCallbacks() {
|
|
111
|
+
val activity = reactContext.currentActivity
|
|
112
|
+
|
|
113
|
+
if (activity != null) {
|
|
114
|
+
eventView = ReactViewGroup(context)
|
|
115
|
+
val root = this.getContentView()
|
|
116
|
+
root?.addView(eventView)
|
|
117
|
+
|
|
118
|
+
callback = KeyboardAnimationCallback(
|
|
119
|
+
view = this,
|
|
120
|
+
persistentInsetTypes = WindowInsetsCompat.Type.systemBars(),
|
|
121
|
+
deferredInsetTypes = WindowInsetsCompat.Type.ime(),
|
|
122
|
+
dispatchMode = WindowInsetsAnimationCompat.Callback.DISPATCH_MODE_CONTINUE_ON_SUBTREE,
|
|
123
|
+
context = reactContext,
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
eventView?.let {
|
|
127
|
+
ViewCompat.setWindowInsetsAnimationCallback(it, callback)
|
|
128
|
+
ViewCompat.setOnApplyWindowInsetsListener(it, callback)
|
|
129
|
+
it.requestApplyInsetsWhenAttached()
|
|
130
|
+
}
|
|
131
|
+
} else {
|
|
132
|
+
Log.w(TAG, "Can not setup keyboard animation listener, since `currentActivity` is null")
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
private fun removeKeyboardCallbacks() {
|
|
137
|
+
callback?.destroy()
|
|
138
|
+
|
|
139
|
+
// capture view into closure, because if `onDetachedFromWindow` and `onAttachedToWindow`
|
|
140
|
+
// dispatched synchronously after each other (open application on Fabric), then `.post`
|
|
141
|
+
// will destroy just newly created view (if we have a reference via `this`)
|
|
142
|
+
// and we'll have a memory leak or zombie-view
|
|
143
|
+
val view = eventView
|
|
144
|
+
// we need to remove view asynchronously from `onDetachedFromWindow` method
|
|
145
|
+
// otherwise we may face NPE when app is getting opened via universal link
|
|
146
|
+
// see https://github.com/kirillzyusko/react-native-keyboard-controller/issues/242
|
|
147
|
+
// for more details
|
|
148
|
+
Handler(Looper.getMainLooper()).post { view.removeSelf() }
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
private fun getContentView(): FitWindowsLinearLayout? {
|
|
152
|
+
return reactContext.currentActivity?.window?.decorView?.rootView?.findViewById(
|
|
153
|
+
androidx.appcompat.R.id.action_bar_root,
|
|
154
|
+
)
|
|
155
|
+
}
|
|
156
|
+
// endregion
|
|
157
|
+
|
|
158
|
+
// region State managers
|
|
159
|
+
private fun enable() {
|
|
160
|
+
this.goToEdgeToEdge(true)
|
|
161
|
+
this.setupWindowInsets()
|
|
162
|
+
this.setupKeyboardCallbacks()
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
private fun disable() {
|
|
166
|
+
this.goToEdgeToEdge(false)
|
|
167
|
+
this.setupWindowInsets()
|
|
168
|
+
this.removeKeyboardCallbacks()
|
|
169
|
+
}
|
|
170
|
+
// endregion
|
|
171
|
+
|
|
172
|
+
// region Props setters
|
|
173
|
+
fun setStatusBarTranslucent(isStatusBarTranslucent: Boolean) {
|
|
174
|
+
this.isStatusBarTranslucent = isStatusBarTranslucent
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
fun setNavigationBarTranslucent(isNavigationBarTranslucent: Boolean) {
|
|
178
|
+
this.isNavigationBarTranslucent = isNavigationBarTranslucent
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
fun setActive(active: Boolean) {
|
|
182
|
+
this.active = active
|
|
183
|
+
|
|
184
|
+
if (active) {
|
|
185
|
+
this.enable()
|
|
186
|
+
} else {
|
|
187
|
+
this.disable()
|
|
188
|
+
}
|
|
189
|
+
}
|
|
120
190
|
// endregion
|
|
121
191
|
}
|