keyboard focus + app_info

This commit is contained in:
agra
2026-04-21 16:54:56 +03:00
parent fb40549cce
commit b8d77e0a3a
8 changed files with 449 additions and 1 deletions

View File

@@ -3,6 +3,7 @@ package io.swipelab.ux
import android.app.Activity import android.app.Activity
import android.graphics.Insets import android.graphics.Insets
import android.os.Build import android.os.Build
import android.view.ViewTreeObserver
import android.view.WindowInsets import android.view.WindowInsets
import android.view.WindowInsetsAnimation import android.view.WindowInsetsAnimation
import io.flutter.embedding.engine.plugins.FlutterPlugin import io.flutter.embedding.engine.plugins.FlutterPlugin
@@ -14,6 +15,7 @@ import io.flutter.plugin.common.MethodChannel
class KeyboardPlugin : FlutterPlugin, MethodChannel.MethodCallHandler, ActivityAware { class KeyboardPlugin : FlutterPlugin, MethodChannel.MethodCallHandler, ActivityAware {
private var methodChannel: MethodChannel? = null private var methodChannel: MethodChannel? = null
private var activity: Activity? = null private var activity: Activity? = null
private var windowFocusListener: ViewTreeObserver.OnWindowFocusChangeListener? = null
override fun onAttachedToEngine(binding: FlutterPlugin.FlutterPluginBinding) { override fun onAttachedToEngine(binding: FlutterPlugin.FlutterPluginBinding) {
methodChannel = MethodChannel(binding.binaryMessenger, "ux/keyboard").also { methodChannel = MethodChannel(binding.binaryMessenger, "ux/keyboard").also {
@@ -37,21 +39,47 @@ class KeyboardPlugin : FlutterPlugin, MethodChannel.MethodCallHandler, ActivityA
override fun onAttachedToActivity(binding: ActivityPluginBinding) { override fun onAttachedToActivity(binding: ActivityPluginBinding) {
activity = binding.activity activity = binding.activity
setupInsetsCallback() setupInsetsCallback()
setupWindowFocusListener()
} }
override fun onDetachedFromActivity() { override fun onDetachedFromActivity() {
teardownWindowFocusListener()
activity = null activity = null
} }
override fun onReattachedToActivityForConfigChanges(binding: ActivityPluginBinding) { override fun onReattachedToActivityForConfigChanges(binding: ActivityPluginBinding) {
activity = binding.activity activity = binding.activity
setupInsetsCallback() setupInsetsCallback()
setupWindowFocusListener()
} }
override fun onDetachedFromActivityForConfigChanges() { override fun onDetachedFromActivityForConfigChanges() {
teardownWindowFocusListener()
activity = null activity = null
} }
/// Reports window-focus changes to Dart via the `onWindowFocused` method.
/// Needed because Flutter's `TextField(autofocus: true)` at cold start
/// fires before the Activity window has native focus, so its `TextInput.show`
/// is ignored ("view not served"). Dart listens and re-requests focus once
/// the window is focused, at which point the IME reliably appears.
private fun setupWindowFocusListener() {
val decor = activity?.window?.decorView ?: return
val listener = ViewTreeObserver.OnWindowFocusChangeListener { hasFocus ->
if (hasFocus) {
methodChannel?.invokeMethod("onWindowFocused", null)
}
}
decor.viewTreeObserver.addOnWindowFocusChangeListener(listener)
windowFocusListener = listener
}
private fun teardownWindowFocusListener() {
val listener = windowFocusListener ?: return
activity?.window?.decorView?.viewTreeObserver?.removeOnWindowFocusChangeListener(listener)
windowFocusListener = null
}
private fun setupInsetsCallback() { private fun setupInsetsCallback() {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) return if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) return
val view = activity?.window?.decorView ?: return val view = activity?.window?.decorView ?: return

10
build.yaml Normal file
View File

@@ -0,0 +1,10 @@
builders:
app_info:
import: "package:ux/builder.dart"
builder_factories:
- "appInfoBuilder"
build_extensions:
"$package$":
- "lib/app_info.g.dart"
auto_apply: dependents
build_to: source

35
lib/builder.dart Normal file
View File

@@ -0,0 +1,35 @@
import 'dart:async';
import 'package:build/build.dart';
Builder appInfoBuilder(BuilderOptions options) => _AppInfoBuilder();
class _AppInfoBuilder implements Builder {
@override
Map<String, List<String>> get buildExtensions => const {
r'$package$': ['lib/app_info.g.dart'],
};
@override
Future<void> build(BuildStep buildStep) async {
final pkg = buildStep.inputId.package;
final raw = await buildStep.readAsString(AssetId(pkg, 'pubspec.yaml'));
final match =
RegExp(r'^version:\s*([^\s#]+)', multiLine: true).firstMatch(raw);
final combined = match?.group(1) ?? '0.0.0+0';
final plus = combined.indexOf('+');
final version = plus < 0 ? combined : combined.substring(0, plus);
final buildNumber =
plus < 0 ? 0 : int.tryParse(combined.substring(plus + 1)) ?? 0;
await buildStep.writeAsString(
AssetId(pkg, 'lib/app_info.g.dart'),
'''
// GENERATED by build_runner from pubspec.yaml — do not edit.
import 'package:ux/ux.dart';
const kAppInfo = AppInfo(version: '$version', buildNumber: $buildNumber);
''',
);
}
}

5
lib/src/app_info.dart Normal file
View File

@@ -0,0 +1,5 @@
class AppInfo {
final String version;
final int buildNumber;
const AppInfo({required this.version, required this.buildNumber});
}

View File

@@ -4,6 +4,7 @@ import 'dart:math' as math;
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/scheduler.dart'; import 'package:flutter/scheduler.dart';
import 'package:flutter/services.dart';
DynamicLibrary? _initLib() { DynamicLibrary? _initLib() {
if (Platform.isIOS) return DynamicLibrary.process(); if (Platform.isIOS) return DynamicLibrary.process();
@@ -141,6 +142,9 @@ double _inverseLerp(List<double> samples, double value) {
/// ``` /// ```
class UxKeyboard with ChangeNotifier { class UxKeyboard with ChangeNotifier {
UxKeyboard._() { UxKeyboard._() {
if (Platform.isAndroid) {
_channel.setMethodCallHandler(_onMethodCall);
}
if (_lib == null) return; if (_lib == null) return;
SchedulerBinding.instance.addPersistentFrameCallback(_onFrame); SchedulerBinding.instance.addPersistentFrameCallback(_onFrame);
} }
@@ -148,6 +152,26 @@ class UxKeyboard with ChangeNotifier {
/// The singleton instance. /// The singleton instance.
static final UxKeyboard instance = UxKeyboard._(); static final UxKeyboard instance = UxKeyboard._();
static const _channel = MethodChannel('ux/keyboard');
/// Increments each time the native window gains input focus — the signal
/// Android needs before the IME can be raised. Listen for changes when you
/// want to re-trigger a pending focus request at cold start (e.g. a
/// `TextField(autofocus: true)` whose initial `TextInput.show` was dropped
/// because the window wasn't yet focused).
///
/// The value itself is a monotonically-increasing counter — only useful as a
/// change signal, not as an absolute state.
final windowFocusCounter = ValueNotifier<int>(0);
Future<dynamic> _onMethodCall(MethodCall call) async {
switch (call.method) {
case 'onWindowFocused':
windowFocusCounter.value++;
break;
}
}
double _height = 0; double _height = 0;
/// The current keyboard height in logical pixels. /// The current keyboard height in logical pixels.

View File

@@ -4,6 +4,7 @@
/// [BendBox] for curved layout painting, and bezier curve utilities. /// [BendBox] for curved layout painting, and bezier curve utilities.
library; library;
export 'src/app_info.dart';
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';

View File

@@ -1,6 +1,30 @@
# Generated by pub # Generated by pub
# See https://dart.dev/tools/pub/glossary#lockfile # See https://dart.dev/tools/pub/glossary#lockfile
packages: packages:
_fe_analyzer_shared:
dependency: transitive
description:
name: _fe_analyzer_shared
sha256: da0d9209ca76bde579f2da330aeb9df62b6319c834fa7baae052021b0462401f
url: "https://pub.dev"
source: hosted
version: "85.0.0"
analyzer:
dependency: transitive
description:
name: analyzer
sha256: "974859dc0ff5f37bc4313244b3218c791810d03ab3470a579580279ba971a48d"
url: "https://pub.dev"
source: hosted
version: "7.7.1"
args:
dependency: transitive
description:
name: args
sha256: d0481093c50b1da8910eb0bb301626d4d8eb7284aa739614d2b394ee09e3ea04
url: "https://pub.dev"
source: hosted
version: "2.7.0"
async: async:
dependency: transitive dependency: transitive
description: description:
@@ -17,6 +41,70 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.1.1" version: "2.1.1"
build:
dependency: "direct main"
description:
name: build
sha256: "51dc711996cbf609b90cbe5b335bbce83143875a9d58e4b5c6d3c4f684d3dda7"
url: "https://pub.dev"
source: hosted
version: "2.5.4"
build_config:
dependency: transitive
description:
name: build_config
sha256: "4ae2de3e1e67ea270081eaee972e1bd8f027d459f249e0f1186730784c2e7e33"
url: "https://pub.dev"
source: hosted
version: "1.1.2"
build_daemon:
dependency: transitive
description:
name: build_daemon
sha256: bf05f6e12cfea92d3c09308d7bcdab1906cd8a179b023269eed00c071004b957
url: "https://pub.dev"
source: hosted
version: "4.1.1"
build_resolvers:
dependency: transitive
description:
name: build_resolvers
sha256: ee4257b3f20c0c90e72ed2b57ad637f694ccba48839a821e87db762548c22a62
url: "https://pub.dev"
source: hosted
version: "2.5.4"
build_runner:
dependency: transitive
description:
name: build_runner
sha256: "382a4d649addbfb7ba71a3631df0ec6a45d5ab9b098638144faf27f02778eb53"
url: "https://pub.dev"
source: hosted
version: "2.5.4"
build_runner_core:
dependency: transitive
description:
name: build_runner_core
sha256: "85fbbb1036d576d966332a3f5ce83f2ce66a40bea1a94ad2d5fc29a19a0d3792"
url: "https://pub.dev"
source: hosted
version: "9.1.2"
built_collection:
dependency: transitive
description:
name: built_collection
sha256: "376e3dd27b51ea877c28d525560790aee2e6fbb5f20e2f85d5081027d94e2100"
url: "https://pub.dev"
source: hosted
version: "5.1.1"
built_value:
dependency: transitive
description:
name: built_value
sha256: "0730c18c770d05636a8f945c32a4d7d81cb6e0f0148c8db4ad12e7748f7e49af"
url: "https://pub.dev"
source: hosted
version: "8.12.5"
characters: characters:
dependency: transitive dependency: transitive
description: description:
@@ -25,6 +113,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.4.1" version: "1.4.1"
checked_yaml:
dependency: transitive
description:
name: checked_yaml
sha256: "959525d3162f249993882720d52b7e0c833978df229be20702b33d48d91de70f"
url: "https://pub.dev"
source: hosted
version: "2.0.4"
clock: clock:
dependency: transitive dependency: transitive
description: description:
@@ -33,6 +129,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.1.2" version: "1.1.2"
code_builder:
dependency: transitive
description:
name: code_builder
sha256: "6a6cab2ba4680d6423f34a9b972a4c9a94ebe1b62ecec4e1a1f2cba91fd1319d"
url: "https://pub.dev"
source: hosted
version: "4.11.1"
collection: collection:
dependency: transitive dependency: transitive
description: description:
@@ -41,6 +145,30 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.19.1" version: "1.19.1"
convert:
dependency: transitive
description:
name: convert
sha256: b30acd5944035672bc15c6b7a8b47d773e41e2f17de064350988c5d02adb1c68
url: "https://pub.dev"
source: hosted
version: "3.1.2"
crypto:
dependency: transitive
description:
name: crypto
sha256: c8ea0233063ba03258fbcf2ca4d6dadfefe14f02fab57702265467a19f27fadf
url: "https://pub.dev"
source: hosted
version: "3.0.7"
dart_style:
dependency: transitive
description:
name: dart_style
sha256: "8a0e5fba27e8ee025d2ffb4ee820b4e6e2cf5e4246a6b1a477eb66866947e0bb"
url: "https://pub.dev"
source: hosted
version: "3.1.1"
fake_async: fake_async:
dependency: transitive dependency: transitive
description: description:
@@ -49,6 +177,22 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.3.3" version: "1.3.3"
file:
dependency: transitive
description:
name: file
sha256: a3b4f84adafef897088c160faf7dfffb7696046cb13ae90b508c2cbc95d3b8d4
url: "https://pub.dev"
source: hosted
version: "7.0.1"
fixnum:
dependency: transitive
description:
name: fixnum
sha256: b6dc7065e46c974bc7c5f143080a6764ec7a4be6da1285ececdc37be96de53be
url: "https://pub.dev"
source: hosted
version: "1.1.1"
flutter: flutter:
dependency: "direct main" dependency: "direct main"
description: flutter description: flutter
@@ -67,6 +211,78 @@ packages:
description: flutter description: flutter
source: sdk source: sdk
version: "0.0.0" version: "0.0.0"
frontend_server_client:
dependency: transitive
description:
name: frontend_server_client
sha256: f64a0333a82f30b0cca061bc3d143813a486dc086b574bfb233b7c1372427694
url: "https://pub.dev"
source: hosted
version: "4.0.0"
glob:
dependency: transitive
description:
name: glob
sha256: c3f1ee72c96f8f78935e18aa8cecced9ab132419e8625dc187e1c2408efc20de
url: "https://pub.dev"
source: hosted
version: "2.1.3"
graphs:
dependency: transitive
description:
name: graphs
sha256: "741bbf84165310a68ff28fe9e727332eef1407342fca52759cb21ad8177bb8d0"
url: "https://pub.dev"
source: hosted
version: "2.3.2"
http:
dependency: transitive
description:
name: http
sha256: "87721a4a50b19c7f1d49001e51409bddc46303966ce89a65af4f4e6004896412"
url: "https://pub.dev"
source: hosted
version: "1.6.0"
http_multi_server:
dependency: transitive
description:
name: http_multi_server
sha256: aa6199f908078bb1c5efb8d8638d4ae191aac11b311132c3ef48ce352fb52ef8
url: "https://pub.dev"
source: hosted
version: "3.2.2"
http_parser:
dependency: transitive
description:
name: http_parser
sha256: "178d74305e7866013777bab2c3d8726205dc5a4dd935297175b19a23a2e66571"
url: "https://pub.dev"
source: hosted
version: "4.1.2"
io:
dependency: transitive
description:
name: io
sha256: dfd5a80599cf0165756e3181807ed3e77daf6dd4137caaad72d0b7931597650b
url: "https://pub.dev"
source: hosted
version: "1.0.5"
js:
dependency: transitive
description:
name: js
sha256: "53385261521cc4a0c4658fd0ad07a7d14591cf8fc33abbceae306ddb974888dc"
url: "https://pub.dev"
source: hosted
version: "0.7.2"
json_annotation:
dependency: transitive
description:
name: json_annotation
sha256: cb09e7dac6210041fad964ed7fbee004f14258b4eca4040f72d1234062ace4c8
url: "https://pub.dev"
source: hosted
version: "4.11.0"
leak_tracker: leak_tracker:
dependency: transitive dependency: transitive
description: description:
@@ -99,6 +315,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "6.1.0" version: "6.1.0"
logging:
dependency: transitive
description:
name: logging
sha256: c8245ada5f1717ed44271ed1c26b8ce85ca3228fd2ffdb75468ab01979309d61
url: "https://pub.dev"
source: hosted
version: "1.3.0"
matcher: matcher:
dependency: transitive dependency: transitive
description: description:
@@ -123,6 +347,22 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.17.0" version: "1.17.0"
mime:
dependency: transitive
description:
name: mime
sha256: "41a20518f0cb1256669420fdba0cd90d21561e560ac240f26ef8322e45bb7ed6"
url: "https://pub.dev"
source: hosted
version: "2.0.0"
package_config:
dependency: transitive
description:
name: package_config
sha256: f096c55ebb7deb7e384101542bfba8c52696c1b56fca2eb62827989ef2353bbc
url: "https://pub.dev"
source: hosted
version: "2.2.0"
path: path:
dependency: transitive dependency: transitive
description: description:
@@ -131,6 +371,46 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.9.1" version: "1.9.1"
pool:
dependency: transitive
description:
name: pool
sha256: "978783255c543aa3586a1b3c21f6e9d720eb315376a915872c61ef8b5c20177d"
url: "https://pub.dev"
source: hosted
version: "1.5.2"
pub_semver:
dependency: transitive
description:
name: pub_semver
sha256: "5bfcf68ca79ef689f8990d1160781b4bad40a3bd5e5218ad4076ddb7f4081585"
url: "https://pub.dev"
source: hosted
version: "2.2.0"
pubspec_parse:
dependency: transitive
description:
name: pubspec_parse
sha256: "0560ba233314abbed0a48a2956f7f022cce7c3e1e73df540277da7544cad4082"
url: "https://pub.dev"
source: hosted
version: "1.5.0"
shelf:
dependency: transitive
description:
name: shelf
sha256: e7dd780a7ffb623c57850b33f43309312fc863fb6aa3d276a754bb299839ef12
url: "https://pub.dev"
source: hosted
version: "1.4.2"
shelf_web_socket:
dependency: transitive
description:
name: shelf_web_socket
sha256: "3632775c8e90d6c9712f883e633716432a27758216dfb61bd86a8321c0580925"
url: "https://pub.dev"
source: hosted
version: "3.0.0"
sky_engine: sky_engine:
dependency: transitive dependency: transitive
description: flutter description: flutter
@@ -160,6 +440,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.1.4" version: "2.1.4"
stream_transform:
dependency: transitive
description:
name: stream_transform
sha256: ad47125e588cfd37a9a7f86c7d6356dde8dfe89d071d293f80ca9e9273a33871
url: "https://pub.dev"
source: hosted
version: "2.1.1"
string_scanner: string_scanner:
dependency: transitive dependency: transitive
description: description:
@@ -184,6 +472,22 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.7.10" version: "0.7.10"
timing:
dependency: transitive
description:
name: timing
sha256: "62ee18aca144e4a9f29d212f5a4c6a053be252b895ab14b5821996cff4ed90fe"
url: "https://pub.dev"
source: hosted
version: "1.0.2"
typed_data:
dependency: transitive
description:
name: typed_data
sha256: f9049c039ebfeb4cf7a7104a675823cd72dba8297f264b6637062516699fa006
url: "https://pub.dev"
source: hosted
version: "1.4.0"
vector_math: vector_math:
dependency: transitive dependency: transitive
description: description:
@@ -200,6 +504,46 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "14.2.5" version: "14.2.5"
watcher:
dependency: transitive
description:
name: watcher
sha256: "1398c9f081a753f9226febe8900fce8f7d0a67163334e1c94a2438339d79d635"
url: "https://pub.dev"
source: hosted
version: "1.2.1"
web:
dependency: transitive
description:
name: web
sha256: "868d88a33d8a87b18ffc05f9f030ba328ffefba92d6c127917a2ba740f9cfe4a"
url: "https://pub.dev"
source: hosted
version: "1.1.1"
web_socket:
dependency: transitive
description:
name: web_socket
sha256: "34d64019aa8e36bf9842ac014bb5d2f5586ca73df5e4d9bf5c936975cae6982c"
url: "https://pub.dev"
source: hosted
version: "1.0.1"
web_socket_channel:
dependency: transitive
description:
name: web_socket_channel
sha256: d645757fb0f4773d602444000a8131ff5d48c9e47adfe9772652dd1a4f2d45c8
url: "https://pub.dev"
source: hosted
version: "3.0.3"
yaml:
dependency: transitive
description:
name: yaml
sha256: b9da305ac7c39faa3f030eccd175340f968459dae4af175130b3fc47e40d76ce
url: "https://pub.dev"
source: hosted
version: "3.1.3"
sdks: sdks:
dart: ">=3.9.0-0 <4.0.0" dart: ">=3.9.0 <4.0.0"
flutter: ">=3.18.0-18.0.pre.54" flutter: ">=3.18.0-18.0.pre.54"

View File

@@ -19,6 +19,7 @@ environment:
dependencies: dependencies:
flutter: flutter:
sdk: flutter sdk: flutter
build: ^2.4.0
dev_dependencies: dev_dependencies:
flutter_lints: ^6.0.0 flutter_lints: ^6.0.0