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'
|
version '1.0-SNAPSHOT'
|
||||||
|
|
||||||
buildscript {
|
buildscript {
|
||||||
|
ext.kotlin_version = '1.9.22'
|
||||||
repositories {
|
repositories {
|
||||||
google()
|
google()
|
||||||
jcenter()
|
mavenCentral()
|
||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
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 {
|
rootProject.allprojects {
|
||||||
repositories {
|
repositories {
|
||||||
google()
|
google()
|
||||||
jcenter()
|
mavenCentral()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
apply plugin: 'com.android.library'
|
apply plugin: 'com.android.library'
|
||||||
|
apply plugin: 'kotlin-android'
|
||||||
|
|
||||||
android {
|
android {
|
||||||
compileSdkVersion 28
|
namespace 'io.swipelab.ux'
|
||||||
|
compileSdk 34
|
||||||
|
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
minSdkVersion 16
|
minSdk 21
|
||||||
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
|
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 'package:flutter/material.dart';
|
||||||
import 'dart:async';
|
|
||||||
|
|
||||||
import 'package:flutter/services.dart';
|
|
||||||
import 'package:ux/ux.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
|
@override
|
||||||
_MyAppState createState() => _MyAppState();
|
State<KeyboardExample> createState() => _KeyboardExampleState();
|
||||||
}
|
}
|
||||||
|
|
||||||
class _MyAppState extends State<MyApp> {
|
class _KeyboardExampleState extends State<KeyboardExample> {
|
||||||
String _platformVersion = 'Unknown';
|
final _keyboard = UxKeyboard.instance;
|
||||||
|
final _focusNode = FocusNode();
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
initPlatformState();
|
_keyboard.addListener(_onKeyboard);
|
||||||
}
|
_keyboard.enableInteractiveDismiss(trackingInset: 56);
|
||||||
|
|
||||||
// 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;
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_keyboard.removeListener(_onKeyboard);
|
||||||
|
_keyboard.disableInteractiveDismiss();
|
||||||
|
_focusNode.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
void _onKeyboard() => setState(() {});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return MaterialApp(
|
final bottomInset = _keyboard.height;
|
||||||
routes: {
|
final safeArea = MediaQuery.paddingOf(context).bottom;
|
||||||
'/': (context) => Scaffold(
|
final bottom = bottomInset > 0 ? bottomInset : safeArea;
|
||||||
appBar: AppBar(
|
|
||||||
title: const Text('Plugin example app'),
|
return Scaffold(
|
||||||
),
|
resizeToAvoidBottomInset: false,
|
||||||
body: Builder(
|
appBar: AppBar(title: Text('UxKeyboard')),
|
||||||
builder: (context) => ListView(
|
body: Column(
|
||||||
padding: EdgeInsets.only(top: 48),
|
children: [
|
||||||
children: [
|
Expanded(
|
||||||
ListTile(title: Text('Running on: $_platformVersion\n')),
|
child: ListView.builder(
|
||||||
ListTile(
|
reverse: true,
|
||||||
title: Text('Show a simple note'),
|
padding: EdgeInsets.only(bottom: 60 + bottom, top: 16),
|
||||||
//onTap: () => context.showText('This is a simple note'),
|
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(
|
child: Text('Message ${30 - i}'),
|
||||||
title: Text('Show modal note'),
|
),
|
||||||
//onTap: () => context.showText('This is a modal note', backdropBlur: 6, modal: true),
|
|
||||||
)
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
)
|
),
|
||||||
},
|
),
|
||||||
|
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|
|
Pod::Spec.new do |s|
|
||||||
s.name = 'ux'
|
s.name = 'ux'
|
||||||
s.version = '0.0.1'
|
s.version = '0.2.0'
|
||||||
s.summary = 'A new flutter plugin project.'
|
s.summary = 'UX Kit – Flutter plugin with keyboard tracking and interactive dismiss.'
|
||||||
s.description = <<-DESC
|
s.homepage = 'https://swipelab.co/ux.html'
|
||||||
A new flutter plugin project.
|
|
||||||
DESC
|
|
||||||
s.homepage = 'http://example.com'
|
|
||||||
s.license = { :file => '../LICENSE' }
|
s.license = { :file => '../LICENSE' }
|
||||||
s.author = { 'Your Company' => 'email@example.com' }
|
s.author = { 'Swipelab' => 'hello@swipelab.co' }
|
||||||
s.source = { :path => '.' }
|
s.source = { :path => '.' }
|
||||||
s.source_files = 'Classes/**/*'
|
s.source_files = 'Classes/**/*.swift'
|
||||||
s.public_header_files = 'Classes/**/*.h'
|
|
||||||
s.dependency 'Flutter'
|
s.dependency 'Flutter'
|
||||||
|
s.ios.deployment_target = '13.0'
|
||||||
s.ios.deployment_target = '8.0'
|
s.swift_version = '5.0'
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -5,11 +5,10 @@ import 'package:flutter/animation.dart';
|
|||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter/scheduler.dart';
|
import 'package:flutter/scheduler.dart';
|
||||||
|
|
||||||
final bool _isIOS = Platform.isIOS;
|
|
||||||
|
|
||||||
DynamicLibrary? _initLib() {
|
DynamicLibrary? _initLib() {
|
||||||
if (!_isIOS) return null;
|
if (Platform.isIOS) return DynamicLibrary.process();
|
||||||
return DynamicLibrary.process();
|
if (Platform.isAndroid) return DynamicLibrary.open('libux_keyboard.so');
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
final DynamicLibrary? _lib = _initLib();
|
final DynamicLibrary? _lib = _initLib();
|
||||||
@@ -90,7 +89,7 @@ class _SampledCurve extends Curve {
|
|||||||
|
|
||||||
class UxKeyboard with ChangeNotifier {
|
class UxKeyboard with ChangeNotifier {
|
||||||
UxKeyboard._() {
|
UxKeyboard._() {
|
||||||
if (!_isIOS) return;
|
if (_lib == null) return;
|
||||||
SchedulerBinding.instance.addPersistentFrameCallback(_onFrame);
|
SchedulerBinding.instance.addPersistentFrameCallback(_onFrame);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -116,18 +115,22 @@ class UxKeyboard with ChangeNotifier {
|
|||||||
|
|
||||||
final ts = timestamp.inMicroseconds / Duration.microsecondsPerSecond;
|
final ts = timestamp.inMicroseconds / Duration.microsecondsPerSecond;
|
||||||
|
|
||||||
// Detect new keyboard animation from native
|
// On iOS, replay the animation in Dart with a head start (native only
|
||||||
final gen = _uxAnimGen?.call() ?? 0;
|
// gives start/end via notification). On Android, WindowInsetsAnimation
|
||||||
if (gen != _lastAnimGen) {
|
// pushes per-frame values directly — no replay needed.
|
||||||
_lastAnimGen = gen;
|
if (Platform.isIOS) {
|
||||||
final target = _uxAnimTarget?.call() ?? 0;
|
final gen = _uxAnimGen?.call() ?? 0;
|
||||||
final duration = _uxAnimDuration?.call() ?? 0;
|
if (gen != _lastAnimGen) {
|
||||||
if (duration > 0) {
|
_lastAnimGen = gen;
|
||||||
_animFrom = _height;
|
final target = _uxAnimTarget?.call() ?? 0;
|
||||||
_animTo = target;
|
final duration = _uxAnimDuration?.call() ?? 0;
|
||||||
_animDuration = duration - 0.01; // finish 10ms ahead of native
|
if (duration > 0) {
|
||||||
_animStartTime = ts - 0.016; // compensate 2-frame pipeline delay
|
_animFrom = _height;
|
||||||
_isAnimating = true;
|
_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();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (_isAnimating) {
|
// Keep scheduling frames while animating or keyboard is active
|
||||||
|
if (_isAnimating || (!Platform.isIOS && (h > 0 || _height > 0))) {
|
||||||
SchedulerBinding.instance.scheduleFrame();
|
SchedulerBinding.instance.scheduleFrame();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
13
lib/ux.dart
13
lib/ux.dart
@@ -3,15 +3,4 @@ library ux;
|
|||||||
export 'src/bend_box.dart';
|
export 'src/bend_box.dart';
|
||||||
export 'src/json_extension.dart';
|
export 'src/json_extension.dart';
|
||||||
export 'src/bezier.dart';
|
export 'src/bezier.dart';
|
||||||
|
export 'src/keyboard.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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -16,5 +16,9 @@ dev_dependencies:
|
|||||||
|
|
||||||
flutter:
|
flutter:
|
||||||
plugin:
|
plugin:
|
||||||
androidPackage: io.swipelab.ux
|
platforms:
|
||||||
pluginClass: UxPlugin
|
ios:
|
||||||
|
pluginClass: KeyboardPlugin
|
||||||
|
android:
|
||||||
|
package: io.swipelab.ux
|
||||||
|
pluginClass: KeyboardPlugin
|
||||||
Reference in New Issue
Block a user