keyboard(android): pre-R IME tracking + flush dirty marks during persistent callback
KeyboardPlugin.setupInsetsCallback used to early-return on SDK < R, so the FFI height stayed at 0 on API 29 devices like the Huawei Mate 20 Pro — the chat composer never tracked the IME. Run the global-layout listener on all SDKs, and on pre-R also wire setOnApplyWindowInsetsListener since EMUI 10's IME-hide dispatches new insets without a follow-up layout pass. Pre-R IME height comes from systemWindowInsetBottom − stableInsetBottom (stable insets exclude things that animate in/out). Inside XKeyboard._onFrame, follow notifyListeners with an explicit scheduleFrame. _onFrame runs as a persistent frame callback after the build phase has finished, so setState in listeners marks elements dirty but ensureVisualUpdate is a no-op in this phase — the steady-state pump masked the issue while the keyboard was open but on the close-edge (h transitions to 0) the pump stops and the final rebuild was never scheduled.
This commit is contained in:
@@ -80,19 +80,26 @@ class KeyboardPlugin : NativePlugin, MethodChannel.MethodCallHandler {
|
||||
}
|
||||
|
||||
private fun setupInsetsCallback() {
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) return
|
||||
val view = activity?.window?.decorView ?: return
|
||||
|
||||
// Catch inset changes that don't trigger animations (e.g., emoji keyboard resize)
|
||||
// Layout-driven changes (e.g. emoji keyboard resize on R+ where the
|
||||
// animation callback doesn't fire).
|
||||
view.viewTreeObserver.addOnGlobalLayoutListener {
|
||||
val insets = view.rootWindowInsets?.getInsets(WindowInsets.Type.ime()) ?: Insets.NONE
|
||||
val density = view.resources.displayMetrics.density
|
||||
val height = insets.bottom.toDouble() / density
|
||||
val rootInsets = view.rootWindowInsets ?: return@addOnGlobalLayoutListener
|
||||
publishImeHeight(view, rootInsets)
|
||||
}
|
||||
|
||||
if (height != KeyboardBridge.nGetSystemHeight()) {
|
||||
KeyboardBridge.nSetSystemHeight(height)
|
||||
KeyboardBridge.nSetHeight(height)
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) {
|
||||
// No WindowInsetsAnimation API on pre-R. The global-layout listener
|
||||
// catches IME-open (activity resizes) but on some OEMs (notably
|
||||
// EMUI 10) IME-hide dispatches new insets without a follow-up
|
||||
// layout — leaving that listener silent. setOnApplyWindowInsetsListener
|
||||
// fires on every inset dispatch, so it catches both ends.
|
||||
view.setOnApplyWindowInsetsListener { v, insets ->
|
||||
publishImeHeight(v, insets)
|
||||
v.onApplyWindowInsets(insets)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
view.setWindowInsetsAnimationCallback(object : WindowInsetsAnimation.Callback(DISPATCH_MODE_STOP) {
|
||||
@@ -140,4 +147,23 @@ class KeyboardPlugin : NativePlugin, MethodChannel.MethodCallHandler {
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
private fun publishImeHeight(view: android.view.View, insets: WindowInsets) {
|
||||
val density = view.resources.displayMetrics.density
|
||||
val imePx: Int = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||
insets.getInsets(WindowInsets.Type.ime()).bottom
|
||||
} else {
|
||||
// systemWindowInsetBottom = nav bar + IME; stableInsetBottom = nav
|
||||
// bar only (stable insets exclude things that animate in/out like
|
||||
// the IME). The difference isolates IME height.
|
||||
@Suppress("DEPRECATION")
|
||||
(insets.systemWindowInsetBottom - insets.stableInsetBottom).coerceAtLeast(0)
|
||||
}
|
||||
val height = imePx.toDouble() / density
|
||||
|
||||
if (height != KeyboardBridge.nGetSystemHeight()) {
|
||||
KeyboardBridge.nSetSystemHeight(height)
|
||||
KeyboardBridge.nSetHeight(height)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -229,9 +229,16 @@ class XKeyboard with ChangeNotifier {
|
||||
if ((h - _height).abs() > 0.5) {
|
||||
_height = h;
|
||||
notifyListeners();
|
||||
// notifyListeners triggers setState in listeners, which marks elements
|
||||
// dirty. We're inside a persistent frame callback (after the build phase
|
||||
// has already run), so `ensureVisualUpdate` is a no-op — without an
|
||||
// explicit scheduleFrame the dirty marks never get flushed. This matters
|
||||
// especially on the down-edge (h transitions to 0) where the steady-state
|
||||
// pump below stops scheduling.
|
||||
SchedulerBinding.instance.scheduleFrame();
|
||||
}
|
||||
|
||||
// Schedule frames while the curve is still running.
|
||||
// Steady-state pump while the curve is still running or the keyboard is up.
|
||||
final curveActive = _isAnimating &&
|
||||
(ts - _animStartTime) < _animDuration;
|
||||
if (curveActive || (!Platform.isIOS && (h > 0 || _height > 0))) {
|
||||
|
||||
Reference in New Issue
Block a user