[Программирование, Разработка мобильных приложений, Dart, Flutter] Flutter. Слушатель клавиатуры без платформенного кода
Автор
Сообщение
news_bot ®
Стаж: 6 лет 9 месяцев
Сообщений: 27286
Всем привет! Меня зовут Дмитрий Андриянов, я Flutter-разработчик в Surf.
В предыдущей статье про RenderObject я рассказал, как немного копнул в слой рендеринга и смог получать расположение и размеры любого виджета — даже динамического. Сегодня расскажу, как был написан слушатель появления/скрытия клавиатуры без нативного кода.
Эта статья будет вам полезна, если вы:
- Пишете на Flutter и хотите узнать, что находится у него под капотом.
- Интересуетесь, как MediaQuery предоставляет данные о UI.
- Хотите реализовывать интересные штуки на Flutter, покопавшись в нём на более глубоком уровне.
Зачем нам понадобилось написать слушатель без натива
В одном Flutter приложении нам нужно было отлавливать появление и скрытие клавиатуры — мы делали это с помощью плагина keyboard_visibility. Но в апреле, после очередного обновления Flutter, он сломался, потому что команда разработки не переехала на новую реализацию нативной интеграции. Прочие популярные решения из pub также завязаны на нативную часть, а повторно наступать на те же грабли не хотелось.
Мы решили разобраться, можно ли слушать клавиатуру силами Flutter. Чтобы не вносить много правок в существующий код, при разработке решения желательно было сохранить похожий на keyboard_visibility интерфейс.
Исследуем MediaQuery и копаем вглубь
Из MediaQuery мы можем получить данные о размерах системных UI-элементов, которые перекрывают дисплей:
// Поле с данными элементов перекрывающих дисплей
MediaQuery.of(context).viewInsets
// Отвечает за данные клавиатуры
MediaQuery.of(context).viewInsets.bottom
Пример:
class KeyboardScreen extends StatefulWidget {
@override
_KeyboardScreenState createState() => _KeyboardScreenState();
}
class _KeyboardScreenState extends State<KeyboardScreen> {
@override
Widget build(BuildContext context) {
return Scaffold(
body: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text('Keyboard: ${MediaQuery.of(context).viewInsets.bottom}'),
const SizedBox(height: 20),
TextField(),
],
),
);
}
}
Первая мысль — использовать MediaQuery.of(context).viewInsets при изменениях значения: 0 — клавиатура скрыта, иначе — видна. Но в момент обращения к MediaQueryData мы получим значение, а не Stream, который нужно слушать.
У этого решения две проблемы:
- Для использования требуется контекст, что накладывает дополнительные ограничения. Например когда у вас есть модель данных связанная с UI, реагирующая на появление клавиатуры.
- viewInsets не дает возможности подписаться на изменения значения.
Нужно что-то более надежное. Мы знаем, что можем получить размер клавиатуры в viewInsets.bottom — и это значение меняется динамически, в зависимости от её появления. Значит, где-то есть механизм, который слушает эти изменения.
Переходим в исходный код метода MediaQueryData of и видим:
static MediaQueryData of(BuildContext context, { bool nullOk = false }) {
assert(context != null);
assert(nullOk != null);
final MediaQuery query = context.dependOnInheritedWidgetOfExactType<MediaQuery>();
if (query != null)
return query.data;
if (nullOk)
return null;
throw FlutterError.fromParts(<DiagnosticsNode>[
ErrorSummary('MediaQuery.of() called with a context that does not contain a MediaQuery.'),
ErrorDescription(
),
context.describeElement('The context used was')
]);
}
final MediaQuery query = context.dependOnInheritedWidgetOfExactType<MediaQuery>();
В этой строке по дереву родителей ищется класс MediaQuery. У полученного виджета берутся и возвращаются данные в виде экземпляра MediaQueryData.
Смотрим в MediaQuery: оказывается, это наследник InheritedWidget, и он создаётся в разных виджетах:
В каждом из этих файлов создаётся свой MediaQuery, который получает данные родительского MediaQuery и модифицирует их на свое усмотрение.
Например, файл dialog:
MediaQuery(
data: MediaQuery.of(context).copyWith(
// iOS does not shrink dialog content below a 1.0 scale factor
textScaleFactor: math.max(textScaleFactor, 1.0),
),
Самый верхний MediaQuery создаётся в файле widgets/app.dart.
Класс _MediaQueryFromWindow:
class _MediaQueryFromWindow extends StatefulWidget {
const _MediaQueryFromWindow({Key key, this.child}) : super(key: key);
final Widget child;
@override
_MediaQueryFromWindowsState createState() => _MediaQueryFromWindowsState();
}
class _MediaQueryFromWindowsState extends State<_MediaQueryFromWindow> with WidgetsBindingObserver {
@override
void initState() {
super.initState();
WidgetsBinding.instance.addObserver(this);
}
// ACCESSIBILITY
@override
void didChangeAccessibilityFeatures() {
setState(() {
// The properties of window have changed. We use them in our build
// function, so we need setState(), but we don't cache anything locally.
});
}
// METRICS
@override
void didChangeMetrics() {
setState(() {
// The properties of window have changed. We use them in our build
// function, so we need setState(), but we don't cache anything locally.
});
}
@override
void didChangeTextScaleFactor() {
setState(() {
// The textScaleFactor property of window has changed. We reference
// window in our build function, so we need to call setState(), but
// we don't need to cache anything locally.
});
}
// RENDERING
@override
void didChangePlatformBrightness() {
setState(() {
// The platformBrightness property of window has changed. We reference
// window in our build function, so we need to call setState(), but
// we don't need to cache anything locally.
});
}
@override
Widget build(BuildContext context) {
MediaQueryData data = MediaQueryData.fromWindow(WidgetsBinding.instance.window);
if (!kReleaseMode) {
data = data.copyWith(platformBrightness: debugBrightnessOverride);
}
return MediaQuery(
data: data,
child: widget.child,
);
}
@override
void dispose() {
WidgetsBinding.instance.removeObserver(this);
super.dispose();
}
}
Что здесь происходит:
1. Класс _MediaQueryFromWindowsState замешивает миксин WidgetsBindingObserver, чтобы использоваться в качестве наблюдателя за изменениями системного UI из Flutter.
2. В initState вызываем WidgetsBinding.instance.addObserver(this); — addObserver принимает на вход экземпляр наблюдателя. В данном случае this, так как текущий класс замешивает WidgetsBindingObserver.
3. WidgetsBindingObserver предоставляет методы, которые вызываются при изменении соответствующих метрик:
didChangeAccessibilityFeatures — вызывается при изменении набора активных на данный момент специальных возможностей в системе.
didChangeMetrics — вызывается при изменении размеров приложения из-за системы. Например, при повороте телефона или влиянии системного UI (появлении клавиатуры).
didChangeTextScaleFactor — вызывается при изменении коэффициента масштабирования текста на платформе.
didChangePlatformBrightness — вызывается при изменении яркости.
4. Самое главное, что объединяет эти методы, — в каждом из них вызывается setState. Это запускает метод build, заново строит объект MediaQueryData
Widget build(BuildContext context) {
MediaQueryData data = MediaQueryData.fromWindow(WidgetsBinding.instance.window);
и передает его вниз по дереву до места вызова MediaQuery.of(context).ИмяПоля:
Подробнее про биндинг можно прочесть в статье моего коллеги Миши Зотьева.
Вывод: мы можем получать изменения системного UI, используя WidgetsBinding и WidgetsBindingObserver.
Реализация слушателя клавиатуры
Начнём реализовывать слушатель клавиатуры на основе этих данных. Для начала создадим класс:
class KeyboardListener with WidgetsBindingObserver {}
Добавим геттер bool — чтобы знать, видна ли клавиатура.
Во время его реализации я столкнулся с одной проблемой. Изначально запоминался текущий размер клавиатуры, чтобы внешний код мог получить его у экземпляра слушателя.
double get currentKeyboardHeight => _currentKeyboardHeight;
double _currentKeyboardHeight = 0;
bool get _isVisibleKeyboard => _currentKeyboardHeight > 0;
Future(() {
final double newKeyboardHeight =
WidgetsBinding.instance.window.viewInsets.bottom;
if (newKeyboardHeight > _currentKeyboardHeight) {
/// Новая высота больше предыдущей — клавиатура открылась
_onShow();
_onChange(true);
} else if (newKeyboardHeight < _currentKeyboardHeight) {
/// Новая высота меньше предыдущей — клавиатура закрылась
_onHide();
_onChange(false);
}
_currentKeyboardHeight = newKeyboardHeight;
});
Мы знаем, что при видимой клавиатуре в viewInsets.bottom значение больше 0, при скрытой — 0.
bool get _isVisibleKeyboard => _currentKeyboardHeight > 0; выполняет проверку: если высота клавиатуры больше нуля, то она видна.
Но на некоторых устройствах с Android 9 при закрытии клавиатуры высота не всегда становилась 0. Открытая клавиатура могла передать значение 400, а закрытая — 150. А в следующий раз она передавала уже 0. Нестабильный и сложно уловимый баг.
Поэтому я решил отказаться от возможности получать размер клавиатуры из экземпляра слушателя и стал проверять:
WidgetsBinding.instance.window.viewInsets.bottom > 0;
Это решило проблему.
Теперь реализуем непосредственно прослушивание изменений для вызова колбэков:
@override
void didChangeMetrics() {
_listener();
}
void _listener() {
if (isVisibleKeyboard) {
_onChange(true);
} else {
_onChange(false);
}
}
void _onChange(bool isOpen) {
/// Тут вызываются внешние слушатели
}
Как и говорилось выше, благодаря didChangeMetrics мы знаем, что изменился системный UI. И проверяя, видна ли клавиатура, вызываем колбеки появления/сокрытия клавиатуры.
Как использовать
class _KeyboardScreenState extends State<KeyboardScreen> {
bool _isShowKeyboard = false;
KeyboardListener _keyboardListener = KeyboardListener();
@override
void initState() {
super.initState();
_keyboardListener.addListener(onChange: (bool isVisible) {
setState(() {
_isShowKeyboard = isVisible;
});
});
}
@override
void dispose() {
_keyboardListener.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text('Keyboard: $_isShowKeyboard'),
const SizedBox(height: 20),
TextField(),
],
),
);
}
}
Полный код
import 'dart:math';
import 'dart:ui';
import 'package:flutter/widgets.dart';
typedef KeyboardChangeListener = Function(bool isVisible);
class KeyboardListener with WidgetsBindingObserver {
static final Random _random = Random();
/// Колбэки, вызывающиеся при появлении и сокрытии клавиатуры
final Map<String, KeyboardChangeListener> _changeListeners = {};
/// Колбэки, вызывающиеся при появлении клавиатуры
final Map<String, VoidCallback> _showListeners = {};
/// Колбэки, вызывающиеся при сокрытии клавиатуры
final Map<String, VoidCallback> _hideListeners = {};
bool get isVisibleKeyboard =>
WidgetsBinding.instance.window.viewInsets.bottom > 0;
KeyboardListener() {
_init();
}
void dispose() {
// Удаляем текущий класс из списка наблюдателей
WidgetsBinding.instance.removeObserver(this);
// Очищаем списки колбэков
_changeListeners.clear();
_showListeners.clear();
_hideListeners.clear();
}
/// При изменениях системного UI вызываем слушателей
@override
void didChangeMetrics() {
_listener();
}
/// Метод добавления слушателей
String addListener({
String id,
KeyboardChangeListener onChange,
VoidCallback onShow,
VoidCallback onHide,
}) {
assert(onChange != null || onShow != null || onHide != null);
/// Для более удобного доступа к слушателям используются идентификаторы
id ??= _generateId();
if (onChange != null) _changeListeners[id] = onChange;
if (onShow != null) _showListeners[id] = onShow;
if (onHide != null) _hideListeners[id] = onHide;
return id;
}
/// Методы удаления слушателей
void removeChangeListener(KeyboardChangeListener listener) {
_removeListener(_changeListeners, listener);
}
void removeShowListener(VoidCallback listener) {
_removeListener(_showListeners, listener);
}
void removeHideListener(VoidCallback listener) {
_removeListener(_hideListeners, listener);
}
void removeAtChangeListener(String id) {
_removeAtListener(_changeListeners, id);
}
void removeAtShowListener(String id) {
_removeAtListener(_changeListeners, id);
}
void removeAtHideListener(String id) {
_removeAtListener(_changeListeners, id);
}
void _removeAtListener(Map<String, Function> listeners, String id) {
listeners.remove(id);
}
void _removeListener(Map<String, Function> listeners, Function listener) {
listeners.removeWhere((key, value) => value == listener);
}
String _generateId() {
return _random.nextDouble().toString();
}
void _init() {
WidgetsBinding.instance.addObserver(this); // Регистрируем наблюдателя
}
void _listener() {
if (isVisibleKeyboard) {
_onShow();
_onChange(true);
} else {
_onHide();
_onChange(false);
}
}
void _onChange(bool isOpen) {
for (KeyboardChangeListener listener in _changeListeners.values) {
listener(isOpen);
}
}
void _onShow() {
for (VoidCallback listener in _showListeners.values) {
listener();
}
}
void _onHide() {
for (VoidCallback listener in _hideListeners.values) {
listener();
}
}
}
Можно было реализовать только _changeListeners или всего один колбэк. Но перед нами стояла задача сохранить API в проекте, который уверенно двигался к релизу. Поэтому использование нового слушателя должно было принести минимум правок.
Итог
Мы в очередной раз увидели, что решить проблемы и реализовать интересные штуки можно без нативной реализации. Достаточно копнуть чуть глубже или просто изучить механизм работы того или иного виджета.
Это решение находится в SurfGear, пакет keyboard_listener.
===========
Источник:
habr.com
===========
Похожие новости:
- [Программирование, Kotlin] Корутины и синхронизация. Лучше не смешивать (перевод)
- [Разработка веб-сайтов, JavaScript, Программирование] JavaScript: что нас ждет в следующем году
- [Программирование, IT-стандарты, Управление сообществом] Безумная система
- [Программирование микроконтроллеров] STM32 и бесконтактный датчик температуры MLX90614. Подключение по I2C
- [Программирование, Учебный процесс в IT] Здравствуй, дорогой я двадцать лет назад
- [Программирование, .NET, ASP, C#] Разбираемся с middleware в ASP.NET Core (перевод)
- [Программирование, Машинное обучение] Ранжирование признаков с помощью Recursive Feature Elimination в Scikit-Learn (перевод)
- [Программирование, Разработка под MacOS, Компьютерное железо, Софт] Новым Mac с Apple M1 пока не хватает нативного софта и сред разработки
- [Программирование, Учебный процесс в IT, Карьера в IT-индустрии, IT-компании] Вы безумны, остановитесь пока не поздно
- [Python, Программирование] Каверзные вопросы по Python
Теги для поиска: #_programmirovanie (Программирование), #_razrabotka_mobilnyh_prilozhenij (Разработка мобильных приложений), #_dart, #_flutter, #_surf, #_dart, #_flutter, #_razrabotka_mobilnyh_prilozhenij (разработка мобильных приложений), #_razrabotka_mobilnogo_prilozhenija (разработка мобильного приложения), #_razrabotka_mobilnogo_po (разработка мобильного по), #_flutter_app_development, #_blog_kompanii_surf (
Блог компании Surf
), #_programmirovanie (
Программирование
), #_razrabotka_mobilnyh_prilozhenij (
Разработка мобильных приложений
), #_dart, #_flutter
Вы не можете начинать темы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Текущее время: 25-Ноя 23:43
Часовой пояс: UTC + 5
Автор | Сообщение |
---|---|
news_bot ®
Стаж: 6 лет 9 месяцев |
|
Всем привет! Меня зовут Дмитрий Андриянов, я Flutter-разработчик в Surf. В предыдущей статье про RenderObject я рассказал, как немного копнул в слой рендеринга и смог получать расположение и размеры любого виджета — даже динамического. Сегодня расскажу, как был написан слушатель появления/скрытия клавиатуры без нативного кода. Эта статья будет вам полезна, если вы:
Зачем нам понадобилось написать слушатель без натива В одном Flutter приложении нам нужно было отлавливать появление и скрытие клавиатуры — мы делали это с помощью плагина keyboard_visibility. Но в апреле, после очередного обновления Flutter, он сломался, потому что команда разработки не переехала на новую реализацию нативной интеграции. Прочие популярные решения из pub также завязаны на нативную часть, а повторно наступать на те же грабли не хотелось. Мы решили разобраться, можно ли слушать клавиатуру силами Flutter. Чтобы не вносить много правок в существующий код, при разработке решения желательно было сохранить похожий на keyboard_visibility интерфейс. Исследуем MediaQuery и копаем вглубь Из MediaQuery мы можем получить данные о размерах системных UI-элементов, которые перекрывают дисплей: // Поле с данными элементов перекрывающих дисплей
MediaQuery.of(context).viewInsets // Отвечает за данные клавиатуры MediaQuery.of(context).viewInsets.bottom Пример: class KeyboardScreen extends StatefulWidget {
@override _KeyboardScreenState createState() => _KeyboardScreenState(); } class _KeyboardScreenState extends State<KeyboardScreen> { @override Widget build(BuildContext context) { return Scaffold( body: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Text('Keyboard: ${MediaQuery.of(context).viewInsets.bottom}'), const SizedBox(height: 20), TextField(), ], ), ); } } Первая мысль — использовать MediaQuery.of(context).viewInsets при изменениях значения: 0 — клавиатура скрыта, иначе — видна. Но в момент обращения к MediaQueryData мы получим значение, а не Stream, который нужно слушать. У этого решения две проблемы:
Нужно что-то более надежное. Мы знаем, что можем получить размер клавиатуры в viewInsets.bottom — и это значение меняется динамически, в зависимости от её появления. Значит, где-то есть механизм, который слушает эти изменения. Переходим в исходный код метода MediaQueryData of и видим: static MediaQueryData of(BuildContext context, { bool nullOk = false }) {
assert(context != null); assert(nullOk != null); final MediaQuery query = context.dependOnInheritedWidgetOfExactType<MediaQuery>(); if (query != null) return query.data; if (nullOk) return null; throw FlutterError.fromParts(<DiagnosticsNode>[ ErrorSummary('MediaQuery.of() called with a context that does not contain a MediaQuery.'), ErrorDescription( ), context.describeElement('The context used was') ]); } final MediaQuery query = context.dependOnInheritedWidgetOfExactType<MediaQuery>();
В этой строке по дереву родителей ищется класс MediaQuery. У полученного виджета берутся и возвращаются данные в виде экземпляра MediaQueryData. Смотрим в MediaQuery: оказывается, это наследник InheritedWidget, и он создаётся в разных виджетах: В каждом из этих файлов создаётся свой MediaQuery, который получает данные родительского MediaQuery и модифицирует их на свое усмотрение. Например, файл dialog: MediaQuery(
data: MediaQuery.of(context).copyWith( // iOS does not shrink dialog content below a 1.0 scale factor textScaleFactor: math.max(textScaleFactor, 1.0), ), Самый верхний MediaQuery создаётся в файле widgets/app.dart. Класс _MediaQueryFromWindow: class _MediaQueryFromWindow extends StatefulWidget {
const _MediaQueryFromWindow({Key key, this.child}) : super(key: key); final Widget child; @override _MediaQueryFromWindowsState createState() => _MediaQueryFromWindowsState(); } class _MediaQueryFromWindowsState extends State<_MediaQueryFromWindow> with WidgetsBindingObserver { @override void initState() { super.initState(); WidgetsBinding.instance.addObserver(this); } // ACCESSIBILITY @override void didChangeAccessibilityFeatures() { setState(() { // The properties of window have changed. We use them in our build // function, so we need setState(), but we don't cache anything locally. }); } // METRICS @override void didChangeMetrics() { setState(() { // The properties of window have changed. We use them in our build // function, so we need setState(), but we don't cache anything locally. }); } @override void didChangeTextScaleFactor() { setState(() { // The textScaleFactor property of window has changed. We reference // window in our build function, so we need to call setState(), but // we don't need to cache anything locally. }); } // RENDERING @override void didChangePlatformBrightness() { setState(() { // The platformBrightness property of window has changed. We reference // window in our build function, so we need to call setState(), but // we don't need to cache anything locally. }); } @override Widget build(BuildContext context) { MediaQueryData data = MediaQueryData.fromWindow(WidgetsBinding.instance.window); if (!kReleaseMode) { data = data.copyWith(platformBrightness: debugBrightnessOverride); } return MediaQuery( data: data, child: widget.child, ); } @override void dispose() { WidgetsBinding.instance.removeObserver(this); super.dispose(); } } Что здесь происходит: 1. Класс _MediaQueryFromWindowsState замешивает миксин WidgetsBindingObserver, чтобы использоваться в качестве наблюдателя за изменениями системного UI из Flutter. 2. В initState вызываем WidgetsBinding.instance.addObserver(this); — addObserver принимает на вход экземпляр наблюдателя. В данном случае this, так как текущий класс замешивает WidgetsBindingObserver. 3. WidgetsBindingObserver предоставляет методы, которые вызываются при изменении соответствующих метрик: didChangeAccessibilityFeatures — вызывается при изменении набора активных на данный момент специальных возможностей в системе. didChangeMetrics — вызывается при изменении размеров приложения из-за системы. Например, при повороте телефона или влиянии системного UI (появлении клавиатуры). didChangeTextScaleFactor — вызывается при изменении коэффициента масштабирования текста на платформе. didChangePlatformBrightness — вызывается при изменении яркости. 4. Самое главное, что объединяет эти методы, — в каждом из них вызывается setState. Это запускает метод build, заново строит объект MediaQueryData Widget build(BuildContext context) {
MediaQueryData data = MediaQueryData.fromWindow(WidgetsBinding.instance.window); и передает его вниз по дереву до места вызова MediaQuery.of(context).ИмяПоля: Подробнее про биндинг можно прочесть в статье моего коллеги Миши Зотьева. Вывод: мы можем получать изменения системного UI, используя WidgetsBinding и WidgetsBindingObserver. Реализация слушателя клавиатуры Начнём реализовывать слушатель клавиатуры на основе этих данных. Для начала создадим класс: class KeyboardListener with WidgetsBindingObserver {}
Добавим геттер bool — чтобы знать, видна ли клавиатура. Во время его реализации я столкнулся с одной проблемой. Изначально запоминался текущий размер клавиатуры, чтобы внешний код мог получить его у экземпляра слушателя. double get currentKeyboardHeight => _currentKeyboardHeight;
double _currentKeyboardHeight = 0; bool get _isVisibleKeyboard => _currentKeyboardHeight > 0; Future(() { final double newKeyboardHeight = WidgetsBinding.instance.window.viewInsets.bottom; if (newKeyboardHeight > _currentKeyboardHeight) { /// Новая высота больше предыдущей — клавиатура открылась _onShow(); _onChange(true); } else if (newKeyboardHeight < _currentKeyboardHeight) { /// Новая высота меньше предыдущей — клавиатура закрылась _onHide(); _onChange(false); } _currentKeyboardHeight = newKeyboardHeight; }); Мы знаем, что при видимой клавиатуре в viewInsets.bottom значение больше 0, при скрытой — 0. bool get _isVisibleKeyboard => _currentKeyboardHeight > 0; выполняет проверку: если высота клавиатуры больше нуля, то она видна. Но на некоторых устройствах с Android 9 при закрытии клавиатуры высота не всегда становилась 0. Открытая клавиатура могла передать значение 400, а закрытая — 150. А в следующий раз она передавала уже 0. Нестабильный и сложно уловимый баг. Поэтому я решил отказаться от возможности получать размер клавиатуры из экземпляра слушателя и стал проверять: WidgetsBinding.instance.window.viewInsets.bottom > 0;
Это решило проблему. Теперь реализуем непосредственно прослушивание изменений для вызова колбэков: @override
void didChangeMetrics() { _listener(); } void _listener() { if (isVisibleKeyboard) { _onChange(true); } else { _onChange(false); } } void _onChange(bool isOpen) { /// Тут вызываются внешние слушатели } Как и говорилось выше, благодаря didChangeMetrics мы знаем, что изменился системный UI. И проверяя, видна ли клавиатура, вызываем колбеки появления/сокрытия клавиатуры. Как использовать class _KeyboardScreenState extends State<KeyboardScreen> {
bool _isShowKeyboard = false; KeyboardListener _keyboardListener = KeyboardListener(); @override void initState() { super.initState(); _keyboardListener.addListener(onChange: (bool isVisible) { setState(() { _isShowKeyboard = isVisible; }); }); } @override void dispose() { _keyboardListener.dispose(); super.dispose(); } @override Widget build(BuildContext context) { return Scaffold( body: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Text('Keyboard: $_isShowKeyboard'), const SizedBox(height: 20), TextField(), ], ), ); } } Полный код import 'dart:math';
import 'dart:ui'; import 'package:flutter/widgets.dart'; typedef KeyboardChangeListener = Function(bool isVisible); class KeyboardListener with WidgetsBindingObserver { static final Random _random = Random(); /// Колбэки, вызывающиеся при появлении и сокрытии клавиатуры final Map<String, KeyboardChangeListener> _changeListeners = {}; /// Колбэки, вызывающиеся при появлении клавиатуры final Map<String, VoidCallback> _showListeners = {}; /// Колбэки, вызывающиеся при сокрытии клавиатуры final Map<String, VoidCallback> _hideListeners = {}; bool get isVisibleKeyboard => WidgetsBinding.instance.window.viewInsets.bottom > 0; KeyboardListener() { _init(); } void dispose() { // Удаляем текущий класс из списка наблюдателей WidgetsBinding.instance.removeObserver(this); // Очищаем списки колбэков _changeListeners.clear(); _showListeners.clear(); _hideListeners.clear(); } /// При изменениях системного UI вызываем слушателей @override void didChangeMetrics() { _listener(); } /// Метод добавления слушателей String addListener({ String id, KeyboardChangeListener onChange, VoidCallback onShow, VoidCallback onHide, }) { assert(onChange != null || onShow != null || onHide != null); /// Для более удобного доступа к слушателям используются идентификаторы id ??= _generateId(); if (onChange != null) _changeListeners[id] = onChange; if (onShow != null) _showListeners[id] = onShow; if (onHide != null) _hideListeners[id] = onHide; return id; } /// Методы удаления слушателей void removeChangeListener(KeyboardChangeListener listener) { _removeListener(_changeListeners, listener); } void removeShowListener(VoidCallback listener) { _removeListener(_showListeners, listener); } void removeHideListener(VoidCallback listener) { _removeListener(_hideListeners, listener); } void removeAtChangeListener(String id) { _removeAtListener(_changeListeners, id); } void removeAtShowListener(String id) { _removeAtListener(_changeListeners, id); } void removeAtHideListener(String id) { _removeAtListener(_changeListeners, id); } void _removeAtListener(Map<String, Function> listeners, String id) { listeners.remove(id); } void _removeListener(Map<String, Function> listeners, Function listener) { listeners.removeWhere((key, value) => value == listener); } String _generateId() { return _random.nextDouble().toString(); } void _init() { WidgetsBinding.instance.addObserver(this); // Регистрируем наблюдателя } void _listener() { if (isVisibleKeyboard) { _onShow(); _onChange(true); } else { _onHide(); _onChange(false); } } void _onChange(bool isOpen) { for (KeyboardChangeListener listener in _changeListeners.values) { listener(isOpen); } } void _onShow() { for (VoidCallback listener in _showListeners.values) { listener(); } } void _onHide() { for (VoidCallback listener in _hideListeners.values) { listener(); } } } Можно было реализовать только _changeListeners или всего один колбэк. Но перед нами стояла задача сохранить API в проекте, который уверенно двигался к релизу. Поэтому использование нового слушателя должно было принести минимум правок. Итог Мы в очередной раз увидели, что решить проблемы и реализовать интересные штуки можно без нативной реализации. Достаточно копнуть чуть глубже или просто изучить механизм работы того или иного виджета. Это решение находится в SurfGear, пакет keyboard_listener. =========== Источник: habr.com =========== Похожие новости:
Блог компании Surf ), #_programmirovanie ( Программирование ), #_razrabotka_mobilnyh_prilozhenij ( Разработка мобильных приложений ), #_dart, #_flutter |
|
Вы не можете начинать темы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Текущее время: 25-Ноя 23:43
Часовой пояс: UTC + 5