react-native-keyboard-controller 1.15.2 → 1.16.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/android/src/fabric/java/com/reactnativekeyboardcontroller/KeyboardControllerViewManager.kt +6 -0
- package/android/src/fabric/java/com/reactnativekeyboardcontroller/KeyboardGestureAreaViewManager.kt +8 -0
- package/android/src/main/java/com/reactnativekeyboardcontroller/extensions/EditText.kt +77 -51
- package/android/src/main/java/com/reactnativekeyboardcontroller/listeners/FocusedInputObserver.kt +3 -3
- package/android/src/main/java/com/reactnativekeyboardcontroller/listeners/KeyboardAnimationCallback.kt +2 -2
- package/android/src/main/java/com/reactnativekeyboardcontroller/managers/KeyboardControllerViewManagerImpl.kt +7 -0
- package/android/src/main/java/com/reactnativekeyboardcontroller/views/EdgeToEdgeReactViewGroup.kt +9 -1
- package/android/src/paper/java/com/reactnativekeyboardcontroller/KeyboardControllerViewManager.kt +8 -0
- package/android/src/paper/java/com/reactnativekeyboardcontroller/KeyboardGestureAreaViewManager.kt +9 -0
- package/ios/animations/KeyboardAnimation.swift +4 -4
- package/ios/animations/SpringAnimation.swift +28 -5
- package/ios/animations/TimingAnimation.swift +77 -32
- package/ios/delegates/KCTextInputCompositeDelegate.swift +73 -1
- package/ios/extensions/Notification.swift +4 -0
- package/ios/extensions/UIResponder.swift +14 -0
- package/ios/interactive/InvisibleInputAccessoryView.swift +59 -0
- package/ios/interactive/KeyboardAreaExtender.swift +74 -0
- package/ios/interactive/KeyboardOffsetProvider.swift +39 -0
- package/ios/observers/FocusedInputObserver.swift +81 -25
- package/ios/observers/KeyboardEventsIgnorer.swift +39 -0
- package/ios/observers/KeyboardMovementObserver.swift +59 -61
- package/ios/protocols/TextInput.swift +2 -0
- package/ios/swizzling/UIResponderSwizzle.swift +60 -0
- package/ios/traversal/ViewHierarchyNavigator.swift +1 -1
- package/ios/views/KeyboardGestureAreaManager.h +31 -0
- package/ios/views/KeyboardGestureAreaManager.mm +174 -0
- package/lib/commonjs/animated.js +4 -1
- package/lib/commonjs/animated.js.map +1 -1
- package/lib/commonjs/bindings.native.js +1 -1
- package/lib/commonjs/bindings.native.js.map +1 -1
- package/lib/commonjs/specs/KeyboardControllerViewNativeComponent.js.map +1 -1
- package/lib/commonjs/specs/KeyboardGestureAreaNativeComponent.js +1 -3
- package/lib/commonjs/specs/KeyboardGestureAreaNativeComponent.js.map +1 -1
- package/lib/commonjs/types.js.map +1 -1
- package/lib/module/animated.js +4 -1
- package/lib/module/animated.js.map +1 -1
- package/lib/module/bindings.native.js +1 -1
- package/lib/module/bindings.native.js.map +1 -1
- package/lib/module/specs/KeyboardControllerViewNativeComponent.js.map +1 -1
- package/lib/module/specs/KeyboardGestureAreaNativeComponent.js +1 -3
- package/lib/module/specs/KeyboardGestureAreaNativeComponent.js.map +1 -1
- package/lib/module/types.js.map +1 -1
- package/lib/typescript/animated.d.ts +9 -1
- package/lib/typescript/specs/KeyboardControllerViewNativeComponent.d.ts +1 -0
- package/lib/typescript/specs/KeyboardGestureAreaNativeComponent.d.ts +1 -0
- package/lib/typescript/types.d.ts +6 -1
- package/package.json +1 -1
- package/src/animated.tsx +15 -1
- package/src/bindings.native.ts +1 -1
- package/src/specs/KeyboardControllerViewNativeComponent.ts +1 -0
- package/src/specs/KeyboardGestureAreaNativeComponent.ts +4 -3
- package/src/types.ts +6 -1
- package/android/src/main/java/com/reactnativekeyboardcontroller/ui/FrameScheduler.kt +0 -43
package/android/src/fabric/java/com/reactnativekeyboardcontroller/KeyboardControllerViewManager.kt
CHANGED
|
@@ -36,6 +36,12 @@ class KeyboardControllerViewManager(
|
|
|
36
36
|
value: Boolean,
|
|
37
37
|
) = manager.setNavigationBarTranslucent(view as EdgeToEdgeReactViewGroup, value)
|
|
38
38
|
|
|
39
|
+
@ReactProp(name = "preserveEdgeToEdge")
|
|
40
|
+
override fun setPreserveEdgeToEdge(
|
|
41
|
+
view: ReactViewGroup,
|
|
42
|
+
value: Boolean,
|
|
43
|
+
) = manager.setPreserveEdgeToEdge(view as EdgeToEdgeReactViewGroup, value)
|
|
44
|
+
|
|
39
45
|
@ReactProp(name = "enabled")
|
|
40
46
|
override fun setEnabled(
|
|
41
47
|
view: ReactViewGroup,
|
package/android/src/fabric/java/com/reactnativekeyboardcontroller/KeyboardGestureAreaViewManager.kt
CHANGED
|
@@ -56,4 +56,12 @@ class KeyboardGestureAreaViewManager(
|
|
|
56
56
|
) {
|
|
57
57
|
manager.setScrollKeyboardOffScreenWhenVisible(view as KeyboardGestureAreaReactViewGroup, value)
|
|
58
58
|
}
|
|
59
|
+
|
|
60
|
+
@ReactProp(name = "textInputNativeID")
|
|
61
|
+
override fun setTextInputNativeID(
|
|
62
|
+
view: ReactViewGroup,
|
|
63
|
+
value: String?,
|
|
64
|
+
) {
|
|
65
|
+
// no-op
|
|
66
|
+
}
|
|
59
67
|
}
|
|
@@ -4,12 +4,13 @@ import android.os.Build
|
|
|
4
4
|
import android.text.Editable
|
|
5
5
|
import android.text.InputType
|
|
6
6
|
import android.text.TextWatcher
|
|
7
|
+
import android.view.Gravity
|
|
7
8
|
import android.view.View
|
|
9
|
+
import android.view.ViewTreeObserver.OnPreDrawListener
|
|
8
10
|
import android.widget.EditText
|
|
9
11
|
import com.facebook.react.views.scroll.ReactScrollView
|
|
10
12
|
import com.facebook.react.views.textinput.ReactEditText
|
|
11
13
|
import com.reactnativekeyboardcontroller.log.Logger
|
|
12
|
-
import com.reactnativekeyboardcontroller.ui.FrameScheduler
|
|
13
14
|
import java.lang.reflect.Field
|
|
14
15
|
import kotlin.math.max
|
|
15
16
|
import kotlin.math.min
|
|
@@ -78,6 +79,13 @@ fun EditText.addOnTextChangedListener(action: (String) -> Unit): TextWatcher {
|
|
|
78
79
|
Logger.w(javaClass.simpleName, "Can not attach listener because casting failed: ${e.message}")
|
|
79
80
|
} catch (e: NoSuchFieldException) {
|
|
80
81
|
Logger.w(javaClass.simpleName, "Can not attach listener because field `mListeners` not found: ${e.message}")
|
|
82
|
+
} catch (e: IllegalArgumentException) {
|
|
83
|
+
Logger.w(
|
|
84
|
+
javaClass.simpleName,
|
|
85
|
+
"Can not attach listener to be the first in the list: ${e.message}. Attaching to the end...",
|
|
86
|
+
)
|
|
87
|
+
// it's plain EditText - it doesn't have the same problem as ReactEditText
|
|
88
|
+
this.addTextChangedListener(listener)
|
|
81
89
|
}
|
|
82
90
|
|
|
83
91
|
return listener
|
|
@@ -146,78 +154,96 @@ val EditText?.keyboardType: String
|
|
|
146
154
|
}
|
|
147
155
|
|
|
148
156
|
class KeyboardControllerSelectionWatcher(
|
|
149
|
-
private val editText:
|
|
157
|
+
private val editText: EditText,
|
|
150
158
|
private val action: (start: Int, end: Int, startX: Double, startY: Double, endX: Double, endY: Double) -> Unit,
|
|
151
159
|
) {
|
|
152
160
|
private var lastSelectionStart: Int = -1
|
|
153
161
|
private var lastSelectionEnd: Int = -1
|
|
162
|
+
private var lastEditTextHeight: Int = -1
|
|
154
163
|
|
|
155
|
-
private val
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
lastSelectionStart = start
|
|
162
|
-
lastSelectionEnd = end
|
|
164
|
+
private val preDrawListener: OnPreDrawListener =
|
|
165
|
+
object : OnPreDrawListener {
|
|
166
|
+
override fun onPreDraw(): Boolean {
|
|
167
|
+
val start = editText.selectionStart
|
|
168
|
+
val end = editText.selectionEnd
|
|
169
|
+
val editTextHeight = editText.height
|
|
163
170
|
|
|
164
171
|
val view = editText
|
|
165
172
|
val layout = view.layout
|
|
166
173
|
|
|
167
174
|
if (layout === null) {
|
|
168
|
-
return
|
|
175
|
+
return true
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
if (lastSelectionStart != start || lastSelectionEnd != end || lastEditTextHeight != editTextHeight) {
|
|
179
|
+
lastSelectionStart = start
|
|
180
|
+
lastSelectionEnd = end
|
|
181
|
+
lastEditTextHeight = editTextHeight
|
|
182
|
+
|
|
183
|
+
val cursorPositionStartX: Float
|
|
184
|
+
val cursorPositionStartY: Float
|
|
185
|
+
val cursorPositionEndX: Float
|
|
186
|
+
val cursorPositionEndY: Float
|
|
187
|
+
|
|
188
|
+
val realStart = min(start, end)
|
|
189
|
+
val realEnd = max(start, end)
|
|
190
|
+
|
|
191
|
+
val lineStart = layout.getLineForOffset(realStart)
|
|
192
|
+
val baselineStart = layout.getLineTop(lineStart)
|
|
193
|
+
|
|
194
|
+
val textHeight = layout.height
|
|
195
|
+
val cursorWidth =
|
|
196
|
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
|
197
|
+
view.textCursorDrawable?.intrinsicWidth ?: 0
|
|
198
|
+
} else {
|
|
199
|
+
0
|
|
200
|
+
}
|
|
201
|
+
val gravity = editText.gravity and Gravity.VERTICAL_GRAVITY_MASK
|
|
202
|
+
val paddingVertical = editText.paddingTop + editText.paddingBottom
|
|
203
|
+
val lineHeightHalfHearted = editText.lineHeight / 2
|
|
204
|
+
val verticalOffset =
|
|
205
|
+
when (gravity) {
|
|
206
|
+
Gravity.CENTER_VERTICAL ->
|
|
207
|
+
(editTextHeight - paddingVertical - textHeight) / 2 + editText.paddingTop + lineHeightHalfHearted
|
|
208
|
+
Gravity.BOTTOM -> editTextHeight - textHeight - editText.paddingBottom + lineHeightHalfHearted
|
|
209
|
+
// Default to Gravity.TOP or other cases
|
|
210
|
+
else -> editText.paddingTop + lineHeightHalfHearted
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
cursorPositionStartX = layout.getPrimaryHorizontal(realStart)
|
|
214
|
+
cursorPositionStartY = (baselineStart + verticalOffset).toFloat()
|
|
215
|
+
|
|
216
|
+
val lineEnd = layout.getLineForOffset(realEnd)
|
|
217
|
+
val right = layout.getPrimaryHorizontal(realEnd)
|
|
218
|
+
val bottom = layout.getLineBottom(lineEnd)
|
|
219
|
+
|
|
220
|
+
cursorPositionEndX = right + cursorWidth
|
|
221
|
+
cursorPositionEndY = (bottom + verticalOffset).toFloat()
|
|
222
|
+
|
|
223
|
+
action(
|
|
224
|
+
start,
|
|
225
|
+
end,
|
|
226
|
+
cursorPositionStartX.dp,
|
|
227
|
+
cursorPositionStartY.dp,
|
|
228
|
+
cursorPositionEndX.dp,
|
|
229
|
+
cursorPositionEndY.dp,
|
|
230
|
+
)
|
|
169
231
|
}
|
|
170
232
|
|
|
171
|
-
|
|
172
|
-
val cursorPositionStartY: Float
|
|
173
|
-
val cursorPositionEndX: Float
|
|
174
|
-
val cursorPositionEndY: Float
|
|
175
|
-
|
|
176
|
-
val realStart = min(start, end)
|
|
177
|
-
val realEnd = max(start, end)
|
|
178
|
-
|
|
179
|
-
val lineStart = layout.getLineForOffset(realStart)
|
|
180
|
-
val baselineStart = layout.getLineBaseline(lineStart)
|
|
181
|
-
val ascentStart = layout.getLineAscent(lineStart)
|
|
182
|
-
|
|
183
|
-
cursorPositionStartX = layout.getPrimaryHorizontal(realStart)
|
|
184
|
-
cursorPositionStartY = (baselineStart + ascentStart).toFloat()
|
|
185
|
-
|
|
186
|
-
val lineEnd = layout.getLineForOffset(realEnd)
|
|
187
|
-
|
|
188
|
-
val right = layout.getPrimaryHorizontal(realEnd)
|
|
189
|
-
val bottom = layout.getLineBottom(lineEnd) + layout.getLineAscent(lineEnd)
|
|
190
|
-
val cursorWidth =
|
|
191
|
-
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
|
192
|
-
view.textCursorDrawable?.intrinsicWidth ?: 0
|
|
193
|
-
} else {
|
|
194
|
-
0
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
cursorPositionEndX = right + cursorWidth
|
|
198
|
-
cursorPositionEndY = bottom.toFloat()
|
|
199
|
-
|
|
200
|
-
action(
|
|
201
|
-
start,
|
|
202
|
-
end,
|
|
203
|
-
cursorPositionStartX.dp,
|
|
204
|
-
cursorPositionStartY.dp,
|
|
205
|
-
cursorPositionEndX.dp,
|
|
206
|
-
cursorPositionEndY.dp,
|
|
207
|
-
)
|
|
233
|
+
return true
|
|
208
234
|
}
|
|
209
235
|
}
|
|
210
236
|
|
|
211
237
|
fun setup() {
|
|
212
|
-
|
|
238
|
+
editText.viewTreeObserver.addOnPreDrawListener(preDrawListener)
|
|
213
239
|
}
|
|
214
240
|
|
|
215
241
|
fun destroy() {
|
|
216
|
-
|
|
242
|
+
editText.viewTreeObserver.removeOnPreDrawListener(preDrawListener)
|
|
217
243
|
}
|
|
218
244
|
}
|
|
219
245
|
|
|
220
|
-
fun
|
|
246
|
+
fun EditText.addOnSelectionChangedListener(
|
|
221
247
|
action: (start: Int, end: Int, startX: Double, startY: Double, endX: Double, endY: Double) -> Unit,
|
|
222
248
|
): () -> Unit {
|
|
223
249
|
val listener = KeyboardControllerSelectionWatcher(this, action)
|
package/android/src/main/java/com/reactnativekeyboardcontroller/listeners/FocusedInputObserver.kt
CHANGED
|
@@ -4,10 +4,10 @@ import android.text.TextWatcher
|
|
|
4
4
|
import android.view.View
|
|
5
5
|
import android.view.View.OnLayoutChangeListener
|
|
6
6
|
import android.view.ViewTreeObserver.OnGlobalFocusChangeListener
|
|
7
|
+
import android.widget.EditText
|
|
7
8
|
import com.facebook.react.bridge.Arguments
|
|
8
9
|
import com.facebook.react.uimanager.ThemedReactContext
|
|
9
10
|
import com.facebook.react.uimanager.UIManagerHelper
|
|
10
|
-
import com.facebook.react.views.textinput.ReactEditText
|
|
11
11
|
import com.facebook.react.views.view.ReactViewGroup
|
|
12
12
|
import com.reactnativekeyboardcontroller.events.FocusedInputLayoutChangedEvent
|
|
13
13
|
import com.reactnativekeyboardcontroller.events.FocusedInputLayoutChangedEventData
|
|
@@ -46,7 +46,7 @@ class FocusedInputObserver(
|
|
|
46
46
|
private val surfaceId = UIManagerHelper.getSurfaceId(view)
|
|
47
47
|
|
|
48
48
|
// state variables
|
|
49
|
-
private var lastFocusedInput:
|
|
49
|
+
private var lastFocusedInput: EditText? = null
|
|
50
50
|
private var lastEventDispatched: FocusedInputLayoutChangedEventData = noFocusedInputEvent
|
|
51
51
|
private var textWatcher: TextWatcher? = null
|
|
52
52
|
private var selectionSubscription: (() -> Unit)? = null
|
|
@@ -105,7 +105,7 @@ class FocusedInputObserver(
|
|
|
105
105
|
selectionSubscription?.invoke()
|
|
106
106
|
lastFocusedInput = null
|
|
107
107
|
}
|
|
108
|
-
if (newFocus is
|
|
108
|
+
if (newFocus is EditText) {
|
|
109
109
|
lastFocusedInput = newFocus
|
|
110
110
|
newFocus.addOnLayoutChangeListener(layoutListener)
|
|
111
111
|
this.syncUpLayout()
|
|
@@ -2,6 +2,7 @@ package com.reactnativekeyboardcontroller.listeners
|
|
|
2
2
|
|
|
3
3
|
import android.view.View
|
|
4
4
|
import android.view.ViewTreeObserver.OnGlobalFocusChangeListener
|
|
5
|
+
import android.widget.EditText
|
|
5
6
|
import androidx.core.graphics.Insets
|
|
6
7
|
import androidx.core.view.OnApplyWindowInsetsListener
|
|
7
8
|
import androidx.core.view.ViewCompat
|
|
@@ -11,7 +12,6 @@ import com.facebook.react.bridge.Arguments
|
|
|
11
12
|
import com.facebook.react.bridge.WritableMap
|
|
12
13
|
import com.facebook.react.uimanager.ThemedReactContext
|
|
13
14
|
import com.facebook.react.uimanager.UIManagerHelper
|
|
14
|
-
import com.facebook.react.views.textinput.ReactEditText
|
|
15
15
|
import com.facebook.react.views.view.ReactViewGroup
|
|
16
16
|
import com.reactnativekeyboardcontroller.constants.Keyboard
|
|
17
17
|
import com.reactnativekeyboardcontroller.events.KeyboardTransitionEvent
|
|
@@ -67,7 +67,7 @@ class KeyboardAnimationCallback(
|
|
|
67
67
|
// listeners
|
|
68
68
|
private val focusListener =
|
|
69
69
|
OnGlobalFocusChangeListener { oldFocus, newFocus ->
|
|
70
|
-
if (newFocus is
|
|
70
|
+
if (newFocus is EditText) {
|
|
71
71
|
viewTagFocused = newFocus.id
|
|
72
72
|
|
|
73
73
|
// keyboard is visible and focus has been changed
|
|
@@ -37,6 +37,13 @@ class KeyboardControllerViewManagerImpl(
|
|
|
37
37
|
view.setNavigationBarTranslucent(isNavigationBarTranslucent)
|
|
38
38
|
}
|
|
39
39
|
|
|
40
|
+
fun setPreserveEdgeToEdge(
|
|
41
|
+
view: EdgeToEdgeReactViewGroup,
|
|
42
|
+
isPreservingEdgeToEdge: Boolean,
|
|
43
|
+
) {
|
|
44
|
+
view.setPreserveEdgeToEdge(isPreservingEdgeToEdge)
|
|
45
|
+
}
|
|
46
|
+
|
|
40
47
|
fun getExportedCustomDirectEventTypeConstants(): MutableMap<String, Any> {
|
|
41
48
|
val map: MutableMap<String, Any> =
|
|
42
49
|
MapBuilder.of(
|
package/android/src/main/java/com/reactnativekeyboardcontroller/views/EdgeToEdgeReactViewGroup.kt
CHANGED
|
@@ -31,6 +31,7 @@ class EdgeToEdgeReactViewGroup(
|
|
|
31
31
|
// props
|
|
32
32
|
private var isStatusBarTranslucent = false
|
|
33
33
|
private var isNavigationBarTranslucent = false
|
|
34
|
+
private var isPreservingEdgeToEdge = false
|
|
34
35
|
private var active = false
|
|
35
36
|
|
|
36
37
|
// internal class members
|
|
@@ -188,7 +189,10 @@ class EdgeToEdgeReactViewGroup(
|
|
|
188
189
|
}
|
|
189
190
|
|
|
190
191
|
private fun disable() {
|
|
191
|
-
|
|
192
|
+
if (!isPreservingEdgeToEdge) {
|
|
193
|
+
this.goToEdgeToEdge(false)
|
|
194
|
+
}
|
|
195
|
+
|
|
192
196
|
this.setupWindowInsets()
|
|
193
197
|
this.removeKeyboardCallbacks()
|
|
194
198
|
modalAttachedWatcher.disable()
|
|
@@ -208,6 +212,10 @@ class EdgeToEdgeReactViewGroup(
|
|
|
208
212
|
this.isNavigationBarTranslucent = isNavigationBarTranslucent
|
|
209
213
|
}
|
|
210
214
|
|
|
215
|
+
fun setPreserveEdgeToEdge(isPreservingEdgeToEdge: Boolean) {
|
|
216
|
+
this.isPreservingEdgeToEdge = isPreservingEdgeToEdge
|
|
217
|
+
}
|
|
218
|
+
|
|
211
219
|
fun setActive(active: Boolean) {
|
|
212
220
|
this.active = active
|
|
213
221
|
|
package/android/src/paper/java/com/reactnativekeyboardcontroller/KeyboardControllerViewManager.kt
CHANGED
|
@@ -41,6 +41,14 @@ class KeyboardControllerViewManager(
|
|
|
41
41
|
manager.setNavigationBarTranslucent(view, isNavigationBarTranslucent)
|
|
42
42
|
}
|
|
43
43
|
|
|
44
|
+
@ReactProp(name = "preserveEdgeToEdge")
|
|
45
|
+
fun setPreserveEdgeToEdge(
|
|
46
|
+
view: EdgeToEdgeReactViewGroup,
|
|
47
|
+
isPreservingEdgeToEdge: Boolean,
|
|
48
|
+
) {
|
|
49
|
+
manager.setPreserveEdgeToEdge(view, isPreservingEdgeToEdge)
|
|
50
|
+
}
|
|
51
|
+
|
|
44
52
|
override fun getExportedCustomDirectEventTypeConstants(): MutableMap<String, Any> =
|
|
45
53
|
manager.getExportedCustomDirectEventTypeConstants()
|
|
46
54
|
}
|
package/android/src/paper/java/com/reactnativekeyboardcontroller/KeyboardGestureAreaViewManager.kt
CHANGED
|
@@ -48,4 +48,13 @@ class KeyboardGestureAreaViewManager(
|
|
|
48
48
|
) {
|
|
49
49
|
manager.setScrollKeyboardOffScreenWhenVisible(view, value)
|
|
50
50
|
}
|
|
51
|
+
|
|
52
|
+
@Suppress("detekt:UnusedParameter")
|
|
53
|
+
@ReactProp(name = "textInputNativeID")
|
|
54
|
+
fun setTextInputNativeID(
|
|
55
|
+
view: KeyboardGestureAreaReactViewGroup,
|
|
56
|
+
value: String,
|
|
57
|
+
) {
|
|
58
|
+
// no-op
|
|
59
|
+
}
|
|
51
60
|
}
|
|
@@ -23,14 +23,14 @@ public class KeyboardAnimation: KeyboardAnimationProtocol {
|
|
|
23
23
|
// constructor variables
|
|
24
24
|
let fromValue: Double
|
|
25
25
|
let toValue: Double
|
|
26
|
-
let speed:
|
|
26
|
+
let speed: Double
|
|
27
27
|
let timestamp: CFTimeInterval
|
|
28
28
|
|
|
29
29
|
init(fromValue: Double, toValue: Double, animation: CAMediaTiming) {
|
|
30
30
|
self.fromValue = fromValue
|
|
31
31
|
self.toValue = toValue
|
|
32
32
|
self.animation = animation
|
|
33
|
-
speed = animation.speed
|
|
33
|
+
speed = Double(animation.speed)
|
|
34
34
|
timestamp = CACurrentMediaTime()
|
|
35
35
|
}
|
|
36
36
|
|
|
@@ -63,7 +63,7 @@ public class KeyboardAnimation: KeyboardAnimationProtocol {
|
|
|
63
63
|
|
|
64
64
|
while (upperBound - lowerBound) > tolerance {
|
|
65
65
|
tGuess = (lowerBound + upperBound) / 2
|
|
66
|
-
let currentValue = valueAt(time: tGuess /
|
|
66
|
+
let currentValue = valueAt(time: tGuess / speed)
|
|
67
67
|
|
|
68
68
|
// Adjust the condition to account for the direction of animation
|
|
69
69
|
if (currentValue < value && isIncreasing) || (currentValue > value && !isIncreasing) {
|
|
@@ -73,6 +73,6 @@ public class KeyboardAnimation: KeyboardAnimationProtocol {
|
|
|
73
73
|
}
|
|
74
74
|
}
|
|
75
75
|
|
|
76
|
-
return tGuess /
|
|
76
|
+
return tGuess / speed
|
|
77
77
|
}
|
|
78
78
|
}
|
|
@@ -16,6 +16,13 @@ public final class SpringAnimation: KeyboardAnimation {
|
|
|
16
16
|
private let omega0: Double // Undamped angular frequency of the oscillator
|
|
17
17
|
private let omega1: Double // Exponential decay
|
|
18
18
|
private let v0: Double // Initial velocity
|
|
19
|
+
// pre-computed values
|
|
20
|
+
private let x0: Double
|
|
21
|
+
private let isUnderDamped: Bool
|
|
22
|
+
private let aUnder: Double
|
|
23
|
+
private let bUnder: Double
|
|
24
|
+
private let aCritical: Double
|
|
25
|
+
private let bCritical: Double
|
|
19
26
|
|
|
20
27
|
// constructor variables
|
|
21
28
|
private let stiffness: Double
|
|
@@ -33,24 +40,40 @@ public final class SpringAnimation: KeyboardAnimation {
|
|
|
33
40
|
omega0 = sqrt(stiffness / mass) // Undamped angular frequency of the oscillator
|
|
34
41
|
omega1 = omega0 * sqrt(1.0 - zeta * zeta) // Exponential decay
|
|
35
42
|
v0 = -initialVelocity
|
|
43
|
+
x0 = toValue - fromValue
|
|
44
|
+
isUnderDamped = zeta < 1
|
|
45
|
+
|
|
46
|
+
if isUnderDamped {
|
|
47
|
+
// Under damped
|
|
48
|
+
aCritical = 0
|
|
49
|
+
bCritical = 0
|
|
50
|
+
aUnder = (v0 + zeta * omega0 * x0) / omega1
|
|
51
|
+
bUnder = x0
|
|
52
|
+
} else {
|
|
53
|
+
// Critically damped
|
|
54
|
+
aCritical = x0
|
|
55
|
+
bCritical = (v0 + omega0 * x0)
|
|
56
|
+
aUnder = 0
|
|
57
|
+
bUnder = 0
|
|
58
|
+
}
|
|
36
59
|
|
|
37
60
|
super.init(fromValue: fromValue, toValue: toValue, animation: animation)
|
|
38
61
|
}
|
|
39
62
|
|
|
40
63
|
// public functions
|
|
41
64
|
override func valueAt(time: Double) -> Double {
|
|
42
|
-
let t = time *
|
|
43
|
-
let x0 = toValue - fromValue
|
|
65
|
+
let t = time * speed
|
|
44
66
|
|
|
45
67
|
var y: Double
|
|
46
|
-
if
|
|
68
|
+
if isUnderDamped {
|
|
47
69
|
// Under damped
|
|
48
70
|
let envelope = exp(-zeta * omega0 * t)
|
|
49
|
-
|
|
71
|
+
let angle = omega1 * t
|
|
72
|
+
y = toValue - envelope * (aUnder * sin(angle) + bUnder * cos(angle))
|
|
50
73
|
} else {
|
|
51
74
|
// Critically damped
|
|
52
75
|
let envelope = exp(-omega0 * t)
|
|
53
|
-
y = toValue - envelope * (
|
|
76
|
+
y = toValue - envelope * (aCritical + bCritical * t)
|
|
54
77
|
}
|
|
55
78
|
|
|
56
79
|
return y
|
|
@@ -33,19 +33,38 @@ public final class TimingAnimation: KeyboardAnimation {
|
|
|
33
33
|
super.init(fromValue: fromValue, toValue: toValue, animation: animation)
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
-
// public functions
|
|
36
|
+
// MARK: public functions
|
|
37
|
+
|
|
37
38
|
override func valueAt(time: Double) -> Double {
|
|
38
|
-
let
|
|
39
|
-
|
|
40
|
-
let fraction = min(x / frames, 1)
|
|
41
|
-
let t = findTForX(xTarget: fraction)
|
|
39
|
+
let duration = animation?.duration ?? 0.0
|
|
40
|
+
guard duration > 0 else { return toValue }
|
|
42
41
|
|
|
42
|
+
let fraction = min(time / duration, 1.0)
|
|
43
|
+
let t = findTForX(xTarget: fraction)
|
|
43
44
|
let progress = bezierY(t: t)
|
|
44
45
|
|
|
45
|
-
return fromValue + (toValue - fromValue) *
|
|
46
|
+
return fromValue + (toValue - fromValue) * progress
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
override func timingAt(value: Double) -> Double {
|
|
50
|
+
guard (toValue - fromValue) != 0 else { return 0 }
|
|
51
|
+
|
|
52
|
+
let targetY = (value - fromValue) / (toValue - fromValue)
|
|
53
|
+
let clampedY = max(min(targetY, 1.0), 0.0)
|
|
54
|
+
|
|
55
|
+
let t = findTForY(yTarget: clampedY)
|
|
56
|
+
let x = bezierX(t: t)
|
|
57
|
+
|
|
58
|
+
let duration = animation?.duration ?? 0.0
|
|
59
|
+
let time = x * duration / speed
|
|
60
|
+
|
|
61
|
+
return time
|
|
46
62
|
}
|
|
47
63
|
|
|
48
64
|
// private functions
|
|
65
|
+
|
|
66
|
+
// MARK: Bézier
|
|
67
|
+
|
|
49
68
|
private func bezier(t: CGFloat, valueForPoint: (CGPoint) -> CGFloat) -> CGFloat {
|
|
50
69
|
let u = 1 - t
|
|
51
70
|
let tt = t * t
|
|
@@ -70,39 +89,65 @@ public final class TimingAnimation: KeyboardAnimation {
|
|
|
70
89
|
return bezier(t: t) { $0.x }
|
|
71
90
|
}
|
|
72
91
|
|
|
73
|
-
private func
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
92
|
+
private func bezierDerivative(t: CGFloat, valueForPoint: (CGPoint) -> CGFloat) -> CGFloat {
|
|
93
|
+
let u = 1 - t
|
|
94
|
+
let term1 = (3 * u * u - 6 * t * u) * valueForPoint(p1)
|
|
95
|
+
let term2 = (6 * t * u - 3 * t * t) * valueForPoint(p2)
|
|
96
|
+
let term3 = 3 * t * t
|
|
97
|
+
return term1 + term2 + term3
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
private func bezierXDerivative(t: CGFloat) -> CGFloat {
|
|
101
|
+
bezierDerivative(t: t) { $0.x }
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
private func bezierYDerivative(t: CGFloat) -> CGFloat {
|
|
105
|
+
bezierDerivative(t: t) { $0.y }
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
private enum BezierComponent {
|
|
109
|
+
case x
|
|
110
|
+
case y
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
private func calculateComponents(t: CGFloat, component: BezierComponent) -> (value: CGFloat, derivative: CGFloat) {
|
|
114
|
+
switch component {
|
|
115
|
+
case .x:
|
|
116
|
+
return (bezierX(t: t), bezierXDerivative(t: t))
|
|
117
|
+
case .y:
|
|
118
|
+
return (bezierY(t: t), bezierYDerivative(t: t))
|
|
84
119
|
}
|
|
85
|
-
return t // Return the approximation of t
|
|
86
120
|
}
|
|
87
121
|
|
|
88
|
-
|
|
89
|
-
let u = 1 - t
|
|
90
|
-
let uu = u * u
|
|
91
|
-
let tt = t * t
|
|
92
|
-
let tu = t * u
|
|
122
|
+
// MARK: Newton Raphson
|
|
93
123
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
124
|
+
private func findT(
|
|
125
|
+
target: CGFloat,
|
|
126
|
+
component: BezierComponent,
|
|
127
|
+
epsilon: CGFloat = 0.0001,
|
|
128
|
+
maxIterations: Int = 100
|
|
129
|
+
) -> CGFloat {
|
|
130
|
+
var t: CGFloat = 0.5
|
|
131
|
+
for _ in 0 ..< maxIterations {
|
|
132
|
+
let (value, derivative) = calculateComponents(t: t, component: component)
|
|
133
|
+
let error = value - target
|
|
99
134
|
|
|
100
|
-
|
|
101
|
-
|
|
135
|
+
if abs(error) < epsilon {
|
|
136
|
+
break
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
t -= error / derivative
|
|
140
|
+
t = max(0, min(t, 1))
|
|
141
|
+
}
|
|
142
|
+
return t
|
|
102
143
|
}
|
|
103
144
|
|
|
104
|
-
private func
|
|
105
|
-
|
|
145
|
+
private func findTForX(xTarget: CGFloat) -> CGFloat {
|
|
146
|
+
findT(target: xTarget, component: .x)
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
private func findTForY(yTarget: CGFloat) -> CGFloat {
|
|
150
|
+
findT(target: yTarget, component: .y)
|
|
106
151
|
}
|
|
107
152
|
}
|
|
108
153
|
|
|
@@ -51,6 +51,9 @@ class KCTextInputCompositeDelegate: NSObject, UITextViewDelegate, UITextFieldDel
|
|
|
51
51
|
weak var textViewDelegate: UITextViewDelegate?
|
|
52
52
|
weak var textFieldDelegate: UITextFieldDelegate?
|
|
53
53
|
|
|
54
|
+
// Keep track of which textField we’re observing (iOS < 13 only)
|
|
55
|
+
private weak var observedTextFieldForSelection: UITextField?
|
|
56
|
+
|
|
54
57
|
public init(
|
|
55
58
|
onSelectionChange: @escaping (_ event: NSDictionary) -> Void,
|
|
56
59
|
onTextChange: @escaping (_ text: String?) -> Void
|
|
@@ -62,13 +65,35 @@ class KCTextInputCompositeDelegate: NSObject, UITextViewDelegate, UITextFieldDel
|
|
|
62
65
|
// MARK: setters/getters
|
|
63
66
|
|
|
64
67
|
public func setTextViewDelegate(delegate: UITextViewDelegate?) {
|
|
68
|
+
// remove KVO from any old textField
|
|
69
|
+
if let oldTextField = observedTextFieldForSelection {
|
|
70
|
+
removeSelectionRangeObserver(from: oldTextField)
|
|
71
|
+
}
|
|
72
|
+
|
|
65
73
|
textViewDelegate = delegate
|
|
66
74
|
textFieldDelegate = nil
|
|
75
|
+
observedTextFieldForSelection = nil
|
|
67
76
|
}
|
|
68
77
|
|
|
69
|
-
public func setTextFieldDelegate(delegate: UITextFieldDelegate?) {
|
|
78
|
+
public func setTextFieldDelegate(delegate: UITextFieldDelegate?, textField: UITextField?) {
|
|
79
|
+
// remove KVO from any old textField
|
|
80
|
+
if let oldTextField = observedTextFieldForSelection {
|
|
81
|
+
removeSelectionRangeObserver(from: oldTextField)
|
|
82
|
+
}
|
|
83
|
+
|
|
70
84
|
textFieldDelegate = delegate
|
|
71
85
|
textViewDelegate = nil
|
|
86
|
+
|
|
87
|
+
// If iOS < 13, add KVO to the actual textField object
|
|
88
|
+
if #available(iOS 13.0, *) {
|
|
89
|
+
// rely on textFieldDidChangeSelection
|
|
90
|
+
observedTextFieldForSelection = nil
|
|
91
|
+
} else {
|
|
92
|
+
if let realTextField = textField {
|
|
93
|
+
addSelectionRangeObserver(to: realTextField)
|
|
94
|
+
observedTextFieldForSelection = realTextField
|
|
95
|
+
}
|
|
96
|
+
}
|
|
72
97
|
}
|
|
73
98
|
|
|
74
99
|
// Getter for the active delegate
|
|
@@ -116,9 +141,56 @@ class KCTextInputCompositeDelegate: NSObject, UITextViewDelegate, UITextFieldDel
|
|
|
116
141
|
self.onTextChange(textField.text)
|
|
117
142
|
}
|
|
118
143
|
|
|
144
|
+
if #unavailable(iOS 13.0) {
|
|
145
|
+
DispatchQueue.main.asyncAfter(deadline: .now() + UIUtils.nextFrame) {
|
|
146
|
+
updateSelectionPosition(textInput: textField, sendEvent: self.onSelectionChange)
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
119
150
|
return textFieldDelegate?.textField?(textField, shouldChangeCharactersIn: range, replacementString: string) ?? true
|
|
120
151
|
}
|
|
121
152
|
|
|
153
|
+
func textFieldDidEndEditing(_ textField: UITextField) {
|
|
154
|
+
textFieldDelegate?.textFieldDidEndEditing?(textField)
|
|
155
|
+
|
|
156
|
+
if #unavailable(iOS 13.0) {
|
|
157
|
+
removeSelectionRangeObserver(from: textField)
|
|
158
|
+
if observedTextFieldForSelection === textField {
|
|
159
|
+
observedTextFieldForSelection = nil
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// MARK: KVO for iOS < 13
|
|
165
|
+
|
|
166
|
+
private func addSelectionRangeObserver(to textField: UITextField) {
|
|
167
|
+
textField.addObserver(
|
|
168
|
+
self,
|
|
169
|
+
forKeyPath: "selectedTextRange",
|
|
170
|
+
options: [.new],
|
|
171
|
+
context: nil
|
|
172
|
+
)
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
private func removeSelectionRangeObserver(from textField: UITextField) {
|
|
176
|
+
textField.removeObserver(self, forKeyPath: "selectedTextRange")
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// swiftlint:disable:next block_based_kvo
|
|
180
|
+
override func observeValue(
|
|
181
|
+
forKeyPath keyPath: String?,
|
|
182
|
+
of object: Any?,
|
|
183
|
+
change: [NSKeyValueChangeKey: Any]?,
|
|
184
|
+
context: UnsafeMutableRawPointer?
|
|
185
|
+
) {
|
|
186
|
+
guard keyPath == "selectedTextRange", let textField = object as? UITextField else {
|
|
187
|
+
super.observeValue(forKeyPath: keyPath, of: object, change: change, context: context)
|
|
188
|
+
return
|
|
189
|
+
}
|
|
190
|
+
// selection changed => forward the event
|
|
191
|
+
updateSelectionPosition(textInput: textField, sendEvent: onSelectionChange)
|
|
192
|
+
}
|
|
193
|
+
|
|
122
194
|
// MARK: call forwarding
|
|
123
195
|
|
|
124
196
|
override func responds(to aSelector: Selector!) -> Bool {
|