android: keyboard height tracking via JNI/FFI bridge

- C bridge (keyboard_bridge.c) stores keyboard state in globals.
  Kotlin writes via JNI, Dart reads via FFI — zero async delay,
  same architecture as iOS.
- WindowInsetsAnimation.Callback tracks open/close per-frame.
- OnGlobalLayoutListener catches silent height changes (emoji
  keyboard resize, floating keyboard toggle).
- Dart animation replay stays iOS-only; Android reads native
  per-frame values directly.
- Cleaned up old Java stub, updated build.gradle for Kotlin + CMake
  with 16KB page alignment (Android 15+).
- Example app rewritten to demonstrate UxKeyboard usage.
This commit is contained in:
agra
2026-04-15 23:49:16 +03:00
parent a1ab667178
commit 0be198e388
13 changed files with 336 additions and 151 deletions

View File

@@ -5,11 +5,10 @@ import 'package:flutter/animation.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/scheduler.dart';
final bool _isIOS = Platform.isIOS;
DynamicLibrary? _initLib() {
if (!_isIOS) return null;
return DynamicLibrary.process();
if (Platform.isIOS) return DynamicLibrary.process();
if (Platform.isAndroid) return DynamicLibrary.open('libux_keyboard.so');
return null;
}
final DynamicLibrary? _lib = _initLib();
@@ -90,7 +89,7 @@ class _SampledCurve extends Curve {
class UxKeyboard with ChangeNotifier {
UxKeyboard._() {
if (!_isIOS) return;
if (_lib == null) return;
SchedulerBinding.instance.addPersistentFrameCallback(_onFrame);
}
@@ -116,18 +115,22 @@ class UxKeyboard with ChangeNotifier {
final ts = timestamp.inMicroseconds / Duration.microsecondsPerSecond;
// Detect new keyboard animation from native
final gen = _uxAnimGen?.call() ?? 0;
if (gen != _lastAnimGen) {
_lastAnimGen = gen;
final target = _uxAnimTarget?.call() ?? 0;
final duration = _uxAnimDuration?.call() ?? 0;
if (duration > 0) {
_animFrom = _height;
_animTo = target;
_animDuration = duration - 0.01; // finish 10ms ahead of native
_animStartTime = ts - 0.016; // compensate 2-frame pipeline delay
_isAnimating = true;
// On iOS, replay the animation in Dart with a head start (native only
// gives start/end via notification). On Android, WindowInsetsAnimation
// pushes per-frame values directly — no replay needed.
if (Platform.isIOS) {
final gen = _uxAnimGen?.call() ?? 0;
if (gen != _lastAnimGen) {
_lastAnimGen = gen;
final target = _uxAnimTarget?.call() ?? 0;
final duration = _uxAnimDuration?.call() ?? 0;
if (duration > 0) {
_animFrom = _height;
_animTo = target;
_animDuration = duration - 0.01; // finish 10ms ahead of native
_animStartTime = ts - 0.016; // compensate 2-frame pipeline delay
_isAnimating = true;
}
}
}
@@ -155,7 +158,8 @@ class UxKeyboard with ChangeNotifier {
notifyListeners();
}
if (_isAnimating) {
// Keep scheduling frames while animating or keyboard is active
if (_isAnimating || (!Platform.isIOS && (h > 0 || _height > 0))) {
SchedulerBinding.instance.scheduleFrame();
}
}

View File

@@ -3,15 +3,4 @@ library ux;
export 'src/bend_box.dart';
export 'src/json_extension.dart';
export 'src/bezier.dart';
import 'dart:async';
import 'package:flutter/services.dart';
class UX {
static const MethodChannel _channel = const MethodChannel('ux');
static Future<String> get platformVersion async {
final String version = await _channel.invokeMethod('getPlatformVersion');
return version;
}
}
export 'src/keyboard.dart';