react-native-keyboard-controller 1.4.4 → 1.5.1
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 +1 -1
- package/android/build.gradle +9 -2
- package/android/gradle.properties +2 -2
- package/android/react-native-helpers.gradle +48 -0
- package/android/src/fabric/java/com/reactnativekeyboardcontroller/KeyboardGestureAreaViewManager.kt +44 -0
- package/android/src/main/java/com/reactnativekeyboardcontroller/InteractiveKeyboardProvider.kt +6 -0
- package/android/src/main/java/com/reactnativekeyboardcontroller/KeyboardAnimationCallback.kt +27 -13
- package/android/src/main/java/com/reactnativekeyboardcontroller/KeyboardAnimationController.kt +394 -0
- package/android/src/main/java/com/reactnativekeyboardcontroller/KeyboardControllerPackage.kt +1 -1
- package/android/src/main/java/com/reactnativekeyboardcontroller/events/KeyboardTransitionEvent.kt +2 -2
- package/android/src/main/java/com/reactnativekeyboardcontroller/extensions/Float.kt +4 -4
- package/android/src/main/java/com/reactnativekeyboardcontroller/extensions/View.kt +21 -0
- package/android/src/main/java/com/reactnativekeyboardcontroller/interpolators/Interpolator.kt +14 -0
- package/android/src/main/java/com/reactnativekeyboardcontroller/interpolators/IosInterpolator.kt +18 -0
- package/android/src/main/java/com/reactnativekeyboardcontroller/interpolators/LinearInterpolator.kt +11 -0
- package/android/src/main/java/com/reactnativekeyboardcontroller/managers/KeyboardControllerViewManagerImpl.kt +2 -0
- package/android/src/main/java/com/reactnativekeyboardcontroller/managers/KeyboardGestureAreaViewManagerImpl.kt +27 -0
- package/android/src/main/java/com/reactnativekeyboardcontroller/views/KeyboardGestureAreaReactViewGroup.kt +192 -0
- package/android/src/paper/java/com/reactnativekeyboardcontroller/KeyboardGestureAreaViewManager.kt +33 -0
- package/ios/KeyboardControllerView.mm +10 -3
- package/ios/KeyboardControllerViewManager.mm +1 -0
- package/ios/KeyboardControllerViewManager.swift +1 -0
- package/ios/KeyboardMovementObserver.swift +78 -4
- package/jest/index.js +1 -0
- package/lib/commonjs/animated.js +13 -19
- package/lib/commonjs/animated.js.map +1 -1
- package/lib/commonjs/context.js +0 -3
- package/lib/commonjs/context.js.map +1 -1
- package/lib/commonjs/hooks.js +0 -13
- package/lib/commonjs/hooks.js.map +1 -1
- package/lib/commonjs/index.js +0 -13
- package/lib/commonjs/index.js.map +1 -1
- package/lib/commonjs/internal.js +13 -22
- package/lib/commonjs/internal.js.map +1 -1
- package/lib/commonjs/monkey-patch.js +3 -12
- package/lib/commonjs/monkey-patch.js.map +1 -1
- package/lib/commonjs/native.js +10 -10
- package/lib/commonjs/native.js.map +1 -1
- package/lib/commonjs/replicas.js +3 -19
- package/lib/commonjs/replicas.js.map +1 -1
- package/lib/commonjs/specs/KeyboardControllerViewNativeComponent.js +0 -5
- package/lib/commonjs/specs/KeyboardControllerViewNativeComponent.js.map +1 -1
- package/lib/commonjs/specs/KeyboardGestureAreaNativeComponent.js +13 -0
- package/lib/commonjs/specs/KeyboardGestureAreaNativeComponent.js.map +1 -0
- package/lib/commonjs/specs/NativeKeyboardController.js +0 -3
- package/lib/commonjs/specs/NativeKeyboardController.js.map +1 -1
- package/lib/commonjs/specs/NativeStatusBarManagerCompat.js +0 -3
- package/lib/commonjs/specs/NativeStatusBarManagerCompat.js.map +1 -1
- package/lib/commonjs/types.js.map +1 -1
- package/lib/commonjs/utils.js +0 -2
- package/lib/commonjs/utils.js.map +1 -1
- package/lib/module/animated.js +13 -7
- package/lib/module/animated.js.map +1 -1
- package/lib/module/context.js.map +1 -1
- package/lib/module/hooks.js.map +1 -1
- package/lib/module/index.js.map +1 -1
- package/lib/module/internal.js +13 -16
- package/lib/module/internal.js.map +1 -1
- package/lib/module/monkey-patch.js +4 -10
- package/lib/module/monkey-patch.js.map +1 -1
- package/lib/module/native.js +8 -5
- package/lib/module/native.js.map +1 -1
- package/lib/module/replicas.js +3 -5
- package/lib/module/replicas.js.map +1 -1
- package/lib/module/specs/KeyboardControllerViewNativeComponent.js +0 -1
- package/lib/module/specs/KeyboardControllerViewNativeComponent.js.map +1 -1
- package/lib/module/specs/KeyboardGestureAreaNativeComponent.js +5 -0
- package/lib/module/specs/KeyboardGestureAreaNativeComponent.js.map +1 -0
- 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/module/utils.js.map +1 -1
- package/lib/typescript/animated.d.ts +1 -1
- package/lib/typescript/context.d.ts +3 -3
- package/lib/typescript/internal.d.ts +1 -0
- package/lib/typescript/native.d.ts +2 -1
- package/lib/typescript/replicas.d.ts +1 -1
- package/lib/typescript/specs/KeyboardControllerViewNativeComponent.d.ts +6 -5
- package/lib/typescript/specs/KeyboardGestureAreaNativeComponent.d.ts +10 -0
- package/lib/typescript/types.d.ts +29 -13
- package/package.json +11 -12
- package/src/animated.tsx +7 -0
- package/src/internal.ts +20 -3
- package/src/native.ts +5 -0
- package/src/specs/KeyboardControllerViewNativeComponent.ts +5 -6
- package/src/specs/KeyboardGestureAreaNativeComponent.ts +14 -0
- package/src/types.ts +24 -5
package/README.md
CHANGED
|
@@ -12,7 +12,7 @@ Keyboard manager which works in identical way on both iOS and Android.
|
|
|
12
12
|
- missing `keyboardWillShow` / `keyboardWillHide` events are available on Android 😍
|
|
13
13
|
- module for changing soft input mode on Android 🤔
|
|
14
14
|
- reanimated support 🚀
|
|
15
|
-
- interactive keyboard dismissing
|
|
15
|
+
- interactive keyboard dismissing 👆📱
|
|
16
16
|
- and more is coming... Stay tuned! 😊
|
|
17
17
|
|
|
18
18
|
## Installation
|
package/android/build.gradle
CHANGED
|
@@ -33,6 +33,7 @@ def reactNativeArchitectures() {
|
|
|
33
33
|
|
|
34
34
|
apply plugin: 'com.android.library'
|
|
35
35
|
apply plugin: 'kotlin-android'
|
|
36
|
+
apply from: "$projectDir/react-native-helpers.gradle"
|
|
36
37
|
|
|
37
38
|
if (isNewArchitectureEnabled()) {
|
|
38
39
|
apply plugin: 'com.facebook.react'
|
|
@@ -151,8 +152,14 @@ repositories {
|
|
|
151
152
|
def kotlin_version = getExtOrDefault('kotlinVersion')
|
|
152
153
|
|
|
153
154
|
dependencies {
|
|
154
|
-
|
|
155
|
-
|
|
155
|
+
if (project.ext.shouldConsumeReactNativeFromMavenCentral()) {
|
|
156
|
+
// noinspection GradleDynamicVersion
|
|
157
|
+
implementation 'com.facebook.react:react-android:+'
|
|
158
|
+
} else {
|
|
159
|
+
// noinspection GradleDynamicVersion
|
|
160
|
+
implementation 'com.facebook.react:react-native:+'
|
|
161
|
+
}
|
|
156
162
|
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
|
|
157
163
|
implementation 'androidx.core:core-ktx:1.5.0-beta03'
|
|
164
|
+
implementation 'androidx.dynamicanimation:dynamicanimation-ktx:1.0.0-alpha03'
|
|
158
165
|
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
def safeAppExtGet(prop, fallback) {
|
|
2
|
+
def appProject = rootProject.allprojects.find { it.plugins.hasPlugin('com.android.application') }
|
|
3
|
+
appProject?.ext?.has(prop) ? appProject.ext.get(prop) : fallback
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
// Let's detect react-native's directory, it will be used to determine RN's version
|
|
7
|
+
// https://github.com/software-mansion/react-native-reanimated/blob/cda4627c3337c33674f05f755b7485165c6caca9/android/build.gradle#L88
|
|
8
|
+
def resolveReactNativeDirectory() {
|
|
9
|
+
def reactNativeLocation = safeAppExtGet("REACT_NATIVE_NODE_MODULES_DIR", null)
|
|
10
|
+
if (reactNativeLocation != null) {
|
|
11
|
+
return file(reactNativeLocation)
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
// monorepo workaround
|
|
15
|
+
// react-native can be hoisted or in project's own node_modules
|
|
16
|
+
def reactNativeFromProjectNodeModules = file("${rootProject.projectDir}/../node_modules/react-native")
|
|
17
|
+
if (reactNativeFromProjectNodeModules.exists()) {
|
|
18
|
+
return reactNativeFromProjectNodeModules
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
def reactNativeFromNodeModules = file("${projectDir}/../../react-native")
|
|
22
|
+
if (reactNativeFromNodeModules.exists()) {
|
|
23
|
+
return reactNativeFromNodeModules
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
throw new GradleException(
|
|
27
|
+
"${project.name}: unable to resolve react-native location in " +
|
|
28
|
+
"node_modules. You should project extension property (in app/build.gradle) " +
|
|
29
|
+
"`REACT_NATIVE_NODE_MODULES_DIR` with path to react-native."
|
|
30
|
+
)
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// https://github.com/software-mansion/react-native-reanimated/blob/cda4627c3337c33674f05f755b7485165c6caca9/android/build.gradle#L199#L205
|
|
34
|
+
def reactNativeRootDir = resolveReactNativeDirectory()
|
|
35
|
+
|
|
36
|
+
def reactProperties = new Properties()
|
|
37
|
+
file("$reactNativeRootDir/ReactAndroid/gradle.properties").withInputStream { reactProperties.load(it) }
|
|
38
|
+
|
|
39
|
+
def REACT_NATIVE_VERSION = reactProperties.getProperty("VERSION_NAME")
|
|
40
|
+
def REACT_NATIVE_MINOR_VERSION = REACT_NATIVE_VERSION.startsWith("0.0.0-") ? 1000 : REACT_NATIVE_VERSION.split("\\.")[1].toInteger()
|
|
41
|
+
|
|
42
|
+
project.ext.resolveReactNativeDirectory = { ->
|
|
43
|
+
return resolveReactNativeDirectory()
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
project.ext.shouldConsumeReactNativeFromMavenCentral = { ->
|
|
47
|
+
return REACT_NATIVE_MINOR_VERSION >= 71
|
|
48
|
+
}
|
package/android/src/fabric/java/com/reactnativekeyboardcontroller/KeyboardGestureAreaViewManager.kt
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
package com.reactnativekeyboardcontroller
|
|
2
|
+
|
|
3
|
+
import com.facebook.react.bridge.ReactApplicationContext
|
|
4
|
+
import com.facebook.react.uimanager.ThemedReactContext
|
|
5
|
+
import com.facebook.react.uimanager.ViewManagerDelegate
|
|
6
|
+
import com.facebook.react.uimanager.annotations.ReactProp
|
|
7
|
+
import com.facebook.react.viewmanagers.KeyboardGestureAreaManagerDelegate
|
|
8
|
+
import com.facebook.react.viewmanagers.KeyboardGestureAreaManagerInterface
|
|
9
|
+
import com.facebook.react.views.view.ReactViewGroup
|
|
10
|
+
import com.facebook.react.views.view.ReactViewManager
|
|
11
|
+
import com.reactnativekeyboardcontroller.managers.KeyboardGestureAreaViewManagerImpl
|
|
12
|
+
import com.reactnativekeyboardcontroller.views.KeyboardGestureAreaReactViewGroup
|
|
13
|
+
|
|
14
|
+
class KeyboardGestureAreaViewManager(mReactContext: ReactApplicationContext) :
|
|
15
|
+
ReactViewManager(),
|
|
16
|
+
KeyboardGestureAreaManagerInterface<ReactViewGroup> {
|
|
17
|
+
private val manager = KeyboardGestureAreaViewManagerImpl(mReactContext)
|
|
18
|
+
private val mDelegate = KeyboardGestureAreaManagerDelegate(this)
|
|
19
|
+
|
|
20
|
+
override fun getDelegate(): ViewManagerDelegate<ReactViewGroup?> {
|
|
21
|
+
return mDelegate
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
override fun getName(): String = KeyboardGestureAreaViewManagerImpl.NAME
|
|
25
|
+
|
|
26
|
+
override fun createViewInstance(context: ThemedReactContext): KeyboardGestureAreaReactViewGroup {
|
|
27
|
+
return manager.createViewInstance(context)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
@ReactProp(name = "interpolator")
|
|
31
|
+
override fun setInterpolator(view: ReactViewGroup, value: String?) {
|
|
32
|
+
manager.setInterpolator(view as KeyboardGestureAreaReactViewGroup, value ?: "linear")
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
@ReactProp(name = "showOnSwipeUp")
|
|
36
|
+
override fun setShowOnSwipeUp(view: ReactViewGroup, value: Boolean) {
|
|
37
|
+
manager.setScrollKeyboardOnScreenWhenNotVisible(view as KeyboardGestureAreaReactViewGroup, value)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
@ReactProp(name = "enableSwipeToDismiss")
|
|
41
|
+
override fun setEnableSwipeToDismiss(view: ReactViewGroup?, value: Boolean) {
|
|
42
|
+
manager.setScrollKeyboardOffScreenWhenVisible(view as KeyboardGestureAreaReactViewGroup, value)
|
|
43
|
+
}
|
|
44
|
+
}
|
package/android/src/main/java/com/reactnativekeyboardcontroller/KeyboardAnimationCallback.kt
CHANGED
|
@@ -20,6 +20,7 @@ import com.facebook.react.uimanager.events.EventDispatcher
|
|
|
20
20
|
import com.facebook.react.views.view.ReactViewGroup
|
|
21
21
|
import com.reactnativekeyboardcontroller.events.KeyboardTransitionEvent
|
|
22
22
|
import com.reactnativekeyboardcontroller.extensions.dp
|
|
23
|
+
import kotlin.math.abs
|
|
23
24
|
|
|
24
25
|
class KeyboardAnimationCallback(
|
|
25
26
|
val view: ReactViewGroup,
|
|
@@ -30,7 +31,7 @@ class KeyboardAnimationCallback(
|
|
|
30
31
|
val onApplyWindowInsetsListener: OnApplyWindowInsetsListener,
|
|
31
32
|
) : WindowInsetsAnimationCompat.Callback(dispatchMode), OnApplyWindowInsetsListener {
|
|
32
33
|
private val TAG = KeyboardAnimationCallback::class.qualifiedName
|
|
33
|
-
private var persistentKeyboardHeight = 0
|
|
34
|
+
private var persistentKeyboardHeight = 0.0
|
|
34
35
|
private var isKeyboardVisible = false
|
|
35
36
|
private var isTransitioning = false
|
|
36
37
|
|
|
@@ -62,16 +63,16 @@ class KeyboardAnimationCallback(
|
|
|
62
63
|
// having such check allows us not to dispatch unnecessary incorrect events
|
|
63
64
|
// the condition will be executed only when keyboard is opened and changes its size
|
|
64
65
|
// (for example it happens when user changes keyboard type from 'text' to 'emoji' input
|
|
65
|
-
if (isKeyboardVisible && isKeyboardVisible() && !isTransitioning && Build.VERSION.SDK_INT >= 30) {
|
|
66
|
+
if (isKeyboardVisible && isKeyboardVisible() && !isTransitioning && Build.VERSION.SDK_INT >= 30 && !InteractiveKeyboardProvider.isInteractive) {
|
|
66
67
|
val keyboardHeight = getCurrentKeyboardHeight()
|
|
67
68
|
|
|
68
69
|
this.emitEvent("KeyboardController::keyboardWillShow", getEventParams(keyboardHeight))
|
|
69
70
|
this.sendEventToJS(KeyboardTransitionEvent(view.id, "topKeyboardMoveStart", keyboardHeight, 1.0))
|
|
70
71
|
|
|
71
|
-
val animation = ValueAnimator.
|
|
72
|
+
val animation = ValueAnimator.ofFloat(this.persistentKeyboardHeight.toFloat(), keyboardHeight.toFloat())
|
|
72
73
|
animation.addUpdateListener { animator ->
|
|
73
|
-
val toValue = animator.animatedValue as
|
|
74
|
-
this.sendEventToJS(KeyboardTransitionEvent(view.id, "topKeyboardMove", toValue, toValue.toDouble() / keyboardHeight))
|
|
74
|
+
val toValue = animator.animatedValue as Float
|
|
75
|
+
this.sendEventToJS(KeyboardTransitionEvent(view.id, "topKeyboardMove", toValue.toDouble(), toValue.toDouble() / keyboardHeight))
|
|
75
76
|
}
|
|
76
77
|
animation.doOnEnd {
|
|
77
78
|
this.emitEvent("KeyboardController::keyboardDidShow", getEventParams(keyboardHeight))
|
|
@@ -128,13 +129,14 @@ class KeyboardAnimationCallback(
|
|
|
128
129
|
|
|
129
130
|
var progress = 0.0
|
|
130
131
|
try {
|
|
131
|
-
progress =
|
|
132
|
+
progress = abs((height / persistentKeyboardHeight)).let { if (it.isNaN()) 0.0 else it }
|
|
132
133
|
} catch (e: ArithmeticException) {
|
|
133
134
|
// do nothing, send progress as 0
|
|
134
135
|
}
|
|
135
|
-
Log.i(TAG, "DiffY: $diffY $height $progress")
|
|
136
|
+
Log.i(TAG, "DiffY: $diffY $height $progress ${InteractiveKeyboardProvider.isInteractive}")
|
|
136
137
|
|
|
137
|
-
|
|
138
|
+
val event = if (InteractiveKeyboardProvider.isInteractive) "topKeyboardMoveInteractive" else "topKeyboardMove"
|
|
139
|
+
this.sendEventToJS(KeyboardTransitionEvent(view.id, event, height, progress))
|
|
138
140
|
|
|
139
141
|
return insets
|
|
140
142
|
}
|
|
@@ -143,7 +145,19 @@ class KeyboardAnimationCallback(
|
|
|
143
145
|
super.onEnd(animation)
|
|
144
146
|
|
|
145
147
|
isTransitioning = false
|
|
146
|
-
|
|
148
|
+
// if keyboard becomes shown after interactive animation completion
|
|
149
|
+
// getCurrentKeyboardHeight() will be `0` and isKeyboardVisible will be `false`
|
|
150
|
+
// it's not correct behavior, so we are handling it here
|
|
151
|
+
val isKeyboardShown = InteractiveKeyboardProvider.shown
|
|
152
|
+
if (!isKeyboardShown) {
|
|
153
|
+
this.persistentKeyboardHeight = getCurrentKeyboardHeight()
|
|
154
|
+
} else {
|
|
155
|
+
// if keyboard is shown after interactions and the animation has finished
|
|
156
|
+
// then we need to reset the state
|
|
157
|
+
InteractiveKeyboardProvider.shown = false
|
|
158
|
+
}
|
|
159
|
+
val isKeyboardVisible = isKeyboardVisible || isKeyboardShown
|
|
160
|
+
|
|
147
161
|
this.emitEvent("KeyboardController::" + if (!isKeyboardVisible) "keyboardDidHide" else "keyboardDidShow", getEventParams(this.persistentKeyboardHeight))
|
|
148
162
|
this.sendEventToJS(KeyboardTransitionEvent(view.id, "topKeyboardMoveEnd", this.persistentKeyboardHeight, if (!isKeyboardVisible) 0.0 else 1.0))
|
|
149
163
|
}
|
|
@@ -154,13 +168,13 @@ class KeyboardAnimationCallback(
|
|
|
154
168
|
return insets?.isVisible(WindowInsetsCompat.Type.ime()) ?: false
|
|
155
169
|
}
|
|
156
170
|
|
|
157
|
-
private fun getCurrentKeyboardHeight():
|
|
171
|
+
private fun getCurrentKeyboardHeight(): Double {
|
|
158
172
|
val insets = ViewCompat.getRootWindowInsets(view)
|
|
159
173
|
val keyboardHeight = insets?.getInsets(WindowInsetsCompat.Type.ime())?.bottom ?: 0
|
|
160
174
|
val navigationBar = insets?.getInsets(WindowInsetsCompat.Type.navigationBars())?.bottom ?: 0
|
|
161
175
|
|
|
162
176
|
// on hide it will be negative value, so we are using max function
|
|
163
|
-
return
|
|
177
|
+
return (keyboardHeight - navigationBar).toFloat().dp.coerceAtLeast(0.0)
|
|
164
178
|
}
|
|
165
179
|
|
|
166
180
|
private fun sendEventToJS(event: Event<*>) {
|
|
@@ -175,9 +189,9 @@ class KeyboardAnimationCallback(
|
|
|
175
189
|
Log.i(TAG, event)
|
|
176
190
|
}
|
|
177
191
|
|
|
178
|
-
private fun getEventParams(height:
|
|
192
|
+
private fun getEventParams(height: Double): WritableMap {
|
|
179
193
|
val params: WritableMap = Arguments.createMap()
|
|
180
|
-
params.
|
|
194
|
+
params.putDouble("height", height)
|
|
181
195
|
|
|
182
196
|
return params
|
|
183
197
|
}
|
package/android/src/main/java/com/reactnativekeyboardcontroller/KeyboardAnimationController.kt
ADDED
|
@@ -0,0 +1,394 @@
|
|
|
1
|
+
package com.reactnativekeyboardcontroller
|
|
2
|
+
|
|
3
|
+
import android.os.CancellationSignal
|
|
4
|
+
import android.view.View
|
|
5
|
+
import android.view.animation.LinearInterpolator
|
|
6
|
+
import androidx.core.graphics.Insets
|
|
7
|
+
import androidx.core.view.ViewCompat
|
|
8
|
+
import androidx.core.view.WindowInsetsAnimationControlListenerCompat
|
|
9
|
+
import androidx.core.view.WindowInsetsAnimationControllerCompat
|
|
10
|
+
import androidx.core.view.WindowInsetsCompat
|
|
11
|
+
import androidx.dynamicanimation.animation.SpringAnimation
|
|
12
|
+
import androidx.dynamicanimation.animation.SpringForce
|
|
13
|
+
import androidx.dynamicanimation.animation.springAnimationOf
|
|
14
|
+
import androidx.dynamicanimation.animation.withSpringForceProperties
|
|
15
|
+
import kotlin.math.roundToInt
|
|
16
|
+
|
|
17
|
+
internal class KeyboardAnimationController {
|
|
18
|
+
private var insetsAnimationController: WindowInsetsAnimationControllerCompat? = null
|
|
19
|
+
private var pendingRequestCancellationSignal: CancellationSignal? = null
|
|
20
|
+
private var pendingRequestOnReady: ((WindowInsetsAnimationControllerCompat) -> Unit)? = null
|
|
21
|
+
|
|
22
|
+
/* To take control of the an WindowInsetsAnimation, we need to pass in a listener to
|
|
23
|
+
controlWindowInsetsAnimation() in startControlRequest(). The listener created here
|
|
24
|
+
keeps track of the current WindowInsetsAnimationController and resets our state. */
|
|
25
|
+
private val animationControlListener by lazy {
|
|
26
|
+
object : WindowInsetsAnimationControlListenerCompat {
|
|
27
|
+
/**
|
|
28
|
+
* Once the request is ready, call our [onRequestReady] function
|
|
29
|
+
*/
|
|
30
|
+
override fun onReady(
|
|
31
|
+
controller: WindowInsetsAnimationControllerCompat,
|
|
32
|
+
types: Int,
|
|
33
|
+
) = onRequestReady(controller)
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* If the request is finished, we should reset our internal state
|
|
37
|
+
*/
|
|
38
|
+
override fun onFinished(controller: WindowInsetsAnimationControllerCompat) {
|
|
39
|
+
reset()
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* If the request is cancelled, we should reset our internal state
|
|
44
|
+
*/
|
|
45
|
+
override fun onCancelled(controller: WindowInsetsAnimationControllerCompat?) {
|
|
46
|
+
reset()
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* True if the IME was shown at the start of the current animation.
|
|
53
|
+
*/
|
|
54
|
+
private var isImeShownAtStart = false
|
|
55
|
+
|
|
56
|
+
private var currentSpringAnimation: SpringAnimation? = null
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Start a control request to the [view]s [android.view.WindowInsetsController]. This should
|
|
60
|
+
* be called once the view is in a position to take control over the position of the IME.
|
|
61
|
+
*
|
|
62
|
+
* @param view The view which is triggering this request
|
|
63
|
+
* @param onRequestReady optional listener which will be called when the request is ready and
|
|
64
|
+
* the animation can proceed
|
|
65
|
+
*/
|
|
66
|
+
fun startControlRequest(
|
|
67
|
+
view: View,
|
|
68
|
+
onRequestReady: ((WindowInsetsAnimationControllerCompat) -> Unit)? = null,
|
|
69
|
+
) {
|
|
70
|
+
check(!isInsetAnimationInProgress()) {
|
|
71
|
+
"Animation in progress. Can not start a new request to controlWindowInsetsAnimation()"
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Keep track of the IME insets, and the IME visibility, at the start of the request
|
|
75
|
+
isImeShownAtStart = ViewCompat.getRootWindowInsets(view)
|
|
76
|
+
?.isVisible(WindowInsetsCompat.Type.ime()) == true
|
|
77
|
+
|
|
78
|
+
// Create a cancellation signal, which we pass to controlWindowInsetsAnimation() below
|
|
79
|
+
pendingRequestCancellationSignal = CancellationSignal()
|
|
80
|
+
// Keep reference to the onReady callback
|
|
81
|
+
pendingRequestOnReady = onRequestReady
|
|
82
|
+
|
|
83
|
+
// update our state manager
|
|
84
|
+
InteractiveKeyboardProvider.isInteractive = true
|
|
85
|
+
|
|
86
|
+
// Finally we make a controlWindowInsetsAnimation() request:
|
|
87
|
+
ViewCompat.getWindowInsetsController(view)?.controlWindowInsetsAnimation(
|
|
88
|
+
// We're only catering for IME animations in this listener
|
|
89
|
+
WindowInsetsCompat.Type.ime(),
|
|
90
|
+
// Animation duration. This is not used by the system, and is only passed to any
|
|
91
|
+
// WindowInsetsAnimation.Callback set on views. We pass in -1 to indicate that we're
|
|
92
|
+
// not starting a finite animation, and that this is completely controlled by
|
|
93
|
+
// the user's touch.
|
|
94
|
+
-1,
|
|
95
|
+
// The time interpolator used in calculating the animation progress. The fraction value
|
|
96
|
+
// we passed into setInsetsAndAlpha() which be passed into this interpolator before
|
|
97
|
+
// being used by the system to inset the IME. LinearInterpolator is a good type
|
|
98
|
+
// to use for scrolling gestures.
|
|
99
|
+
linearInterpolator,
|
|
100
|
+
// A cancellation signal, which allows us to cancel the request to control
|
|
101
|
+
pendingRequestCancellationSignal,
|
|
102
|
+
// The WindowInsetsAnimationControlListener
|
|
103
|
+
animationControlListener,
|
|
104
|
+
)
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Start a control request to the [view]s [android.view.WindowInsetsController], similar to
|
|
109
|
+
* [startControlRequest], but immediately fling to a finish using [velocityY] once ready.
|
|
110
|
+
*
|
|
111
|
+
* This function is useful for fire-and-forget operations to animate the IME.
|
|
112
|
+
*
|
|
113
|
+
* @param view The view which is triggering this request
|
|
114
|
+
* @param velocityY the velocity of the touch gesture which caused this call
|
|
115
|
+
*/
|
|
116
|
+
fun startAndFling(view: View, velocityY: Float) = startControlRequest(view) {
|
|
117
|
+
animateToFinish(velocityY)
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Update the inset position of the IME by the given [dy] value. This value will be coerced
|
|
122
|
+
* into the hidden and shown inset values.
|
|
123
|
+
*
|
|
124
|
+
* This function should only be called if [isInsetAnimationInProgress] returns true.
|
|
125
|
+
*
|
|
126
|
+
* @return the amount of [dy] consumed by the inset animation, in pixels
|
|
127
|
+
*/
|
|
128
|
+
fun insetBy(dy: Int): Int {
|
|
129
|
+
val controller = insetsAnimationController
|
|
130
|
+
?: throw IllegalStateException(
|
|
131
|
+
"Current WindowInsetsAnimationController is null." +
|
|
132
|
+
"This should only be called if isAnimationInProgress() returns true",
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
InteractiveKeyboardProvider.isInteractive = true
|
|
136
|
+
// Call updateInsetTo() with the new inset value
|
|
137
|
+
return insetTo(controller.currentInsets.bottom - dy)
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Update the inset position of the IME to be the given [inset] value. This value will be
|
|
142
|
+
* coerced into the hidden and shown inset values.
|
|
143
|
+
*
|
|
144
|
+
* This function should only be called if [isInsetAnimationInProgress] returns true.
|
|
145
|
+
*
|
|
146
|
+
* @return the distance moved by the inset animation, in pixels
|
|
147
|
+
*/
|
|
148
|
+
fun insetTo(inset: Int): Int {
|
|
149
|
+
val controller = insetsAnimationController
|
|
150
|
+
?: throw IllegalStateException(
|
|
151
|
+
"Current WindowInsetsAnimationController is null." +
|
|
152
|
+
"This should only be called if isAnimationInProgress() returns true",
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
val hiddenBottom = controller.hiddenStateInsets.bottom
|
|
156
|
+
val shownBottom = controller.shownStateInsets.bottom
|
|
157
|
+
val startBottom = if (isImeShownAtStart) shownBottom else hiddenBottom
|
|
158
|
+
val endBottom = if (isImeShownAtStart) hiddenBottom else shownBottom
|
|
159
|
+
|
|
160
|
+
// We coerce the given inset within the limits of the hidden and shown insets
|
|
161
|
+
val coercedBottom = inset.coerceIn(hiddenBottom, shownBottom)
|
|
162
|
+
|
|
163
|
+
val consumedDy = controller.currentInsets.bottom - coercedBottom
|
|
164
|
+
|
|
165
|
+
// Finally update the insets in the WindowInsetsAnimationController using
|
|
166
|
+
// setInsetsAndAlpha().
|
|
167
|
+
controller.setInsetsAndAlpha(
|
|
168
|
+
// Here we update the animating insets. This is what controls where the IME is displayed.
|
|
169
|
+
// It is also passed through to views via their WindowInsetsAnimation.Callback.
|
|
170
|
+
Insets.of(0, 0, 0, coercedBottom),
|
|
171
|
+
// This controls the alpha value. We don't want to alter the alpha so use 1f
|
|
172
|
+
1f,
|
|
173
|
+
// Finally we calculate the animation progress fraction. This value is passed through
|
|
174
|
+
// to any WindowInsetsAnimation.Callbacks, but it is not used by the system.
|
|
175
|
+
(coercedBottom - startBottom) / (endBottom - startBottom).toFloat(),
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
return consumedDy
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Return `true` if an inset animation is in progress.
|
|
183
|
+
*/
|
|
184
|
+
fun isInsetAnimationInProgress(): Boolean {
|
|
185
|
+
return insetsAnimationController != null
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Return `true` if an inset animation is currently finishing.
|
|
190
|
+
*/
|
|
191
|
+
fun isInsetAnimationFinishing(): Boolean {
|
|
192
|
+
return currentSpringAnimation != null
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Return `true` if a request to control an inset animation is in progress.
|
|
197
|
+
*/
|
|
198
|
+
fun isInsetAnimationRequestPending(): Boolean {
|
|
199
|
+
return pendingRequestCancellationSignal != null
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Cancel the current [WindowInsetsAnimationControllerCompat]. We immediately finish
|
|
204
|
+
* the animation, reverting back to the state at the start of the gesture.
|
|
205
|
+
*/
|
|
206
|
+
fun cancel() {
|
|
207
|
+
insetsAnimationController?.finish(isImeShownAtStart)
|
|
208
|
+
pendingRequestCancellationSignal?.cancel()
|
|
209
|
+
|
|
210
|
+
// Cancel the current spring animation
|
|
211
|
+
currentSpringAnimation?.cancel()
|
|
212
|
+
|
|
213
|
+
reset()
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Finish the current [WindowInsetsAnimationControllerCompat] immediately.
|
|
218
|
+
*/
|
|
219
|
+
fun finish() {
|
|
220
|
+
val controller = insetsAnimationController
|
|
221
|
+
|
|
222
|
+
if (controller == null) {
|
|
223
|
+
// If we don't currently have a controller, cancel any pending request and return
|
|
224
|
+
pendingRequestCancellationSignal?.cancel()
|
|
225
|
+
return
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
val current = controller.currentInsets.bottom
|
|
229
|
+
val shown = controller.shownStateInsets.bottom
|
|
230
|
+
val hidden = controller.hiddenStateInsets.bottom
|
|
231
|
+
|
|
232
|
+
when (current) {
|
|
233
|
+
// The current inset matches either the shown/hidden inset, finish() immediately
|
|
234
|
+
shown -> {
|
|
235
|
+
InteractiveKeyboardProvider.shown = true
|
|
236
|
+
controller.finish(true)
|
|
237
|
+
}
|
|
238
|
+
hidden -> {
|
|
239
|
+
InteractiveKeyboardProvider.shown = false
|
|
240
|
+
controller.finish(false)
|
|
241
|
+
}
|
|
242
|
+
else -> {
|
|
243
|
+
// Otherwise, we'll look at the current position...
|
|
244
|
+
if (controller.currentFraction >= SCROLL_THRESHOLD) {
|
|
245
|
+
// If the IME is past the 'threshold' we snap to the toggled state
|
|
246
|
+
InteractiveKeyboardProvider.shown = !isImeShownAtStart
|
|
247
|
+
controller.finish(!isImeShownAtStart)
|
|
248
|
+
} else {
|
|
249
|
+
// ...otherwise, we snap back to the original visibility
|
|
250
|
+
InteractiveKeyboardProvider.shown = isImeShownAtStart
|
|
251
|
+
controller.finish(isImeShownAtStart)
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* Finish the current [WindowInsetsAnimationControllerCompat]. We finish the animation,
|
|
259
|
+
* animating to the end state if necessary.
|
|
260
|
+
*
|
|
261
|
+
* @param velocityY the velocity of the touch gesture which caused this call to [animateToFinish].
|
|
262
|
+
* Can be `null` if velocity is not available.
|
|
263
|
+
*/
|
|
264
|
+
fun animateToFinish(velocityY: Float? = null) {
|
|
265
|
+
val controller = insetsAnimationController
|
|
266
|
+
|
|
267
|
+
if (controller == null) {
|
|
268
|
+
// If we don't currently have a controller, cancel any pending request and return
|
|
269
|
+
pendingRequestCancellationSignal?.cancel()
|
|
270
|
+
return
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
InteractiveKeyboardProvider.isInteractive = false
|
|
274
|
+
|
|
275
|
+
val current = controller.currentInsets.bottom
|
|
276
|
+
val shown = controller.shownStateInsets.bottom
|
|
277
|
+
val hidden = controller.hiddenStateInsets.bottom
|
|
278
|
+
|
|
279
|
+
when {
|
|
280
|
+
// If we have a velocity, we can use it's direction to determine
|
|
281
|
+
// the visibility. Upwards == visible
|
|
282
|
+
velocityY != null -> animateImeToVisibility(
|
|
283
|
+
visible = velocityY < 0,
|
|
284
|
+
velocityY = velocityY,
|
|
285
|
+
)
|
|
286
|
+
// The current inset matches either the shown/hidden inset, finish() immediately
|
|
287
|
+
current == shown -> controller.finish(true)
|
|
288
|
+
current == hidden -> controller.finish(false)
|
|
289
|
+
else -> {
|
|
290
|
+
// Otherwise, we'll look at the current position...
|
|
291
|
+
if (controller.currentFraction >= SCROLL_THRESHOLD) {
|
|
292
|
+
// If the IME is past the 'threshold' we animate it to the toggled state
|
|
293
|
+
animateImeToVisibility(!isImeShownAtStart)
|
|
294
|
+
} else {
|
|
295
|
+
// ...otherwise, we animate it back to the original visibility
|
|
296
|
+
animateImeToVisibility(isImeShownAtStart)
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
fun getCurrentKeyboardHeight(): Int {
|
|
303
|
+
val controller = insetsAnimationController
|
|
304
|
+
?: throw IllegalStateException(
|
|
305
|
+
"Current WindowInsetsAnimationController is null." +
|
|
306
|
+
"This should only be called if isAnimationInProgress() returns true",
|
|
307
|
+
)
|
|
308
|
+
|
|
309
|
+
return controller.currentInsets.bottom
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
private fun onRequestReady(controller: WindowInsetsAnimationControllerCompat) {
|
|
313
|
+
// The request is ready, so clear out the pending cancellation signal
|
|
314
|
+
pendingRequestCancellationSignal = null
|
|
315
|
+
// Store the current WindowInsetsAnimationController
|
|
316
|
+
insetsAnimationController = controller
|
|
317
|
+
|
|
318
|
+
// Call any pending callback
|
|
319
|
+
pendingRequestOnReady?.invoke(controller)
|
|
320
|
+
pendingRequestOnReady = null
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
/**
|
|
324
|
+
* Resets all of our internal state.
|
|
325
|
+
*/
|
|
326
|
+
private fun reset() {
|
|
327
|
+
// Clear all of our internal state
|
|
328
|
+
insetsAnimationController = null
|
|
329
|
+
pendingRequestCancellationSignal = null
|
|
330
|
+
|
|
331
|
+
isImeShownAtStart = false
|
|
332
|
+
|
|
333
|
+
currentSpringAnimation?.cancel()
|
|
334
|
+
currentSpringAnimation = null
|
|
335
|
+
|
|
336
|
+
pendingRequestOnReady = null
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
/**
|
|
340
|
+
* Animate the IME to a given visibility.
|
|
341
|
+
*
|
|
342
|
+
* @param visible `true` to animate the IME to it's fully shown state, `false` to it's
|
|
343
|
+
* fully hidden state.
|
|
344
|
+
* @param velocityY the velocity of the touch gesture which caused this call. Can be `null`
|
|
345
|
+
* if velocity is not available.
|
|
346
|
+
*/
|
|
347
|
+
private fun animateImeToVisibility(
|
|
348
|
+
visible: Boolean,
|
|
349
|
+
velocityY: Float? = null,
|
|
350
|
+
) {
|
|
351
|
+
val controller = insetsAnimationController
|
|
352
|
+
?: throw IllegalStateException("Controller should not be null")
|
|
353
|
+
|
|
354
|
+
currentSpringAnimation = springAnimationOf(
|
|
355
|
+
setter = {
|
|
356
|
+
insetTo(it.roundToInt())
|
|
357
|
+
},
|
|
358
|
+
getter = { controller.currentInsets.bottom.toFloat() },
|
|
359
|
+
finalPosition = when {
|
|
360
|
+
visible -> controller.shownStateInsets.bottom.toFloat()
|
|
361
|
+
else -> controller.hiddenStateInsets.bottom.toFloat()
|
|
362
|
+
},
|
|
363
|
+
).withSpringForceProperties {
|
|
364
|
+
// Tweak the damping value, to remove any bounciness.
|
|
365
|
+
dampingRatio = SpringForce.DAMPING_RATIO_NO_BOUNCY
|
|
366
|
+
// The stiffness value controls the strength of the spring animation, which
|
|
367
|
+
// controls the speed. Medium (the default) is a good value, but feel free to
|
|
368
|
+
// play around with this value.
|
|
369
|
+
stiffness = SpringForce.STIFFNESS_MEDIUM
|
|
370
|
+
}.apply {
|
|
371
|
+
if (velocityY != null) {
|
|
372
|
+
setStartVelocity(velocityY)
|
|
373
|
+
}
|
|
374
|
+
addEndListener { anim, _, _, _ ->
|
|
375
|
+
if (anim == currentSpringAnimation) {
|
|
376
|
+
currentSpringAnimation = null
|
|
377
|
+
}
|
|
378
|
+
// Once the animation has ended, finish the controller
|
|
379
|
+
finish()
|
|
380
|
+
}
|
|
381
|
+
}.also { it.start() }
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
/**
|
|
386
|
+
* Scroll threshold for determining whether to animating to the end state, or to the start state.
|
|
387
|
+
* Currently 15% of the total swipe distance distance
|
|
388
|
+
*/
|
|
389
|
+
private const val SCROLL_THRESHOLD = 0.15f
|
|
390
|
+
|
|
391
|
+
/**
|
|
392
|
+
* A LinearInterpolator instance we can re-use across listeners.
|
|
393
|
+
*/
|
|
394
|
+
private val linearInterpolator = LinearInterpolator()
|
package/android/src/main/java/com/reactnativekeyboardcontroller/KeyboardControllerPackage.kt
CHANGED
|
@@ -54,6 +54,6 @@ class KeyboardControllerPackage : TurboReactPackage() {
|
|
|
54
54
|
}
|
|
55
55
|
|
|
56
56
|
override fun createViewManagers(reactContext: ReactApplicationContext): List<ViewManager<*, *>> {
|
|
57
|
-
return listOf(KeyboardControllerViewManager(reactContext))
|
|
57
|
+
return listOf(KeyboardControllerViewManager(reactContext), KeyboardGestureAreaViewManager(reactContext))
|
|
58
58
|
}
|
|
59
59
|
}
|
package/android/src/main/java/com/reactnativekeyboardcontroller/events/KeyboardTransitionEvent.kt
CHANGED
|
@@ -4,7 +4,7 @@ import com.facebook.react.bridge.Arguments
|
|
|
4
4
|
import com.facebook.react.uimanager.events.Event
|
|
5
5
|
import com.facebook.react.uimanager.events.RCTEventEmitter
|
|
6
6
|
|
|
7
|
-
class KeyboardTransitionEvent(viewId: Int, private val event: String, private val height:
|
|
7
|
+
class KeyboardTransitionEvent(viewId: Int, private val event: String, private val height: Double, private val progress: Double) : Event<KeyboardTransitionEvent>(viewId) {
|
|
8
8
|
override fun getEventName() = event
|
|
9
9
|
|
|
10
10
|
// TODO: All events for a given view can be coalesced?
|
|
@@ -13,7 +13,7 @@ class KeyboardTransitionEvent(viewId: Int, private val event: String, private va
|
|
|
13
13
|
override fun dispatch(rctEventEmitter: RCTEventEmitter) {
|
|
14
14
|
val map = Arguments.createMap()
|
|
15
15
|
map.putDouble("progress", progress)
|
|
16
|
-
map.
|
|
16
|
+
map.putDouble("height", height)
|
|
17
17
|
rctEventEmitter.receiveEvent(viewTag, eventName, map)
|
|
18
18
|
}
|
|
19
19
|
}
|