96df891b9d7ae15d1293b223e55f34d3f4f51ce4
The Dart-side predictive curve plays a sampled LUT to lead the system IME animation by `_headStart`, so the composer can render where the IME will be by the time the Flutter frame paints. When the LUT hasn't yet adapted to the actual device curve, the prediction runs above the IME's real position and the composer overshoots — quite visibly in debug mode where Flutter's lower frame rate amplifies per-frame divergence, milder in release where the rate is higher and learning converges faster. Cap each per-frame `h` against the system-reported value (which is per-vsync fresh on Android R+ via WindowInsetsAnimation.onProgress and iOS via CADisplayLink). The clamp is a no-op on Android pre-R because the native value snaps to target up-front, so the Huawei pre-R fallback keeps animating via the LUT alone. Net effect on adapted devices: the predictive lead is sacrificed for a per-frame "track the IME exactly" guarantee — no overshoot regardless of LUT freshness. Also narrow the previous-commit's `scheduleFrame` after notifyListeners to fire only at the close-edge (`!isIOS && _height == 0`). Mid-animation the steady-state pump or curveActive condition already schedules; only the down-to-zero transition needs the explicit flush.
ux
A Flutter toolkit for building fluid, native-feeling UIs.
XKeyboard
Frame-accurate keyboard height tracking for iOS and Android, with interactive dismiss.
Flutter's built-in MediaQuery.viewInsets.bottom lags behind the actual keyboard position
and doesn't support interactive dismiss. XKeyboard reads the keyboard height directly
from the native layer via FFI — zero channel latency, every frame.
Features
- Real-time height — reads the keyboard's actual position each frame via FFI (iOS) / JNI (Android)
- Native animation curves — sampled from
CADisplayLink(iOS) andWindowInsetsAnimation(Android), with adaptive learning that refines the curve from observations - Interactive dismiss — swipe the keyboard down like iMessage/Telegram, with snap-back or dismiss
- Scroll freeze —
isTrackingflag lets you freeze scrolling during interactive dismiss
Quick start
final keyboard = XKeyboard.instance;
// Enable swipe-to-dismiss. trackingInset is the height of your input bar.
keyboard.enableInteractiveDismiss(trackingInset: 56);
Use ListenableBuilder to rebuild when the keyboard height changes:
Scaffold(
resizeToAvoidBottomInset: false, // we handle it ourselves
body: ListenableBuilder(
listenable: keyboard,
builder: (context, _) {
final keyboardHeight = keyboard.height;
final safeBottom = MediaQuery.viewPaddingOf(context).bottom;
final bottom = max(keyboardHeight, safeBottom);
return Column(
children: [
Expanded(
child: ListView.builder(
reverse: true,
// Freeze scrolling during interactive dismiss
physics: keyboard.isTracking
? NeverScrollableScrollPhysics()
: null,
// ...
),
),
Container(
padding: EdgeInsets.only(bottom: 8 + bottom),
// your input bar
),
],
);
},
),
);
API
| Member | Description |
|---|---|
XKeyboard.instance |
Singleton instance |
.height |
Current keyboard height in logical pixels |
.systemHeight |
Last system-reported keyboard height |
.isOpen |
Whether the keyboard is visible |
.isTracking |
Whether a dismiss pan gesture is active |
.enableInteractiveDismiss({trackingInset}) |
Enable swipe-to-dismiss |
.disableInteractiveDismiss() |
Disable swipe-to-dismiss |
addListener / removeListener |
Standard ChangeNotifier API |
Key points
- Set
resizeToAvoidBottomInset: falseon yourScaffold— otherwise Flutter's built-in resize fights withXKeyboard - Use
MediaQuery.viewPaddingOf(context).bottomfor the safe area (notpaddingOf, which is consumed byScaffold) - Use
max(keyboardHeight, safeBottom)for bottom padding — the keyboard height includes the safe area when open, andsafeBottomcovers the home indicator when closed
Other utilities
- BendBox — a flexible layout widget
- Bezier — bezier curve utilities
Languages
Dart
40.2%
Swift
29.1%
Kotlin
18.4%
Java
5.2%
C++
3.1%
Other
4%