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.
Files changed (87) hide show
  1. package/README.md +1 -1
  2. package/android/build.gradle +9 -2
  3. package/android/gradle.properties +2 -2
  4. package/android/react-native-helpers.gradle +48 -0
  5. package/android/src/fabric/java/com/reactnativekeyboardcontroller/KeyboardGestureAreaViewManager.kt +44 -0
  6. package/android/src/main/java/com/reactnativekeyboardcontroller/InteractiveKeyboardProvider.kt +6 -0
  7. package/android/src/main/java/com/reactnativekeyboardcontroller/KeyboardAnimationCallback.kt +27 -13
  8. package/android/src/main/java/com/reactnativekeyboardcontroller/KeyboardAnimationController.kt +394 -0
  9. package/android/src/main/java/com/reactnativekeyboardcontroller/KeyboardControllerPackage.kt +1 -1
  10. package/android/src/main/java/com/reactnativekeyboardcontroller/events/KeyboardTransitionEvent.kt +2 -2
  11. package/android/src/main/java/com/reactnativekeyboardcontroller/extensions/Float.kt +4 -4
  12. package/android/src/main/java/com/reactnativekeyboardcontroller/extensions/View.kt +21 -0
  13. package/android/src/main/java/com/reactnativekeyboardcontroller/interpolators/Interpolator.kt +14 -0
  14. package/android/src/main/java/com/reactnativekeyboardcontroller/interpolators/IosInterpolator.kt +18 -0
  15. package/android/src/main/java/com/reactnativekeyboardcontroller/interpolators/LinearInterpolator.kt +11 -0
  16. package/android/src/main/java/com/reactnativekeyboardcontroller/managers/KeyboardControllerViewManagerImpl.kt +2 -0
  17. package/android/src/main/java/com/reactnativekeyboardcontroller/managers/KeyboardGestureAreaViewManagerImpl.kt +27 -0
  18. package/android/src/main/java/com/reactnativekeyboardcontroller/views/KeyboardGestureAreaReactViewGroup.kt +192 -0
  19. package/android/src/paper/java/com/reactnativekeyboardcontroller/KeyboardGestureAreaViewManager.kt +33 -0
  20. package/ios/KeyboardControllerView.mm +10 -3
  21. package/ios/KeyboardControllerViewManager.mm +1 -0
  22. package/ios/KeyboardControllerViewManager.swift +1 -0
  23. package/ios/KeyboardMovementObserver.swift +78 -4
  24. package/jest/index.js +1 -0
  25. package/lib/commonjs/animated.js +13 -19
  26. package/lib/commonjs/animated.js.map +1 -1
  27. package/lib/commonjs/context.js +0 -3
  28. package/lib/commonjs/context.js.map +1 -1
  29. package/lib/commonjs/hooks.js +0 -13
  30. package/lib/commonjs/hooks.js.map +1 -1
  31. package/lib/commonjs/index.js +0 -13
  32. package/lib/commonjs/index.js.map +1 -1
  33. package/lib/commonjs/internal.js +13 -22
  34. package/lib/commonjs/internal.js.map +1 -1
  35. package/lib/commonjs/monkey-patch.js +3 -12
  36. package/lib/commonjs/monkey-patch.js.map +1 -1
  37. package/lib/commonjs/native.js +10 -10
  38. package/lib/commonjs/native.js.map +1 -1
  39. package/lib/commonjs/replicas.js +3 -19
  40. package/lib/commonjs/replicas.js.map +1 -1
  41. package/lib/commonjs/specs/KeyboardControllerViewNativeComponent.js +0 -5
  42. package/lib/commonjs/specs/KeyboardControllerViewNativeComponent.js.map +1 -1
  43. package/lib/commonjs/specs/KeyboardGestureAreaNativeComponent.js +13 -0
  44. package/lib/commonjs/specs/KeyboardGestureAreaNativeComponent.js.map +1 -0
  45. package/lib/commonjs/specs/NativeKeyboardController.js +0 -3
  46. package/lib/commonjs/specs/NativeKeyboardController.js.map +1 -1
  47. package/lib/commonjs/specs/NativeStatusBarManagerCompat.js +0 -3
  48. package/lib/commonjs/specs/NativeStatusBarManagerCompat.js.map +1 -1
  49. package/lib/commonjs/types.js.map +1 -1
  50. package/lib/commonjs/utils.js +0 -2
  51. package/lib/commonjs/utils.js.map +1 -1
  52. package/lib/module/animated.js +13 -7
  53. package/lib/module/animated.js.map +1 -1
  54. package/lib/module/context.js.map +1 -1
  55. package/lib/module/hooks.js.map +1 -1
  56. package/lib/module/index.js.map +1 -1
  57. package/lib/module/internal.js +13 -16
  58. package/lib/module/internal.js.map +1 -1
  59. package/lib/module/monkey-patch.js +4 -10
  60. package/lib/module/monkey-patch.js.map +1 -1
  61. package/lib/module/native.js +8 -5
  62. package/lib/module/native.js.map +1 -1
  63. package/lib/module/replicas.js +3 -5
  64. package/lib/module/replicas.js.map +1 -1
  65. package/lib/module/specs/KeyboardControllerViewNativeComponent.js +0 -1
  66. package/lib/module/specs/KeyboardControllerViewNativeComponent.js.map +1 -1
  67. package/lib/module/specs/KeyboardGestureAreaNativeComponent.js +5 -0
  68. package/lib/module/specs/KeyboardGestureAreaNativeComponent.js.map +1 -0
  69. package/lib/module/specs/NativeKeyboardController.js.map +1 -1
  70. package/lib/module/specs/NativeStatusBarManagerCompat.js.map +1 -1
  71. package/lib/module/types.js.map +1 -1
  72. package/lib/module/utils.js.map +1 -1
  73. package/lib/typescript/animated.d.ts +1 -1
  74. package/lib/typescript/context.d.ts +3 -3
  75. package/lib/typescript/internal.d.ts +1 -0
  76. package/lib/typescript/native.d.ts +2 -1
  77. package/lib/typescript/replicas.d.ts +1 -1
  78. package/lib/typescript/specs/KeyboardControllerViewNativeComponent.d.ts +6 -5
  79. package/lib/typescript/specs/KeyboardGestureAreaNativeComponent.d.ts +10 -0
  80. package/lib/typescript/types.d.ts +29 -13
  81. package/package.json +11 -12
  82. package/src/animated.tsx +7 -0
  83. package/src/internal.ts +20 -3
  84. package/src/native.ts +5 -0
  85. package/src/specs/KeyboardControllerViewNativeComponent.ts +5 -6
  86. package/src/specs/KeyboardGestureAreaNativeComponent.ts +14 -0
  87. 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 (planned) 👆📱
