После прочтения этой статьи вы не только узнаете реализацию и состав
TextField
, но также узнаете много "странного" знаний, которые раньше не использовались так часто.
TextField
в Flutter — это довольно сложный компонент, внутри которого содержится множество различных компонентов с различной реализацией. Эти компоненты вместе создают эффекты, которые мы обычно видим в обычных полях ввода. Ниже приведена диаграмма основных частей TextField
, а также то, что будет подробно рассмотрено в данной статье.
Компонент FocusTrapArea
может показаться вам незнакомым, поскольку он был добавлен недавно. Сам по себе FocusTrapArea
ничего особенного не представляет; он просто помещает FocusNode
в дерево RenderObject
.
Основная цель его внедрения заключается в поддержке платформ Web/Desktop. Внедрение FocusTrapArea
позволяет полю ввода TextField
сохранять фокус после выполнения метода TextEditingController.clear
на этих платформах.
Подробнее см. issue Flutter: #86154, #86041.Обычное поведение | Ненормальное поведение | | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | |
|
|
Он主要用于处理 TextField
中当前输入框是否可用的状态,例如当 widget.enabled
或者 widget.decoration?.enabled
为 false
时,IgnorePointer
将屏蔽区域内所有手势事件,从而使 TextField
不可点击输入。
关于 TextSelectionGestureDetectorBuilder
,你可能最不常遇到它,而在 TextField
中使用的是它的子类 _TextFieldSelectionGestureDetectorBuilder
:
Основная задача этого класса заключается в обработке событий кликов, свайпов и долгих нажатий внутри
EditableText
вTextField
. Например, одиночный клик вызывает появление клавиатуры, а долгое нажатие вызывает появление меню выбора и копирования/вставки.
Внутри TextSelectionGestureDetectorBuilder
основной механизм состоит в использовании GlobalKey editableTextKey
для получения состояния EditableTextState
, что позволяет связывать различные события жестов с поведением EditableText
.
Внутри данного компонента используется
TextSelectionGestureDetector
.
Например, в _TextFieldSelectionGestureDetectorBuilder
можно найти процесс обработки события onSingleTapUp
:
Как показывают вышеупомянутые строки кода:- 1) Скрытие уже открытого ToolBar (первоначально это Overlay, то есть диалоговое окно для копирования/вставки);
onTap
в TextField
может оказаться неудобным, поскольку он уже вызвал появление клавиатуры.В конце концов, _TextFieldSelectionGestureDetectorBuilder
вызывает метод buildGestureDetector
для создания контроллера, который слушает и обрабатывает события касания, чтобы использовать его для управления потомством.
Что касается внутренних параметров InputDecorator
, подробное объяснение здесь приведено не будет, так как ранее данная тема была рассмотрена более детально в книге. Для тех, кто работал с TextField
, InputDecorator
должен быть знакомым компонентом; в TextField
реализация InputDecorator
осуществляется вместе с AnimatedBuilder
. Поскольку в TextField
как FocusNode
, так и TextEditingController
являются ChangeNotifier
(Listenable
), они могут использоваться в качестве animation
в AnimatedBuilder
.
Когда FocusNode
и TextEditingController
изменяются, это приводит к тому, что InputDecorator
заново строится (rebuild
), меняя тем самым эффект отрисовки, например, при вводе данных в поле ввода или смене фокуса можно менять цвет фона поля ввода.
Обратите внимание, чтобы не спутать
InputDecorator
иInputDecoration
.InputDecoration
используется для конфигурацииInputDecorator
.
Как видно, InputDecorator
имеет множество параметров и возможностей для настройки, позволяющих разработчику создавать сложные пользовательские интерфейсы для полей ввода. Однако если вы столкнетесь со случаями, когда некоторые позиции или зазоры не удовлетворяют странным требованиям продукта, то поздравляем вас — вы начинаете путь продвинутого разработчика Flutter.
Почему?
Проще говоря, реализация InputDecorator
основана на внутреннем пользовательском RenderBox
, где части кода, связанные с макетом, занимают более 600 строк. Это значит, что согласно параметрам InputDecoration
, таким как icon
, prefixIcon
, suffix
и т.д., происходит расположение компонентов, расчет положений и направлений, а также корректировка положений относительно базовой линии.
Кроме того, анимационные эффекты внутри
InputDecorator
создаются главным образом через внутренние компоненты типаAnimatedOpacity
.
Поэтому, если вам не нравятся какие-то позиции или границы в InputDecorator
, либо придется переопределить его реализацию, либо придется принять ситуацию «все равно лучше, чем ничего».
Зачем внутри TextField
есть RepaintBoundary
? В первую очередь, что делает RepaintBoundary
?
Ранее было подробно объяснено в статье "Полное понимание рендеринга в Flutter". Проще говоря, RepaintBoundary
主要用于创建一个Layer
,获得一个独立的绘图区域.Часто это встречается при переходах между страницами в Navigator
, где каждый регион является самостоятельной областью рисования благодаря наличию RepaintBoundary
.
Также стоит отметить, что каждая страница в
Navigator
имеет свойFocusScope
, который мы используем для управления клавиатурой и фокусом, например, через методFocusScope.of(context)
. ВнутриTextField
существуетRepaintBoundary
, посколькуTextField
представляет собой компонент, который требует частого обновления. В то же время изменения содержимого внутриTextField
обычно не требуют перерисовки родительского контейнера, поэтомуRepaintBoundary
позволяет реализовать более эффективное локальное отрисовывание.
UnmanagedRestorationScope
используется реже всего; это InheritedWidget
, который предназначен для передачи вниз одного RestorationBucket
. RestorationBucket
主要用于与状态的保存和恢复相关的工作。
例如,当应用程序由于内存不足而在后台被系统回收时,可以使用它来恢复特定数据。举个例子:
import 'package:flutter/material.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
// Присвойте вашему RootRestorationScope уникальный идентификатор, значение по умолчанию — null.
restorationScopeId: 'root',
home: HomePage(),
);
}
}
class HomePage extends StatefulWidget {
@override
_HomePageState createState() => _HomePageState();
}
// Наше состояние должно быть смешано с RestorationMixin
class _HomePageState extends State<HomePage> with RestorationMixin {
``` // Для каждого состояния нам нужно использовать восстановляемое свойство
final RestorableInt _index = RestorableInt(0);
@override
Widget build(BuildContext context) {
return Scaffold(
body: Center(child: Text('Индекс равен ${_index.value}')),
bottomNavigationBar: BottomNavigationBar(
currentIndex: _index.value,
onTap: (i) => setState(() => _index.value = i),
items: <BottomNavigationBarItem>[
BottomNavigationBarItem(
icon: Icon(Icons.home),
label: 'Главная'
),
BottomNavigationBarItem(
icon: Icon(Icons.notifications),
label: 'Уведомления'
),
BottomNavigationBarItem(
icon: Icon(Icons.settings),
label: 'Настройки'
),
],
),
);
}
@override
// Уникальный идентификатор для этой страницы,
// давайте назовём его по имени нашей страницы!
String get restorationId => 'home_page';
@override
void restoreState(RestorationBucket? oldBucket, bool initialRestore) {
// Регистрация нашего свойства для сохранения каждый раз, когда оно меняется,
// и для восстановления каждый раз, когда наша приложение уничтожается ОС!
registerForRestoration(_index, 'nav_bar_index');
}
}
```Как показано выше кодом:
- Вначале конфигурируется `MaterialApp` с помощью `restorationScopeId` (необходимо обязательно указать, чтобы включить данную функцию).
- Используется `RestorableInt`, чтобы настроить и сохранить значение `index` компонента `BottomNavigationBar`.
- В `State` внедряется `RestorationMixin`, а состояние `index` восстанавливается внутри метода `restoreState`.```По умолчанию внутренняя часть `MaterialApp` использует `RootRestorationScope`, который сам представляет собой `UnmanagedRestorationScope`. После запуска данного примера можно наблюдать эффект, отключив опцию "Не сохранять активность" в настройках разработчика эмулятора.> Пример взят из статьи "[Введение в восстановление состояния в Flutter](https://dev.to/pedromassango/что-такое-восстановление-состояния-и-как-его-использовать-в-flutter-5blm)".
Перейдем к `TextField`: в `_TextFieldState` внедряется `RestorationMixin`, а затем используется `RestorableTextEditingController` для восстановления `TextEditingController`.
> Поскольку содержимое поля ввода по умолчанию сохраняется в поле `TextEditingValue` объекта `TextEditingController`, здесь используется `RestorableTextEditingController`.

Обычно `MaterialApp` уже содержит встроенную реализацию `RootRestorationScope`, поэтому нам достаточно установить `restorationScopeId` для `MaterialApp`. А **`TextField` благодаря встроенной логике `UnmanagedRestorationScope` обеспечивает сохранение и восстановление введенного текста**.
## EditableText
`EditableText` — это основной компонент `TextField`, который позволяет прокручивать контент через `Scrollable`. Также он использует соответствующий `restorationId` для восстановления и кэширования данных.
**Сначала обратите внимание на возможность прокрутки: для `EditableText` это как "viewport", который работает на основе `ViewportOffset` для достижения эффекта прокрутки**.
А внутри `EditableText` используется `CompositedTransformTarget` для связи между `Toolbar` и полем ввода, то есть для связи между элементами управления вводом и плавающими окнами "вставить/вырезать".
**Поэтому давайте кратко рассмотрим `CompositedTransformTarget`: этот компонент обычно используется вместе с `CompositedTransformFollower` для создания связей между различными элементами управления**. Как показано выше, часто используемый встроенный `Slider`, реализуется с использованием `CompositedTransformTarget` и `CompositedTransformFollower`. **Это позволяет одному компоненту следовать за другим без необходимости вычисления позиций; они соединены между собой через `LayerLink`**. Возврат к `TextField`, на самом деле, помимо панели инструментов "Копировать/вставить", выборка выделенного текста внутри `EditableText` также реализуется аналогичным образом, просто здесь используется слой `LeaderLayer` напрямую, а не через его упаковку `CompositedTransformTarget`.
> Для заинтересованных в использовании `CompositedTransformTarget` можно обратиться к следующей статье: https://juejin.cn/post/6946416845537116190
Конечно, использование `CompositedTransformTarget` всё ещё приводит к значительному снижению производительности, поэтому не рекомендуется использовать его часто и массово, так как это относится к операциям типа `pushLayer`.
Кроме того, внутренняя часть `EditableText`, отвечающая за отрисовку содержимого, основана на известной нам `TextPainter`. В этом нет ничего особенного, подробнее мы пока не будем раскрывать.
**Поэтому цель данной статьи — это представление составных частей `TextField` и объяснение их функциональности, чтобы разработчики могли лучше понять реализацию часто используемых полей ввода текста в Flutter, что поможет быстро находить и решать проблемы**, такие как:- Откуда появляется панель инструментов "Копировать/вставить";
- Как панель инструментов располагается и размещается;
- Как при нажатии на `TextField` вызывается клавиатура и обрабатываются события жестами;
- Как `TextField` обеспечивает локальное отображение;
- ...
Наконец, рассмотрим простой пример. Недавно кто-то спросил меня: ***как реализовать эффект перехода с однострочного поля ввода на многолинейное в Flutter, подобно тому, как это сделано в WeChat***, вот код:
```dart
TextField(
focusNode: _focusNode,
maxLines: 7,
minLines: 1,
decoration:
const InputDecoration(border: OutlineInputBorder()),
)
Вы можете оставить комментарий после Вход в систему
Неприемлемый контент может быть отображен здесь и не будет показан на странице. Вы можете проверить и изменить его с помощью соответствующей функции редактирования.
Если вы подтверждаете, что содержание не содержит непристойной лексики/перенаправления на рекламу/насилия/вульгарной порнографии/нарушений/пиратства/ложного/незначительного или незаконного контента, связанного с национальными законами и предписаниями, вы можете нажать «Отправить» для подачи апелляции, и мы обработаем ее как можно скорее.
Опубликовать ( 0 )