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() {
|
private fun setupInsetsCallback() {
|
||||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) return
|
|
||||||
val view = activity?.window?.decorView ?: 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 {
|
view.viewTreeObserver.addOnGlobalLayoutListener {
|
||||||
val insets = view.rootWindowInsets?.getInsets(WindowInsets.Type.ime()) ?: Insets.NONE
|
val rootInsets = view.rootWindowInsets ?: return@addOnGlobalLayoutListener
|
||||||
val density = view.resources.displayMetrics.density
|
publishImeHeight(view, rootInsets)
|
||||||
val height = insets.bottom.toDouble() / density
|
}
|
||||||
|
|
||||||
if (height != KeyboardBridge.nGetSystemHeight()) {
|
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) {
|
||||||
KeyboardBridge.nSetSystemHeight(height)
|
// No WindowInsetsAnimation API on pre-R. The global-layout listener
|
||||||
KeyboardBridge.nSetHeight(height)
|
// 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) {
|
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) {
|
if ((h - _height).abs() > 0.5) {
|
||||||
_height = h;
|
_height = h;
|
||||||
notifyListeners();
|
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 &&
|
final curveActive = _isAnimating &&
|
||||||
(ts - _animStartTime) < _animDuration;
|
(ts - _animStartTime) < _animDuration;
|
||||||
if (curveActive || (!Platform.isIOS && (h > 0 || _height > 0))) {
|
if (curveActive || (!Platform.isIOS && (h > 0 || _height > 0))) {
|
||||||
|
|||||||
Reference in New Issue
Block a user