react-native-controlled-input 0.18.0 → 0.19.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
|
@@ -27,6 +27,7 @@ import androidx.savedstate.SavedStateRegistryOwner
|
|
|
27
27
|
import androidx.savedstate.setViewTreeSavedStateRegistryOwner
|
|
28
28
|
import com.facebook.react.bridge.ReactContext
|
|
29
29
|
import com.facebook.react.uimanager.UIManagerHelper
|
|
30
|
+
import com.facebook.react.uimanager.events.Event
|
|
30
31
|
import kotlinx.coroutines.flow.MutableStateFlow
|
|
31
32
|
|
|
32
33
|
/**
|
|
@@ -171,72 +172,137 @@ class ControlledInputView : LinearLayout, LifecycleOwner {
|
|
|
171
172
|
}
|
|
172
173
|
|
|
173
174
|
/**
|
|
174
|
-
*
|
|
175
|
-
*
|
|
176
|
-
*
|
|
177
|
-
* Chain: EdgeToEdgeViewRegistry.get() → .callback → .layoutObserver → .lastFocusedInput / .syncUpLayout()
|
|
175
|
+
* EdgeToEdgeViewRegistry.get() → callback → FocusedInputObserver.
|
|
176
|
+
* Null if react-native-keyboard-controller is missing or not initialized.
|
|
178
177
|
*/
|
|
179
|
-
private fun
|
|
180
|
-
Log.d(TAG, "restoreKbcTracking: starting reflection chain")
|
|
178
|
+
private fun resolveKbcFocusedInputObserver(): Any? {
|
|
181
179
|
try {
|
|
182
|
-
// 1. EdgeToEdgeViewRegistry is a Kotlin object — access via INSTANCE field
|
|
183
180
|
val registryClass =
|
|
184
181
|
Class.forName("com.reactnativekeyboardcontroller.views.EdgeToEdgeViewRegistry")
|
|
185
182
|
val registryInstance = registryClass.getField("INSTANCE").get(null)
|
|
186
183
|
val edgeToEdgeView =
|
|
187
184
|
registryClass.getDeclaredMethod("get").invoke(registryInstance)
|
|
188
185
|
?: run {
|
|
189
|
-
Log.w(TAG, "
|
|
190
|
-
return
|
|
186
|
+
Log.w(TAG, "resolveKbcFocusedInputObserver: EdgeToEdgeViewRegistry.get() == null")
|
|
187
|
+
return null
|
|
191
188
|
}
|
|
192
189
|
|
|
193
|
-
// 2. callback: KeyboardAnimationCallback (internal var — find by type)
|
|
194
190
|
val callbackField =
|
|
195
191
|
edgeToEdgeView.javaClass.declaredFields.firstOrNull {
|
|
196
192
|
it.type.simpleName == "KeyboardAnimationCallback"
|
|
197
193
|
}
|
|
198
194
|
?: run {
|
|
199
|
-
Log.w(TAG, "
|
|
200
|
-
return
|
|
195
|
+
Log.w(TAG, "resolveKbcFocusedInputObserver: KeyboardAnimationCallback field not found")
|
|
196
|
+
return null
|
|
201
197
|
}
|
|
202
198
|
callbackField.isAccessible = true
|
|
203
199
|
val callback =
|
|
204
200
|
callbackField.get(edgeToEdgeView)
|
|
205
201
|
?: run {
|
|
206
|
-
Log.w(TAG, "
|
|
207
|
-
return
|
|
202
|
+
Log.w(TAG, "resolveKbcFocusedInputObserver: callback == null")
|
|
203
|
+
return null
|
|
208
204
|
}
|
|
209
205
|
|
|
210
|
-
// 3. layoutObserver: FocusedInputObserver (internal var — find by type)
|
|
211
206
|
val observerField =
|
|
212
207
|
callback.javaClass.declaredFields.firstOrNull {
|
|
213
208
|
it.type.simpleName == "FocusedInputObserver"
|
|
214
209
|
}
|
|
215
210
|
?: run {
|
|
216
|
-
Log.w(TAG, "
|
|
217
|
-
return
|
|
211
|
+
Log.w(TAG, "resolveKbcFocusedInputObserver: FocusedInputObserver field not found")
|
|
212
|
+
return null
|
|
218
213
|
}
|
|
219
214
|
observerField.isAccessible = true
|
|
220
|
-
|
|
221
|
-
|
|
215
|
+
return observerField.get(callback)
|
|
216
|
+
?: run {
|
|
217
|
+
Log.w(TAG, "resolveKbcFocusedInputObserver: layoutObserver == null")
|
|
218
|
+
null
|
|
219
|
+
}
|
|
220
|
+
} catch (_: ClassNotFoundException) {
|
|
221
|
+
Log.d(TAG, "resolveKbcFocusedInputObserver: keyboard-controller not on classpath")
|
|
222
|
+
return null
|
|
223
|
+
} catch (e: Exception) {
|
|
224
|
+
Log.w(TAG, "resolveKbcFocusedInputObserver: ${e.javaClass.simpleName}: ${e.message}")
|
|
225
|
+
return null
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/** Approximate one line height in dp for JS customHeight when proxy has no Layout yet. */
|
|
230
|
+
private fun approximateSelectionEndYDp(): Double {
|
|
231
|
+
viewModel.inputStyle.value?.fontSize?.toDouble()?.takeIf { it > 0 }?.let { return it }
|
|
232
|
+
val dm = resources.displayMetrics
|
|
233
|
+
return (focusProxy.textSize / dm.density).toDouble().coerceAtLeast(12.0)
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Synthetic topFocusedInputSelectionChanged without a compile dependency on KBC.
|
|
238
|
+
* Helps older KeyboardAwareScrollView when proxy never gets android.text.Layout in time.
|
|
239
|
+
*/
|
|
240
|
+
private fun dispatchSyntheticKbcSelectionEvent(observer: Any) {
|
|
241
|
+
val reactContext = context as? ReactContext ?: return
|
|
242
|
+
try {
|
|
243
|
+
val epField = observer.javaClass.getDeclaredField("eventPropagationView")
|
|
244
|
+
epField.isAccessible = true
|
|
245
|
+
val propagationId = (epField.get(observer) as View).id
|
|
246
|
+
|
|
247
|
+
val surfaceId = UIManagerHelper.getSurfaceId(this)
|
|
248
|
+
val targetId = id
|
|
249
|
+
val endY = approximateSelectionEndYDp()
|
|
250
|
+
|
|
251
|
+
val dataClz =
|
|
252
|
+
Class.forName("com.reactnativekeyboardcontroller.events.FocusedInputSelectionChangedEventData")
|
|
253
|
+
val dataCtor =
|
|
254
|
+
dataClz.declaredConstructors.singleOrNull { it.parameterTypes.size == 7 }
|
|
222
255
|
?: run {
|
|
223
|
-
Log.w(TAG, "
|
|
256
|
+
Log.w(TAG, "dispatchSyntheticKbcSelectionEvent: no 7-arg data ctor")
|
|
224
257
|
return
|
|
225
258
|
}
|
|
259
|
+
dataCtor.isAccessible = true
|
|
260
|
+
val data =
|
|
261
|
+
dataCtor.newInstance(targetId, 0.0, 0.0, 0.0, endY, 0, 0)
|
|
262
|
+
|
|
263
|
+
val eventClz =
|
|
264
|
+
Class.forName("com.reactnativekeyboardcontroller.events.FocusedInputSelectionChangedEvent")
|
|
265
|
+
val eventCtor =
|
|
266
|
+
eventClz.getConstructor(
|
|
267
|
+
Int::class.javaPrimitiveType,
|
|
268
|
+
Int::class.javaPrimitiveType,
|
|
269
|
+
dataClz,
|
|
270
|
+
)
|
|
271
|
+
val event = eventCtor.newInstance(surfaceId, propagationId, data) as Event<*>
|
|
272
|
+
|
|
273
|
+
UIManagerHelper.getEventDispatcherForReactTag(reactContext, propagationId)
|
|
274
|
+
?.dispatchEvent(event)
|
|
275
|
+
Log.d(
|
|
276
|
+
TAG,
|
|
277
|
+
"dispatchSyntheticKbcSelectionEvent: propagationId=$propagationId target=$targetId endY(dp)=$endY",
|
|
278
|
+
)
|
|
279
|
+
} catch (e: Exception) {
|
|
280
|
+
Log.w(TAG, "dispatchSyntheticKbcSelectionEvent: ${e.javaClass.simpleName}: ${e.message}")
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* Reflection: lastFocusedInput = focusProxy, syncUpLayout(), then synthetic selection
|
|
286
|
+
* so JS gets customHeight (selection.end.y) without waiting for KeyboardControllerSelectionWatcher.
|
|
287
|
+
*/
|
|
288
|
+
private fun restoreKbcTracking() {
|
|
289
|
+
Log.d(TAG, "restoreKbcTracking: starting reflection chain")
|
|
290
|
+
try {
|
|
291
|
+
val observer = resolveKbcFocusedInputObserver() ?: return
|
|
226
292
|
|
|
227
|
-
// 4. Set lastFocusedInput = focusProxy (private var)
|
|
228
293
|
val lastFocusedField = observer.javaClass.getDeclaredField("lastFocusedInput")
|
|
229
294
|
lastFocusedField.isAccessible = true
|
|
230
295
|
lastFocusedField.set(observer, focusProxy)
|
|
231
296
|
Log.d(TAG, "restoreKbcTracking: lastFocusedInput set to focusProxy")
|
|
232
297
|
|
|
233
|
-
// 5. Call syncUpLayout() — public fun, dispatches FocusedInputLayoutChangedEvent to JS
|
|
234
298
|
val syncMethod = observer.javaClass.getDeclaredMethod("syncUpLayout")
|
|
235
299
|
syncMethod.isAccessible = true
|
|
236
300
|
syncMethod.invoke(observer)
|
|
237
|
-
Log.d(TAG, "restoreKbcTracking:
|
|
301
|
+
Log.d(TAG, "restoreKbcTracking: syncUpLayout() invoked")
|
|
302
|
+
|
|
303
|
+
dispatchSyntheticKbcSelectionEvent(observer)
|
|
238
304
|
} catch (e: Exception) {
|
|
239
|
-
Log.w(TAG, "restoreKbcTracking:
|
|
305
|
+
Log.w(TAG, "restoreKbcTracking: ${e.javaClass.simpleName}: ${e.message}")
|
|
240
306
|
}
|
|
241
307
|
}
|
|
242
308
|
|
|
@@ -417,7 +483,7 @@ class ControlledInputView : LinearLayout, LifecycleOwner {
|
|
|
417
483
|
)
|
|
418
484
|
},
|
|
419
485
|
onFocus = {
|
|
420
|
-
Log.d(TAG, "
|
|
486
|
+
Log.d(TAG, "Compose onFocus id=$id isRestoring=$isRestoringComposeFocus")
|
|
421
487
|
if (isRestoringComposeFocus) {
|
|
422
488
|
// Second onFocus triggered by focusRequester.requestFocus() inside the restoration
|
|
423
489
|
// dance — KBC is being synced via reflection, keyboard is showing.
|
|
@@ -435,7 +501,7 @@ class ControlledInputView : LinearLayout, LifecycleOwner {
|
|
|
435
501
|
}
|
|
436
502
|
},
|
|
437
503
|
onBlur = {
|
|
438
|
-
Log.d(TAG, "
|
|
504
|
+
Log.d(TAG, "Compose onBlur id=$id proxyFocused=${focusProxy.isFocused}")
|
|
439
505
|
if (focusProxy.isFocused) {
|
|
440
506
|
// Proxy stole Android focus from AndroidComposeView → this blur is synthetic.
|
|
441
507
|
// Restore Compose focus so the keyboard stays/re-appears.
|
package/package.json
CHANGED