[Dart, Flutter, Программирование, Разработка мобильных приложений] Flutter. RenderObject — замеряй и властвуй
Автор
Сообщение
news_bot ®
Стаж: 6 лет 9 месяцев
Сообщений: 27286
Всем привет, меня зовут Дмитрий Андриянов. Я Flutter-разработчик в Surf. Чтобы построить эффективный и производительный UI достаточно основной библиотеки Flutter. Но бывают случаи, когда нужно реализовывать специфичные кейсы и тогда придётся копать в глубь.
Вводная
Имеется экран со множеством текстовых полей. Их может быть как 5, так и 30. Между ними могут находиться различные виджеты.
Задача
- Поместить над клавиатурой блок с кнопкой «Далее» для переключения на следующее поле.
- При смене фокуса подскролливать поле к блоку с кнопкой «Далее».
Проблема
Блок с кнопкой перекрывает текстовое поле. Нужно реализовать автоматический скролл на размер перекрываемого пространства текстового поля.
Подготовка к решению
1.Возьмём экран из 20 полей.
Код:
List<String> list = List.generate(20, (index) => index.toString());
@override
Widget build(BuildContext context) {
return Scaffold(
body: SingleChildScrollView(
child: SafeArea(
child: Padding(
padding: const EdgeInsets.all(20),
child: Column(
children: <Widget>[
for (String value in list)
TextField(
decoration: InputDecoration(labelText: value),
)
],
),
),
),
),
);
}
При фокусе в текстовом поле видим следующую картину:
Поле прекрасно видно и всё в порядке.
2. Добавим блок с кнопкой.
Для отображения блока используется Overlay. Это позволяет показывать плашку независимо от виджетов на экране и не использовать обёртки в виде Stack. При этом у нас нет прямого взаимодействия между полями и блоком «Далее».
Хорошая статья про Overlay.
Если кратко: Overlay позволяет накладывать виджеты поверх других виджетов, через стек наложения. OverlayEntry позволяют управлять соответствующим ему Overlay.
Код:
bool _isShow = false;
OverlayEntry _overlayEntry;
KeyboardListener _keyboardListener;
@override
void initState() {
SchedulerBinding.instance.addPostFrameCallback((_) {
_overlayEntry = OverlayEntry(builder: _buildOverlay);
Overlay.of(context).insert(_overlayEntry);
_keyboardListener = KeyboardListener()
..addListener(onChange: _keyboardHandle);
});
super.initState();
}
@override
void dispose() {
_keyboardListener.dispose();
_overlayEntry.remove();
super.dispose();
}
Widget _buildOverlay(BuildContext context) {
return Stack(
children: <Widget>[
Positioned(
bottom: MediaQuery.of(context).viewInsets.bottom,
left: 0,
right: 0,
child: AnimatedOpacity(
duration: const Duration(milliseconds: 200),
opacity: _isShow ? 1.0 : 0.0,
child: NextBlock(
onPressed: () {},
isShow: _isShow,
),
),
),
],
);
void _keyboardHandle(bool isVisible) {
_isShow = isVisible;
_overlayEntry?.markNeedsBuild();
}
3. Как и ожидалось, блок перекрывает поле.
Идеи по решению
1. Брать текущую позицию прокрутки экрана из ScrollController и скроллить до поля.
Размеры поля неизвестны, особенно если оно многострочное, то скролл к нему даст неточный результат. Решение будет не идеальным и не гибким.
2. Складывать размеры виджетов вне списка и учитывать прокрутку.
Если задать виджетам фиксированную высоту, тогда, зная положение прокрутки и размеры виджетов, будет известно, что сейчас в зоне видимости и на сколько нужно скроллить, чтобы показать определённый виджет.
Минусы:
- Придётся учитывать все виджеты вне списка и задавать им фиксированные размеры, которые будут использоваться в расчетах, что не всегда соответствует требуемому дизайну и поведению интерфейса.
- Правки UI приведут к правкам в расчётах.
3. Брать позицию виджетов относительно экрана поля и блока «Далее» и доскралливать на разницу.
Минус — из коробки такой возможности нет.
4. Использовать слой рендера.
Исходя из статьи, Flutter знает, как расположить своих потомков в дереве, а значит эту информацию можно вытащить. За рендер отвечает RenderObject, к нему то и направимся. RenderBox имеет поле size с шириной и высотой виджета. Они рассчитываются при рендере для виджетов: будь то списки, контейнеры, текстовые поля (даже многострочные) и т.д.
Получить RenderBox можно через
context context.findRenderObject() as RenderBox
Для получения контекста поля можно использовать GlobalKey.
Минус:
GlobalKey не самая легкая штука. И применять её лучше как можно реже.
«Виджеты с глобальными ключами перерисовывают свои поддеревья, когда они перемещаются из одного места в дереве в другое. Чтобы перерисовать своё поддерево, виджет должен прибыть в своё новое местоположение в дереве в том же кадре анимации, в котором он был удалён из старого места.
Глобальные ключи относительно дороги в плане производительности. Если вам не нужны какие-либо функции, перечисленные выше, рассмотрите возможность использования Key, ValueKey, ObjectKey или UniqueKey.
Вы не можете одновременно включить два виджета в дерево с одним и тем же глобальным ключом. При попытке сделать это будет ошибка во время исполнения». Источник.
На самом деле, если держать на экране 20 GlobalKey, ничего страшного не случится, но раз рекомендуется использовать его только в случаях необходимости, то попробуем поискать другой путь.
Решение без GlobalKey
Будем использовать слой рендера. Первым делом нужно проверить — можем ли мы вытащить что-то из RenderBox и будут ли это те данные, что нам нужны.
Код для проверки гипотезы:
FocusNode get focus => widget.focus;
@override
void initState() {
super.initState();
Future.delayed(const Duration(seconds: 1)).then((_) {
// (1)
RenderBox rb = (focus.context.findRenderObject() as RenderBox);
//(3)
RenderBox parent = _getParent(rb);
//(4)
print('parent = ${parent.size.height}');
});
}
RenderBox _getParent(RenderBox rb) {
return rb.parent is RenderWrapper ? rb.parent : _getParent(rb.parent);
}
Widget build(BuildContext context) {
return Wrapper(
child: Container(
color: Colors.red,
width: double.infinity,
height: 100,
child: Center(
child: TextField(
focusNode: focus,
),
),
),
);
}
//(2)
class Wrapper extends SingleChildRenderObjectWidget {
const Wrapper({
Key key,
Widget child,
}) : super(key: key, child: child);
@override
RenderWrapper createRenderObject(BuildContext context) {
return RenderWrapper();
}
}
class RenderWrapper extends RenderProxyBox {
RenderWrapper({
RenderBox child,
}) : super(child);
}
(1) Так как нужна прокрутка до поля, надо получить его контекст (например, через FocusNode), найти RenderBox и взять size. Но это size текстового поля и если нам нужны также родительские виджеты (например, Padding), надо взять родительский RenderBox через поле parent.
(2) Наследуем наш класс RenderWrapper от SingleChildRenderObjectWidget и создаём RenderProxyBox для него. RenderProxyBox имитирует все свойства дочернего элемента, отображая его при рендере дерева виджетов.
Flutter сам часто использует наследников SingleChildRenderObjectWidget:
Align, AnimatedSize, SizedBox, Opacity, Padding.
(3) Рекурсивно проходим родителей по дереву, пока не встретим RenderWrapper.
(4) Берём parent.size.height — это выдаст правильную высоту. Это правильный путь.
Так оставлять, конечно же, нельзя.
Но у рекурсивного подхода тоже есть минусы:
- Рекурсивный обход дерева не гарантирует, что мы не нарвёмся на предка к которому не готовы. Он может не подойти по типу и всё. Как-то на тестах я нарвался на RenderView и всё упало. Можно, конечно, игнорировать неподходящего предка, но хочется более надежного подхода.
- Это неуправляемое и всё еще не гибкое решение.
Использование RenderObject
Данный подход вылился пакет render_metrics и уже давно используется на одном из наших приложений.
Логика работы:
1. Оборачиваем интересующий виджет (потомок класса Widget) в RenderMetricsObject. Вложенность и целевой виджет не имеют значения.
RenderMetricsObject(
child: ...,
)
2. После первого фрейма нам будут доступны его метрики. Если размер или позиция виджета относительно экрана (абсолютное или в прокрутке), то при повторном запросе метрик уже будут новые данные.
3. Использовать менеджер RenderManager не обязательно, но при его использовании нужно передавать id для виджета.
RenderMetricsObject(
id: _text1Id,
manager: renderManager,
child: ...
4. Можно использовать колбэки:
- onMount — создание RenderObject. В аргументы получает переданный id (или null, если не был передан) и соответствующий экземпляр RenderMetricsBox.
- onUnMount — удаление из дерева.
В параметрах функция получает id, переданный в RenderMetricsObject. Эти функции полезны тогда, когда не нужен менеджер и/или нужно знать когда был создан и удалился RenderObject из дерева.
RenderMetricsObject(
id: _textBlockId,
onMount: (id, box) {},
onUnMount: (box) {},
child...
)
5. Получение метрик. Класс RenderMetricsBox реализует геттер data, в котором берёт свои размеры через localToGlobal. localToGlobal преобразует точку из локальной системы координат для этого RenderBox в глобальную систему координат относительно экрана в логических пикселях.
A — ширина width виджета, преобразуется в самую правую точку координат относительно экрана.
B — высота height преобразуется в самую нижнюю точку координат относительно экрана.
class RenderMetricsBox extends RenderProxyBox {
RenderData get data {
Size size = this.size;
double width = size.width;
double height = size.height;
Offset globalOffset = localToGlobal(Offset(width, height));
double dy = globalOffset.dy;
double dx = globalOffset.dx;
return RenderData(
yTop: dy - height,
yBottom: dy,
yCenter: dy - height / 2,
xLeft: dx - width,
xRight: dx,
xCenter: dx - width / 2,
width: width,
height: height,
);
}
RenderMetricsBox({
RenderBox child,
}) : super(child);
}
6. RenderData — просто класс с данными, предоставляющий отдельные x и y значения в виде double и точки координат в виде CoordsMetrics.
7. ComparisonDiff — при вычитании двух RenderData возвращается экземпляр ComparisonDiff с разницей между ними. Также он предоставляет геттер (diffTopToBottom) для разницы позиций между нижним краем первого виджета и верхним второго и наоборот (diffBottomToTop). diffLeftToRight и diffRightToLeft соответственно.
8. RenderParametersManager — наследник RenderManager. Для получения метрик виджета и разницы между ними.
Код:
class RenderMetricsScreen extends StatefulWidget {
@override
State<StatefulWidget> createState() => _RenderMetricsScreenState();
}
class _RenderMetricsScreenState extends State<RenderMetricsScreen> {
final List<String> list = List.generate(20, (index) => index.toString());
/// Менеджер из библиотеки render_metrics
/// для замеров позиционирования виджетов на экране
final _renderParametersManager = RenderParametersManager();
final ScrollController scrollController = ScrollController();
/// id блока с кнопкой "Далее"
final doneBlockId = 'doneBlockId';
final List<FocusNode> focusNodes = [];
bool _isShow = false;
OverlayEntry _overlayEntry;
KeyboardListener _keyboardListener;
/// Последний полученный FocusNode, зарегистрированный при смене фокуса
FocusNode lastFocusedNode;
@override
void initState() {
SchedulerBinding.instance.addPostFrameCallback((_) {
_overlayEntry = OverlayEntry(builder: _buildOverlay);
Overlay.of(context).insert(_overlayEntry);
_keyboardListener = KeyboardListener()
..addListener(onChange: _keyboardHandle);
});
FocusNode node;
for(int i = 0; i < list.length; i++) {
node = FocusNode(debugLabel: i.toString());
focusNodes.add(node);
node.addListener(_onChangeFocus(node));
}
super.initState();
}
@override
void dispose() {
_keyboardListener.dispose();
_overlayEntry.remove();
focusNodes.forEach((node) => node.dispose());
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: SingleChildScrollView(
controller: scrollController,
child: SafeArea(
child: Padding(
padding: const EdgeInsets.all(20),
child: Column(
children: <Widget>[
for (int i = 0; i < list.length; i++)
RenderMetricsObject(
id: focusNodes[i],
manager: _renderParametersManager,
child: TextField(
focusNode: focusNodes[i],
decoration: InputDecoration(labelText: list[i]),
),
),
],
),
),
),
),
);
}
Widget _buildOverlay(BuildContext context) {
return Stack(
children: <Widget>[
Positioned(
bottom: MediaQuery.of(context).viewInsets.bottom,
left: 0,
right: 0,
child: RenderMetricsObject(
id: doneBlockId,
manager: _renderParametersManager,
child: AnimatedOpacity(
duration: const Duration(milliseconds: 200),
opacity: _isShow ? 1.0 : 0.0,
child: NextBlock(
onPressed: () {},
isShow: _isShow,
),
),
),
),
],
);
}
VoidCallback _onChangeFocus(FocusNode node) => () {
if (!node.hasFocus) return;
lastFocusedNode = node;
_doScrollIfNeeded();
};
/// Метод, срабатывающий при возникновении необходимости расчёта скролла
/// экрана.
void _doScrollIfNeeded() async {
if (lastFocusedNode == null) return;
double scrollOffset;
try {
/// Если нет нужного id, то data рендера вызовется на null
scrollOffset = await _calculateScrollOffset();
} catch (e) {
return;
}
_doScroll(scrollOffset);
}
/// Инициирование подскролла экрана
void _doScroll(double scrollOffset) {
double offset = scrollController.offset + scrollOffset;
if (offset < 0) offset = 0;
scrollController.position.animateTo(
offset,
duration: const Duration(milliseconds: 200),
curve: Curves.linear,
);
}
/// Расчёт необходимого расстояния скролла экрана.
///
/// Скролл произойдет при отклонении текстового поля от плашки "Готово" в обе
/// стороны (вверх/вниз).
Future<double> _calculateScrollOffset() async {
await Future.delayed(const Duration(milliseconds: 300));
ComparisonDiff diff = _renderParametersManager.getDiffById(
lastFocusedNode,
doneBlockId,
);
lastFocusedNode = null;
if (diff == null || diff.firstData == null || diff.secondData == null) {
return 0.0;
}
return diff.diffBottomToTop;
}
void _keyboardHandle(bool isVisible) {
_isShow = isVisible;
_overlayEntry?.markNeedsBuild();
}
}
Результат с использованием render_metrics
Итог
Копнув глубже уровня виджетов, с помощью небольших манипуляций со слоем рендера получили полезную функциональность, которая позволяет писать более сложные UI и логику. Иногда нужно знать размеры динамических виджетов, их позицию или сравнить перекрывающие друг на друга виджеты. И данная библиотека предоставляет все эти возможности для более быстрого и эффективного решения задач. В статье я постарался объяснить механизм работы, привёл пример проблемы и решения. Надеюсь на пользу библиотеки, статьи и на вашу обратную связь.
===========
Источник:
habr.com
===========
Похожие новости:
- [Информационная безопасность, Программирование, Разработка под Linux] Побег из привилегированных Docker-контейнеров (перевод)
- [FPGA, Высокая производительность, Параллельное программирование, Программирование микроконтроллеров, Электроника для начинающих] Как начать путь к работе по проектированию электроники FPGA космического корабля Blue Origin
- [Java, Программирование, Профессиональная литература] Фреймворк Quarkus: как в нем реализуется чистая архитектура (перевод)
- [Java, PostgreSQL, Программирование, Тестирование IT-систем] Работа с базами данных глазами разработчика
- [DIY или Сделай сам, Программирование микроконтроллеров, Производство и разработка электроники, Разработка игр, Схемотехника] Doom Boy ESP32. Вторая итерация
- [Java, Microsoft Azure, Программирование] Использование Azure Service Bus из Java
- [Java, MongoDB, Программирование] The Testcontainers’ MongoDB Module and Spring Data MongoDB in Action
- [Системное программирование, Программирование микроконтроллеров, Компьютерное железо] Моделируем поведение Quartus-проекта на Verilog в среде ModelSim
- [Программирование, Сетевые технологии, IT-стандарты] Закон дырявых абстракций (перевод)
- [Разработка веб-сайтов, Python, Программирование, Функциональное программирование] Какая асинхронность должна была бы быть в Python
Теги для поиска: #_dart, #_flutter, #_programmirovanie (Программирование), #_razrabotka_mobilnyh_prilozhenij (Разработка мобильных приложений), #_surf, #_flutter, #_razrabotka_mobilnyh_prilozhenij (разработка мобильных приложений), #_blog_kompanii_surf (
Блог компании Surf
), #_dart, #_flutter, #_programmirovanie (
Программирование
), #_razrabotka_mobilnyh_prilozhenij (
Разработка мобильных приложений
)
Вы не можете начинать темы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Текущее время: 22-Ноя 19:56
Часовой пояс: UTC + 5
Автор | Сообщение |
---|---|
news_bot ®
Стаж: 6 лет 9 месяцев |
|
Всем привет, меня зовут Дмитрий Андриянов. Я Flutter-разработчик в Surf. Чтобы построить эффективный и производительный UI достаточно основной библиотеки Flutter. Но бывают случаи, когда нужно реализовывать специфичные кейсы и тогда придётся копать в глубь. Вводная Имеется экран со множеством текстовых полей. Их может быть как 5, так и 30. Между ними могут находиться различные виджеты. Задача
Проблема Блок с кнопкой перекрывает текстовое поле. Нужно реализовать автоматический скролл на размер перекрываемого пространства текстового поля. Подготовка к решению 1.Возьмём экран из 20 полей. Код: List<String> list = List.generate(20, (index) => index.toString());
@override Widget build(BuildContext context) { return Scaffold( body: SingleChildScrollView( child: SafeArea( child: Padding( padding: const EdgeInsets.all(20), child: Column( children: <Widget>[ for (String value in list) TextField( decoration: InputDecoration(labelText: value), ) ], ), ), ), ), ); } При фокусе в текстовом поле видим следующую картину: Поле прекрасно видно и всё в порядке. 2. Добавим блок с кнопкой. Для отображения блока используется Overlay. Это позволяет показывать плашку независимо от виджетов на экране и не использовать обёртки в виде Stack. При этом у нас нет прямого взаимодействия между полями и блоком «Далее». Хорошая статья про Overlay. Если кратко: Overlay позволяет накладывать виджеты поверх других виджетов, через стек наложения. OverlayEntry позволяют управлять соответствующим ему Overlay. Код: bool _isShow = false;
OverlayEntry _overlayEntry; KeyboardListener _keyboardListener; @override void initState() { SchedulerBinding.instance.addPostFrameCallback((_) { _overlayEntry = OverlayEntry(builder: _buildOverlay); Overlay.of(context).insert(_overlayEntry); _keyboardListener = KeyboardListener() ..addListener(onChange: _keyboardHandle); }); super.initState(); } @override void dispose() { _keyboardListener.dispose(); _overlayEntry.remove(); super.dispose(); } Widget _buildOverlay(BuildContext context) { return Stack( children: <Widget>[ Positioned( bottom: MediaQuery.of(context).viewInsets.bottom, left: 0, right: 0, child: AnimatedOpacity( duration: const Duration(milliseconds: 200), opacity: _isShow ? 1.0 : 0.0, child: NextBlock( onPressed: () {}, isShow: _isShow, ), ), ), ], ); void _keyboardHandle(bool isVisible) { _isShow = isVisible; _overlayEntry?.markNeedsBuild(); } 3. Как и ожидалось, блок перекрывает поле. Идеи по решению 1. Брать текущую позицию прокрутки экрана из ScrollController и скроллить до поля. Размеры поля неизвестны, особенно если оно многострочное, то скролл к нему даст неточный результат. Решение будет не идеальным и не гибким. 2. Складывать размеры виджетов вне списка и учитывать прокрутку. Если задать виджетам фиксированную высоту, тогда, зная положение прокрутки и размеры виджетов, будет известно, что сейчас в зоне видимости и на сколько нужно скроллить, чтобы показать определённый виджет. Минусы:
3. Брать позицию виджетов относительно экрана поля и блока «Далее» и доскралливать на разницу. Минус — из коробки такой возможности нет. 4. Использовать слой рендера. Исходя из статьи, Flutter знает, как расположить своих потомков в дереве, а значит эту информацию можно вытащить. За рендер отвечает RenderObject, к нему то и направимся. RenderBox имеет поле size с шириной и высотой виджета. Они рассчитываются при рендере для виджетов: будь то списки, контейнеры, текстовые поля (даже многострочные) и т.д. Получить RenderBox можно через context context.findRenderObject() as RenderBox
Для получения контекста поля можно использовать GlobalKey. Минус: GlobalKey не самая легкая штука. И применять её лучше как можно реже. «Виджеты с глобальными ключами перерисовывают свои поддеревья, когда они перемещаются из одного места в дереве в другое. Чтобы перерисовать своё поддерево, виджет должен прибыть в своё новое местоположение в дереве в том же кадре анимации, в котором он был удалён из старого места. Глобальные ключи относительно дороги в плане производительности. Если вам не нужны какие-либо функции, перечисленные выше, рассмотрите возможность использования Key, ValueKey, ObjectKey или UniqueKey. Вы не можете одновременно включить два виджета в дерево с одним и тем же глобальным ключом. При попытке сделать это будет ошибка во время исполнения». Источник. На самом деле, если держать на экране 20 GlobalKey, ничего страшного не случится, но раз рекомендуется использовать его только в случаях необходимости, то попробуем поискать другой путь. Решение без GlobalKey Будем использовать слой рендера. Первым делом нужно проверить — можем ли мы вытащить что-то из RenderBox и будут ли это те данные, что нам нужны. Код для проверки гипотезы: FocusNode get focus => widget.focus;
@override void initState() { super.initState(); Future.delayed(const Duration(seconds: 1)).then((_) { // (1) RenderBox rb = (focus.context.findRenderObject() as RenderBox); //(3) RenderBox parent = _getParent(rb); //(4) print('parent = ${parent.size.height}'); }); } RenderBox _getParent(RenderBox rb) { return rb.parent is RenderWrapper ? rb.parent : _getParent(rb.parent); } Widget build(BuildContext context) { return Wrapper( child: Container( color: Colors.red, width: double.infinity, height: 100, child: Center( child: TextField( focusNode: focus, ), ), ), ); } //(2) class Wrapper extends SingleChildRenderObjectWidget { const Wrapper({ Key key, Widget child, }) : super(key: key, child: child); @override RenderWrapper createRenderObject(BuildContext context) { return RenderWrapper(); } } class RenderWrapper extends RenderProxyBox { RenderWrapper({ RenderBox child, }) : super(child); } (1) Так как нужна прокрутка до поля, надо получить его контекст (например, через FocusNode), найти RenderBox и взять size. Но это size текстового поля и если нам нужны также родительские виджеты (например, Padding), надо взять родительский RenderBox через поле parent. (2) Наследуем наш класс RenderWrapper от SingleChildRenderObjectWidget и создаём RenderProxyBox для него. RenderProxyBox имитирует все свойства дочернего элемента, отображая его при рендере дерева виджетов. Flutter сам часто использует наследников SingleChildRenderObjectWidget: Align, AnimatedSize, SizedBox, Opacity, Padding. (3) Рекурсивно проходим родителей по дереву, пока не встретим RenderWrapper. (4) Берём parent.size.height — это выдаст правильную высоту. Это правильный путь. Так оставлять, конечно же, нельзя. Но у рекурсивного подхода тоже есть минусы:
Использование RenderObject Данный подход вылился пакет render_metrics и уже давно используется на одном из наших приложений. Логика работы: 1. Оборачиваем интересующий виджет (потомок класса Widget) в RenderMetricsObject. Вложенность и целевой виджет не имеют значения. RenderMetricsObject(
child: ..., ) 2. После первого фрейма нам будут доступны его метрики. Если размер или позиция виджета относительно экрана (абсолютное или в прокрутке), то при повторном запросе метрик уже будут новые данные. 3. Использовать менеджер RenderManager не обязательно, но при его использовании нужно передавать id для виджета. RenderMetricsObject(
id: _text1Id, manager: renderManager, child: ... 4. Можно использовать колбэки:
В параметрах функция получает id, переданный в RenderMetricsObject. Эти функции полезны тогда, когда не нужен менеджер и/или нужно знать когда был создан и удалился RenderObject из дерева. RenderMetricsObject(
id: _textBlockId, onMount: (id, box) {}, onUnMount: (box) {}, child... ) 5. Получение метрик. Класс RenderMetricsBox реализует геттер data, в котором берёт свои размеры через localToGlobal. localToGlobal преобразует точку из локальной системы координат для этого RenderBox в глобальную систему координат относительно экрана в логических пикселях. A — ширина width виджета, преобразуется в самую правую точку координат относительно экрана. B — высота height преобразуется в самую нижнюю точку координат относительно экрана. class RenderMetricsBox extends RenderProxyBox {
RenderData get data { Size size = this.size; double width = size.width; double height = size.height; Offset globalOffset = localToGlobal(Offset(width, height)); double dy = globalOffset.dy; double dx = globalOffset.dx; return RenderData( yTop: dy - height, yBottom: dy, yCenter: dy - height / 2, xLeft: dx - width, xRight: dx, xCenter: dx - width / 2, width: width, height: height, ); } RenderMetricsBox({ RenderBox child, }) : super(child); } 6. RenderData — просто класс с данными, предоставляющий отдельные x и y значения в виде double и точки координат в виде CoordsMetrics. 7. ComparisonDiff — при вычитании двух RenderData возвращается экземпляр ComparisonDiff с разницей между ними. Также он предоставляет геттер (diffTopToBottom) для разницы позиций между нижним краем первого виджета и верхним второго и наоборот (diffBottomToTop). diffLeftToRight и diffRightToLeft соответственно. 8. RenderParametersManager — наследник RenderManager. Для получения метрик виджета и разницы между ними. Код: class RenderMetricsScreen extends StatefulWidget {
@override State<StatefulWidget> createState() => _RenderMetricsScreenState(); } class _RenderMetricsScreenState extends State<RenderMetricsScreen> { final List<String> list = List.generate(20, (index) => index.toString()); /// Менеджер из библиотеки render_metrics /// для замеров позиционирования виджетов на экране final _renderParametersManager = RenderParametersManager(); final ScrollController scrollController = ScrollController(); /// id блока с кнопкой "Далее" final doneBlockId = 'doneBlockId'; final List<FocusNode> focusNodes = []; bool _isShow = false; OverlayEntry _overlayEntry; KeyboardListener _keyboardListener; /// Последний полученный FocusNode, зарегистрированный при смене фокуса FocusNode lastFocusedNode; @override void initState() { SchedulerBinding.instance.addPostFrameCallback((_) { _overlayEntry = OverlayEntry(builder: _buildOverlay); Overlay.of(context).insert(_overlayEntry); _keyboardListener = KeyboardListener() ..addListener(onChange: _keyboardHandle); }); FocusNode node; for(int i = 0; i < list.length; i++) { node = FocusNode(debugLabel: i.toString()); focusNodes.add(node); node.addListener(_onChangeFocus(node)); } super.initState(); } @override void dispose() { _keyboardListener.dispose(); _overlayEntry.remove(); focusNodes.forEach((node) => node.dispose()); super.dispose(); } @override Widget build(BuildContext context) { return Scaffold( body: SingleChildScrollView( controller: scrollController, child: SafeArea( child: Padding( padding: const EdgeInsets.all(20), child: Column( children: <Widget>[ for (int i = 0; i < list.length; i++) RenderMetricsObject( id: focusNodes[i], manager: _renderParametersManager, child: TextField( focusNode: focusNodes[i], decoration: InputDecoration(labelText: list[i]), ), ), ], ), ), ), ), ); } Widget _buildOverlay(BuildContext context) { return Stack( children: <Widget>[ Positioned( bottom: MediaQuery.of(context).viewInsets.bottom, left: 0, right: 0, child: RenderMetricsObject( id: doneBlockId, manager: _renderParametersManager, child: AnimatedOpacity( duration: const Duration(milliseconds: 200), opacity: _isShow ? 1.0 : 0.0, child: NextBlock( onPressed: () {}, isShow: _isShow, ), ), ), ), ], ); } VoidCallback _onChangeFocus(FocusNode node) => () { if (!node.hasFocus) return; lastFocusedNode = node; _doScrollIfNeeded(); }; /// Метод, срабатывающий при возникновении необходимости расчёта скролла /// экрана. void _doScrollIfNeeded() async { if (lastFocusedNode == null) return; double scrollOffset; try { /// Если нет нужного id, то data рендера вызовется на null scrollOffset = await _calculateScrollOffset(); } catch (e) { return; } _doScroll(scrollOffset); } /// Инициирование подскролла экрана void _doScroll(double scrollOffset) { double offset = scrollController.offset + scrollOffset; if (offset < 0) offset = 0; scrollController.position.animateTo( offset, duration: const Duration(milliseconds: 200), curve: Curves.linear, ); } /// Расчёт необходимого расстояния скролла экрана. /// /// Скролл произойдет при отклонении текстового поля от плашки "Готово" в обе /// стороны (вверх/вниз). Future<double> _calculateScrollOffset() async { await Future.delayed(const Duration(milliseconds: 300)); ComparisonDiff diff = _renderParametersManager.getDiffById( lastFocusedNode, doneBlockId, ); lastFocusedNode = null; if (diff == null || diff.firstData == null || diff.secondData == null) { return 0.0; } return diff.diffBottomToTop; } void _keyboardHandle(bool isVisible) { _isShow = isVisible; _overlayEntry?.markNeedsBuild(); } } Результат с использованием render_metrics Итог Копнув глубже уровня виджетов, с помощью небольших манипуляций со слоем рендера получили полезную функциональность, которая позволяет писать более сложные UI и логику. Иногда нужно знать размеры динамических виджетов, их позицию или сравнить перекрывающие друг на друга виджеты. И данная библиотека предоставляет все эти возможности для более быстрого и эффективного решения задач. В статье я постарался объяснить механизм работы, привёл пример проблемы и решения. Надеюсь на пользу библиотеки, статьи и на вашу обратную связь. =========== Источник: habr.com =========== Похожие новости:
Блог компании Surf ), #_dart, #_flutter, #_programmirovanie ( Программирование ), #_razrabotka_mobilnyh_prilozhenij ( Разработка мобильных приложений ) |
|
Вы не можете начинать темы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Текущее время: 22-Ноя 19:56
Часовой пояс: UTC + 5