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:
@@ -2,33 +2,53 @@ group 'io.swipelab.ux'
|
||||
version '1.0-SNAPSHOT'
|
||||
|
||||
buildscript {
|
||||
ext.kotlin_version = '1.9.22'
|
||||
repositories {
|
||||
google()
|
||||
jcenter()
|
||||
mavenCentral()
|
||||
}
|
||||
|
||||
dependencies {
|
||||
classpath 'com.android.tools.build:gradle:3.2.1'
|
||||
classpath 'com.android.tools.build:gradle:8.1.0'
|
||||
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
|
||||
}
|
||||
}
|
||||
|
||||
rootProject.allprojects {
|
||||
repositories {
|
||||
google()
|
||||
jcenter()
|
||||
mavenCentral()
|
||||
}
|
||||
}
|
||||
|
||||
apply plugin: 'com.android.library'
|
||||
apply plugin: 'kotlin-android'
|
||||
|
||||
android {
|
||||
compileSdkVersion 28
|
||||
namespace 'io.swipelab.ux'
|
||||
compileSdk 34
|
||||
|
||||
defaultConfig {
|
||||
minSdkVersion 16
|
||||
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
|
||||
minSdk 21
|
||||
externalNativeBuild {
|
||||
cmake {
|
||||
cppFlags ""
|
||||
}
|
||||
}
|
||||
}
|
||||
lintOptions {
|
||||
disable 'InvalidPackage'
|
||||
|
||||
externalNativeBuild {
|
||||
cmake {
|
||||
path "src/main/jni/CMakeLists.txt"
|
||||
}
|
||||
}
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility JavaVersion.VERSION_1_8
|
||||
targetCompatibility JavaVersion.VERSION_1_8
|
||||
}
|
||||
|
||||
kotlinOptions {
|
||||
jvmTarget = '1.8'
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,25 +0,0 @@
|
||||
package io.swipelab.ux;
|
||||
|
||||
import io.flutter.plugin.common.MethodCall;
|
||||
import io.flutter.plugin.common.MethodChannel;
|
||||
import io.flutter.plugin.common.MethodChannel.MethodCallHandler;
|
||||
import io.flutter.plugin.common.MethodChannel.Result;
|
||||
import io.flutter.plugin.common.PluginRegistry.Registrar;
|
||||
|
||||
/** UxPlugin */
|
||||
public class UxPlugin implements MethodCallHandler {
|
||||
/** Plugin registration. */
|
||||
public static void registerWith(Registrar registrar) {
|
||||
final MethodChannel channel = new MethodChannel(registrar.messenger(), "ux");
|
||||
channel.setMethodCallHandler(new UxPlugin());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onMethodCall(MethodCall call, Result result) {
|
||||
if (call.method.equals("getPlatformVersion")) {
|
||||
result.success("Android " + android.os.Build.VERSION.RELEASE);
|
||||
} else {
|
||||
result.notImplemented();
|
||||
}
|
||||
}
|
||||
}
|
||||
8
android/src/main/jni/CMakeLists.txt
Normal file
8
android/src/main/jni/CMakeLists.txt
Normal file
@@ -0,0 +1,8 @@
|
||||
cmake_minimum_required(VERSION 3.10)
|
||||
project(ux_keyboard)
|
||||
|
||||
add_library(ux_keyboard SHARED keyboard_bridge.c)
|
||||
target_link_libraries(ux_keyboard log)
|
||||
|
||||
# 16KB page size support (required for Android 15+)
|
||||
target_link_options(ux_keyboard PRIVATE "-Wl,-z,max-page-size=16384")
|
||||
53
android/src/main/jni/keyboard_bridge.c
Normal file
53
android/src/main/jni/keyboard_bridge.c
Normal file
@@ -0,0 +1,53 @@
|
||||
#include <jni.h>
|
||||
#include <stdint.h>
|
||||
|
||||
// Shared state — Kotlin writes, Dart reads via FFI
|
||||
static double g_keyboard_height = 0;
|
||||
static double g_system_height = 0;
|
||||
static int32_t g_is_tracking = 0;
|
||||
static double g_anim_target = 0;
|
||||
static double g_anim_duration = 0;
|
||||
static int32_t g_anim_generation = 0;
|
||||
|
||||
// --- Dart FFI reads (same signatures as iOS) ---
|
||||
|
||||
double ux_keyboard_height(void) { return g_keyboard_height; }
|
||||
double ux_system_keyboard_height(void) { return g_system_height; }
|
||||
int32_t ux_is_tracking(void) { return g_is_tracking; }
|
||||
double ux_keyboard_anim_target(void) { return g_anim_target; }
|
||||
double ux_keyboard_anim_duration(void) { return g_anim_duration; }
|
||||
int32_t ux_keyboard_anim_gen(void) { return g_anim_generation; }
|
||||
|
||||
// --- Stubs for Dart FFI parity with iOS ---
|
||||
|
||||
void ux_enable_interactive_dismiss(double inset) { (void)inset; }
|
||||
void ux_disable_interactive_dismiss(void) {}
|
||||
|
||||
// --- Kotlin JNI writes ---
|
||||
|
||||
JNIEXPORT void JNICALL
|
||||
Java_io_swipelab_ux_KeyboardBridge_nSetHeight(JNIEnv *env, jclass cls, jdouble h) {
|
||||
g_keyboard_height = h;
|
||||
}
|
||||
|
||||
JNIEXPORT void JNICALL
|
||||
Java_io_swipelab_ux_KeyboardBridge_nSetSystemHeight(JNIEnv *env, jclass cls, jdouble h) {
|
||||
g_system_height = h;
|
||||
}
|
||||
|
||||
JNIEXPORT jdouble JNICALL
|
||||
Java_io_swipelab_ux_KeyboardBridge_nGetSystemHeight(JNIEnv *env, jclass cls) {
|
||||
return g_system_height;
|
||||
}
|
||||
|
||||
JNIEXPORT void JNICALL
|
||||
Java_io_swipelab_ux_KeyboardBridge_nSetTracking(JNIEnv *env, jclass cls, jint v) {
|
||||
g_is_tracking = v;
|
||||
}
|
||||
|
||||
JNIEXPORT void JNICALL
|
||||
Java_io_swipelab_ux_KeyboardBridge_nSetAnim(JNIEnv *env, jclass cls, jdouble target, jdouble duration) {
|
||||
g_anim_target = target;
|
||||
g_anim_duration = duration;
|
||||
g_anim_generation++;
|
||||
}
|
||||
14
android/src/main/kotlin/io/swipelab/ux/KeyboardBridge.kt
Normal file
14
android/src/main/kotlin/io/swipelab/ux/KeyboardBridge.kt
Normal file
@@ -0,0 +1,14 @@
|
||||
package io.swipelab.ux
|
||||
|
||||
/// JNI bridge to the C globals that Dart reads via FFI.
|
||||
object KeyboardBridge {
|
||||
init {
|
||||
System.loadLibrary("ux_keyboard")
|
||||
}
|
||||
|
||||
@JvmStatic external fun nSetHeight(h: Double)
|
||||
@JvmStatic external fun nSetSystemHeight(h: Double)
|
||||
@JvmStatic external fun nGetSystemHeight(): Double
|
||||
@JvmStatic external fun nSetTracking(v: Int)
|
||||
@JvmStatic external fun nSetAnim(target: Double, duration: Double)
|
||||
}
|
||||
116
android/src/main/kotlin/io/swipelab/ux/KeyboardPlugin.kt
Normal file
116
android/src/main/kotlin/io/swipelab/ux/KeyboardPlugin.kt
Normal file
@@ -0,0 +1,116 @@
|
||||
package io.swipelab.ux
|
||||
|
||||
import android.app.Activity
|
||||
import android.graphics.Insets
|
||||
import android.os.Build
|
||||
import android.view.WindowInsets
|
||||
import android.view.WindowInsetsAnimation
|
||||
import io.flutter.embedding.engine.plugins.FlutterPlugin
|
||||
import io.flutter.embedding.engine.plugins.activity.ActivityAware
|
||||
import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding
|
||||
import io.flutter.plugin.common.MethodCall
|
||||
import io.flutter.plugin.common.MethodChannel
|
||||
|
||||
class KeyboardPlugin : FlutterPlugin, MethodChannel.MethodCallHandler, ActivityAware {
|
||||
private var methodChannel: MethodChannel? = null
|
||||
private var activity: Activity? = null
|
||||
|
||||
override fun onAttachedToEngine(binding: FlutterPlugin.FlutterPluginBinding) {
|
||||
methodChannel = MethodChannel(binding.binaryMessenger, "ux/keyboard").also {
|
||||
it.setMethodCallHandler(this)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDetachedFromEngine(binding: FlutterPlugin.FlutterPluginBinding) {
|
||||
methodChannel?.setMethodCallHandler(null)
|
||||
methodChannel = null
|
||||
}
|
||||
|
||||
override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) {
|
||||
when (call.method) {
|
||||
"enableInteractiveDismiss" -> result.success(null)
|
||||
"disableInteractiveDismiss" -> result.success(null)
|
||||
else -> result.notImplemented()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onAttachedToActivity(binding: ActivityPluginBinding) {
|
||||
activity = binding.activity
|
||||
setupInsetsCallback()
|
||||
}
|
||||
|
||||
override fun onDetachedFromActivity() {
|
||||
activity = null
|
||||
}
|
||||
|
||||
override fun onReattachedToActivityForConfigChanges(binding: ActivityPluginBinding) {
|
||||
activity = binding.activity
|
||||
setupInsetsCallback()
|
||||
}
|
||||
|
||||
override fun onDetachedFromActivityForConfigChanges() {
|
||||
activity = null
|
||||
}
|
||||
|
||||
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)
|
||||
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
|
||||
|
||||
if (height != KeyboardBridge.nGetSystemHeight()) {
|
||||
KeyboardBridge.nSetSystemHeight(height)
|
||||
KeyboardBridge.nSetHeight(height)
|
||||
}
|
||||
}
|
||||
|
||||
view.setWindowInsetsAnimationCallback(object : WindowInsetsAnimation.Callback(DISPATCH_MODE_STOP) {
|
||||
override fun onPrepare(animation: WindowInsetsAnimation) {}
|
||||
|
||||
override fun onStart(
|
||||
animation: WindowInsetsAnimation,
|
||||
bounds: WindowInsetsAnimation.Bounds
|
||||
): WindowInsetsAnimation.Bounds {
|
||||
val insets = view.rootWindowInsets?.getInsets(WindowInsets.Type.ime()) ?: Insets.NONE
|
||||
val density = view.resources.displayMetrics.density
|
||||
val targetHeight = insets.bottom.toDouble() / density
|
||||
|
||||
KeyboardBridge.nSetSystemHeight(targetHeight)
|
||||
|
||||
val duration = animation.durationMillis / 1000.0
|
||||
KeyboardBridge.nSetAnim(targetHeight, duration)
|
||||
|
||||
return bounds
|
||||
}
|
||||
|
||||
override fun onProgress(
|
||||
insets: WindowInsets,
|
||||
runningAnimations: MutableList<WindowInsetsAnimation>
|
||||
): WindowInsets {
|
||||
val imeInsets = insets.getInsets(WindowInsets.Type.ime())
|
||||
val density = view.resources.displayMetrics.density
|
||||
val height = imeInsets.bottom.toDouble() / density
|
||||
|
||||
KeyboardBridge.nSetHeight(height)
|
||||
|
||||
return insets
|
||||
}
|
||||
|
||||
override fun onEnd(animation: WindowInsetsAnimation) {
|
||||
val insets = view.rootWindowInsets?.getInsets(WindowInsets.Type.ime()) ?: Insets.NONE
|
||||
val density = view.resources.displayMetrics.density
|
||||
val height = insets.bottom.toDouble() / density
|
||||
|
||||
KeyboardBridge.nSetHeight(height)
|
||||
|
||||
if (height <= 0) {
|
||||
KeyboardBridge.nSetSystemHeight(0.0)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,68 +1,102 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:ux/ux.dart';
|
||||
|
||||
void main() => runApp(MyApp());
|
||||
void main() => runApp(MaterialApp(home: KeyboardExample()));
|
||||
|
||||
class MyApp extends StatefulWidget {
|
||||
class KeyboardExample extends StatefulWidget {
|
||||
@override
|
||||
_MyAppState createState() => _MyAppState();
|
||||
State<KeyboardExample> createState() => _KeyboardExampleState();
|
||||
}
|
||||
|
||||
class _MyAppState extends State<MyApp> {
|
||||
String _platformVersion = 'Unknown';
|
||||
class _KeyboardExampleState extends State<KeyboardExample> {
|
||||
final _keyboard = UxKeyboard.instance;
|
||||
final _focusNode = FocusNode();
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
initPlatformState();
|
||||
}
|
||||
|
||||
// Platform messages are asynchronous, so we initialize in an async method.
|
||||
Future<void> initPlatformState() async {
|
||||
String platformVersion;
|
||||
// Platform messages may fail, so we use a try/catch PlatformException.
|
||||
try {
|
||||
platformVersion = await UX.platformVersion;
|
||||
} on PlatformException {
|
||||
platformVersion = 'Failed to get platform version.';
|
||||
}
|
||||
|
||||
if (!mounted) return;
|
||||
|
||||
setState(() {
|
||||
_platformVersion = platformVersion;
|
||||
});
|
||||
_keyboard.addListener(_onKeyboard);
|
||||
_keyboard.enableInteractiveDismiss(trackingInset: 56);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_keyboard.removeListener(_onKeyboard);
|
||||
_keyboard.disableInteractiveDismiss();
|
||||
_focusNode.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _onKeyboard() => setState(() {});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return MaterialApp(
|
||||
routes: {
|
||||
'/': (context) => Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text('Plugin example app'),
|
||||
),
|
||||
body: Builder(
|
||||
builder: (context) => ListView(
|
||||
padding: EdgeInsets.only(top: 48),
|
||||
children: [
|
||||
ListTile(title: Text('Running on: $_platformVersion\n')),
|
||||
ListTile(
|
||||
title: Text('Show a simple note'),
|
||||
//onTap: () => context.showText('This is a simple note'),
|
||||
final bottomInset = _keyboard.height;
|
||||
final safeArea = MediaQuery.paddingOf(context).bottom;
|
||||
final bottom = bottomInset > 0 ? bottomInset : safeArea;
|
||||
|
||||
return Scaffold(
|
||||
resizeToAvoidBottomInset: false,
|
||||
appBar: AppBar(title: Text('UxKeyboard')),
|
||||
body: Column(
|
||||
children: [
|
||||
Expanded(
|
||||
child: ListView.builder(
|
||||
reverse: true,
|
||||
padding: EdgeInsets.only(bottom: 60 + bottom, top: 16),
|
||||
itemCount: 30,
|
||||
itemBuilder: (context, i) => Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 16, vertical: 4),
|
||||
child: Align(
|
||||
alignment: i % 3 == 0 ? Alignment.centerRight : Alignment.centerLeft,
|
||||
child: Container(
|
||||
padding: EdgeInsets.symmetric(horizontal: 14, vertical: 10),
|
||||
decoration: BoxDecoration(
|
||||
color: i % 3 == 0 ? Colors.blue[100] : Colors.grey[200],
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
),
|
||||
ListTile(
|
||||
title: Text('Show modal note'),
|
||||
//onTap: () => context.showText('This is a modal note', backdropBlur: 6, modal: true),
|
||||
)
|
||||
],
|
||||
child: Text('Message ${30 - i}'),
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
},
|
||||
),
|
||||
),
|
||||
Container(
|
||||
padding: EdgeInsets.only(
|
||||
left: 12,
|
||||
right: 12,
|
||||
top: 8,
|
||||
bottom: 8 + bottom,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
border: Border(top: BorderSide(color: Colors.grey[300]!)),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: TextField(
|
||||
focusNode: _focusNode,
|
||||
decoration: InputDecoration(
|
||||
hintText: 'Type a message...',
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(24),
|
||||
),
|
||||
contentPadding: EdgeInsets.symmetric(horizontal: 16, vertical: 10),
|
||||
isDense: true,
|
||||
),
|
||||
),
|
||||
),
|
||||
SizedBox(width: 8),
|
||||
IconButton(
|
||||
icon: Icon(Icons.send, color: Colors.blue),
|
||||
onPressed: () {},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
#import <Flutter/Flutter.h>
|
||||
|
||||
@interface UxPlugin : NSObject<FlutterPlugin>
|
||||
@end
|
||||
@@ -1,20 +0,0 @@
|
||||
#import "UxPlugin.h"
|
||||
|
||||
@implementation UxPlugin
|
||||
+ (void)registerWithRegistrar:(NSObject<FlutterPluginRegistrar>*)registrar {
|
||||
FlutterMethodChannel* channel = [FlutterMethodChannel
|
||||
methodChannelWithName:@"ux"
|
||||
binaryMessenger:[registrar messenger]];
|
||||
UxPlugin* instance = [[UxPlugin alloc] init];
|
||||
[registrar addMethodCallDelegate:instance channel:channel];
|
||||
}
|
||||
|
||||
- (void)handleMethodCall:(FlutterMethodCall*)call result:(FlutterResult)result {
|
||||
if ([@"getPlatformVersion" isEqualToString:call.method]) {
|
||||
result([@"iOS " stringByAppendingString:[[UIDevice currentDevice] systemVersion]]);
|
||||
} else {
|
||||
result(FlutterMethodNotImplemented);
|
||||
}
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -1,21 +1,13 @@
|
||||
#
|
||||
# To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html
|
||||
#
|
||||
Pod::Spec.new do |s|
|
||||
s.name = 'ux'
|
||||
s.version = '0.0.1'
|
||||
s.summary = 'A new flutter plugin project.'
|
||||
s.description = <<-DESC
|
||||
A new flutter plugin project.
|
||||
DESC
|
||||
s.homepage = 'http://example.com'
|
||||
s.version = '0.2.0'
|
||||
s.summary = 'UX Kit – Flutter plugin with keyboard tracking and interactive dismiss.'
|
||||
s.homepage = 'https://swipelab.co/ux.html'
|
||||
s.license = { :file => '../LICENSE' }
|
||||
s.author = { 'Your Company' => 'email@example.com' }
|
||||
s.author = { 'Swipelab' => 'hello@swipelab.co' }
|
||||
s.source = { :path => '.' }
|
||||
s.source_files = 'Classes/**/*'
|
||||
s.public_header_files = 'Classes/**/*.h'
|
||||
s.source_files = 'Classes/**/*.swift'
|
||||
s.dependency 'Flutter'
|
||||
|
||||
s.ios.deployment_target = '8.0'
|
||||
s.ios.deployment_target = '13.0'
|
||||
s.swift_version = '5.0'
|
||||
end
|
||||
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
13
lib/ux.dart
13
lib/ux.dart
@@ -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';
|
||||
@@ -16,5 +16,9 @@ dev_dependencies:
|
||||
|
||||
flutter:
|
||||
plugin:
|
||||
androidPackage: io.swipelab.ux
|
||||
pluginClass: UxPlugin
|
||||
platforms:
|
||||
ios:
|
||||
pluginClass: KeyboardPlugin
|
||||
android:
|
||||
package: io.swipelab.ux
|
||||
pluginClass: KeyboardPlugin
|
||||
Reference in New Issue
Block a user