15
+ - interactive keyboard dismissing 👆📱
16
16
  - and more is coming... Stay tuned! 😊
17
17
 
18
18
  ## Installation
@@ -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
- // noinspection GradleDynamicVersion
155
- api 'com.facebook.react:react-native:+'
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
  }
@@ -1,5 +1,5 @@
1
1
  KeyboardController_kotlinVersion=1.6.21
2
- KeyboardController_compileSdkVersion=29
3
- KeyboardController_targetSdkVersion=29
2
+ KeyboardController_compileSdkVersion=33
3
+ KeyboardController_targetSdkVersion=33
4
4
 
5
5
  android.useAndroidX=true
@@ -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
+ }
@@ -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
+ }
@@ -0,0 +1,6 @@
1
+ package com.reactnativekeyboardcontroller
2
+
3
+ object InteractiveKeyboardProvider {
4
+ var shown = false
5
+ var isInteractive = false
6
+ }
@@ -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.ofInt(this.persistentKeyboardHeight, keyboardHeight)
72
+ val animation = ValueAnimator.ofFloat(this.persistentKeyboardHeight.toFloat(), keyboardHeight.toFloat())
72
73
  animation.addUpdateListener { animator ->
73
- val toValue = animator.animatedValue as Int
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 = Math.abs((height.toDouble() / persistentKeyboardHeight)).let { if (it.isNaN()) 0.0 else it }
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
- this.sendEventToJS(KeyboardTransitionEvent(view.id, "topKeyboardMove", height, progress))
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
- this.persistentKeyboardHeight = getCurrentKeyboardHeight()
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(): Int {
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 Math.max((keyboardHeight - navigationBar).toFloat().dp, 0)
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: Int): WritableMap {
192
+ private fun getEventParams(height: Double): WritableMap {
179
193
  val params: WritableMap = Arguments.createMap()
180
- params.putInt("height", height)
194
+ params.putDouble("height", height)
181
195
 
182
196
  return params
183
197
  }
@@ -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()
@@ -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
  }
@@ -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: Int, private val progress: Double) : Event<KeyboardTransitionEvent>(viewId) {
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.putInt("height", height)
16
+ map.putDouble("height", height)
17
17
  rctEventEmitter.receiveEvent(viewTag, eventName, map)
18
18
  }
19
19
  }