cc243b7b0a6499f610dcb8d9b6dbfc0ed530f283
Black-screen + extra-90°-rotation on Android both came from
AVFoundation vs CameraX behaving differently at the preview output:
- AVFoundation: data-output connection's `videoOrientation`
pre-rotates sample buffers. The Flutter Texture displays them
upright; `device.activeFormat` reports the sensor-native size
synchronously.
- CameraX: the SurfaceProvider hands back a Surface; CameraX
writes raw sensor frames into it. Rotation is a *transform hint*
via Preview.setTargetRotation that consumers must apply
themselves. And the final negotiated resolution isn't known
until the first SurfaceRequest fires — which happens AFTER
bindToLifecycle, AFTER lifecycle.start, async on the camera
executor. So `create` was returning Size(0,0).
Surface extension to bridge the gap:
- UxCameraValue.previewRotationQuarterTurns (int 0/1/2/3).
iOS native always emits 0; Android native emits
`(sensorRotationDegrees / 90) % 4` for the active camera.
[UxCameraPreview] wraps the Texture in a RotatedBox by that many
quarter-turns (applied *before* the front-cam mirror so the
flip lives in screen space, not sensor space).
- UxCameraPreviewSizeChanged event. Android emits this from
PreviewSink.onResize whenever a SurfaceRequest carries a new
resolution; the controller copies it into value.previewSize.
First emission is what unblocks the camera_thumb's SizedBox
from its initial 0x0 = "render nothing" state.
- UxCameraBackend.setDescription's return changed from `Size` to
`({Size previewSize, int previewRotationQuarterTurns})` so
a lens swap can both update the rotation and signal that a new
previewSizeChanged event is incoming.
iOS continues to send previewSize in the create result (the active
format is known synchronously); no previewSizeChanged emission is
needed there. The new field is set to 0 in both create and
setDescription results on iOS.
ux
A Flutter toolkit for building fluid, native-feeling UIs.
UxKeyboard
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. UxKeyboard 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 = UxKeyboard.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 |
|---|---|
UxKeyboard.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 withUxKeyboard - 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%