Вопрос или проблема
У меня есть сценарий, в котором мне нужна форма с несколькими TextFormField
(в приведенном ниже фрагменте я упростил до трех). Эти TextFormField
должны отображаться только после завершения определенного Future (это анимация, но я упрощаю для удобства воспроизведения).
Когда form
отображается, фокус автоматически устанавливается на первый TextFormField
, и каждый раз, когда пользователь вводит один символ, фокус автоматически перемещается на следующий TextFormField
. Последний TextFormField
при заполнении не меняет focus
.
Основная проблема заключается в том: когда нажата кнопка “done
” на клавиатуре, курсор возвращается к первому TextFormField
, и клавиатура снова появляется (она начинает анимироваться, чтобы скрыться, но анимация не завершается, и она поднимается снова (смотрите гиф ниже)). Это также происходит, если пользователь пытается нажать “done” на клавиатуре, даже если он не находится в последнем TextFormField
.
Смотрите код ниже:
import 'package:flutter/material.dart';
void main() => runApp(const MyApp());
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
home: const MyHomePage(),
);
}
}
class MyHomePage extends StatefulWidget {
const MyHomePage({
super.key,
});
@override
State<MyHomePage> createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
bool _isAnswerCorrect = false;
bool _shouldSetFormVisible = false;
Widget _form = Container();
final _formKey = GlobalKey<FormState>();
FocusNode _focusNode1 = FocusNode();
FocusNode _focusNode2 = FocusNode();
FocusNode _focusNode3 = FocusNode();
TextEditingController _textEditingController1 = TextEditingController();
TextEditingController _textEditingController2 = TextEditingController();
TextEditingController _textEditingController3 = TextEditingController();
void _answerIsCorrect() {
setState(() {
_isAnswerCorrect = true;
});
}
@override
Widget build(BuildContext context) {
if (_shouldSetFormVisible) {
setState(() {
_shouldSetFormVisible = false;
});
//Я попробовал переместить присвоение переменной _form сюда, чтобы цвет шрифта формы изменился при нажатии кнопки (согласно этому ответу: https://stackoverflow.com/questions/79100177/form-set-after-future-delayed-wont-rebuild-after-subsequent-state-change-of-var), но теперь, после добавления _shouldSetFormVisible, чтобы присвоить поле формы только один раз, цвет больше не меняется. Я полагаю, это работало, потому что переменная формы переопределялась во время каждого перестроения, что не должно быть необходимо (смотрите второй фрагмент кода, который я предоставил по ссылке, которую я поделился выше)
_form = SizedBox(
width: 100,
child: Column(
children: [
Form(
key: _formKey,
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Expanded(
child: NumberInputField(
isAnswerCorrect: _isAnswerCorrect,
isFirstField: true,
isLastField: false,
currentFieldFocusNode: _focusNode1,
nextFieldFocusNode: _focusNode2,
textEditingController: _textEditingController1),
),
SizedBox(width: 20),
Expanded(
child: NumberInputField(
isAnswerCorrect: _isAnswerCorrect,
isFirstField: false,
isLastField: false,
currentFieldFocusNode: _focusNode2,
nextFieldFocusNode: _focusNode3,
textEditingController: _textEditingController2),
),
SizedBox(width: 20),
Expanded(
child: NumberInputField(
isAnswerCorrect: _isAnswerCorrect,
isFirstField: false,
isLastField: true,
currentFieldFocusNode: _focusNode3,
nextFieldFocusNode:
_focusNode3, //попробовал установить на другой focusNode, но результат тот же
textEditingController: _textEditingController3),
),
],
),
),
SizedBox(height: 20),
ElevatedButton(
child: Text('Отправить'),
onPressed: _answerIsCorrect,
),
],
),
);
}
return Scaffold(
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
ElevatedButton(
child: const Text('Показать форму'),
onPressed: () {
Future.delayed(
const Duration(milliseconds: 100),
() => setState(
() {
setState(() {
_shouldSetFormVisible = true;
});
},
),
);
},
),
const SizedBox(height: 20),
_form,
],
),
),
);
}
}
class NumberInputField extends StatelessWidget {
final bool isAnswerCorrect;
final bool isFirstField;
final bool isLastField;
final FocusNode currentFieldFocusNode;
final FocusNode nextFieldFocusNode;
final TextEditingController textEditingController;
const NumberInputField({
super.key,
required this.isAnswerCorrect,
required this.isFirstField,
required this.isLastField,
required this.currentFieldFocusNode,
required this.nextFieldFocusNode,
required this.textEditingController,
});
@override
Widget build(BuildContext context) {
Color fontColor = Colors.blue;
if (isAnswerCorrect) {
fontColor = Colors.black;
}
//автоматически устанавливаем фокус на первое поле
if (isFirstField) {
FocusScope.of(context).requestFocus(currentFieldFocusNode);
}
return TextFormField(
controller: textEditingController,
focusNode: currentFieldFocusNode,
onChanged: (val) {
//если пользователь вводит один символ, переходим к следующему полю
if (val.length == 1) {
if (!isLastField) {
FocusScope.of(context).requestFocus(nextFieldFocusNode);
}
//else блок ниже также вызывает перестроение формы (курсора возвращается в первое поле)
// else {
// FocusScope.of(context).unfocus();
// }
}
},
style: TextStyle(color: fontColor),
keyboardType: TextInputType.number,
textAlign: TextAlign.center,
decoration: const InputDecoration(
hintText: '?',
),
);
}
}
Похоже, что форма “перестраивается” каким-то образом (что, кстати, я не понимаю, т.е. что означает перестройка формы и когда это происходит, потому что она изменяет только некоторые свои состояния, т.е. набранные числа не исчезают)
Также есть еще одна, но, вероятно, связанная проблема в том, что цвет шрифта формы не изменяется после нажатия ElevatedButton
. Пожалуйста, смотрите для этой другой проблемы, вопрос, который я задал здесь.
Я думаю, что это из-за того, что вы используете вложенный setState
в onPressed
, потому что внутренний вызов setState
избыточен, поскольку он уже находится внутри колбэка setState
. Вы можете упростить это, имея один setState
.
onPressed: () => setState(() {
_shouldSetFormVisible = true;
}),
Вы также можете инициализировать виджет form
заранее, условно, основываясь на _shouldSetFormVisible
следующим образом:
Widget _form = _shouldSetFormVisible ? buildForm() : Container();
Ответ или решение
Проблема с несколькими TextFormField в Flutter: Кнопка "Готово" не закрывает клавиатуру
В рассматриваемом сценарии вы разработали форму с несколькими TextFormField
, которые появляются только после завершения определенной асинхронной операции. Однако, при нажатии кнопки "Готово" на клавиатуре, курсор возвращается к первому полю ввода, и клавиатура снова открывается. Давайте разберем причину этой проблемы и предложим решения.
Причины проблемы
-
Вторичное использование setState:
Вы используете вложенные вызовыsetState
. Это может вызывать ненужные перестройки виджета и потерю фокуса. В вашем коде изменения состояния происходят несколько раз, что приводит к сбоям в управлении фокусом. -
Управление фокусом:
Каждое нажатие кнопки "Готово" вызывает изменения в состоянии формы, что приводит к потере фокуса на текущемTextFormField
. При этом, курсор возвращается к первому полю. -
Перестройка виджета:
При каждом вызовеsetState
может произойти полная перестройка виджета, что также ведет к возвращению фокуса.
Решения
-
Оптимизация setState:
Убедитесь, что вы вызываетеsetState
только один раз на каждое изменение. Это поможет избежать ненужных перестроек. Например, вы можете упростить обработчик нажатия кнопки следующим образом:onPressed: () => setState(() { _shouldSetFormVisible = true; }),
-
Создание виджета формы в методе:
Вместо динамического назначения формы вbuild
, создайте функцию для возвращения формы:Widget _buildForm() { return SizedBox( width: 100, child: Column( children: [ Form( key: _formKey, child: Row( mainAxisAlignment: MainAxisAlignment.center, children: [ Expanded(child: NumberInputField(/* параметры */)), SizedBox(width: 20), Expanded(child: NumberInputField(/* параметры */)), SizedBox(width: 20), Expanded(child: NumberInputField(/* параметры */)), ], ), ), SizedBox(height: 20), ElevatedButton( child: Text('Submit'), onPressed: _answerIsCorrect, ), ], ), ); }
Затем, в методе
build
можно просто вызывать_buildForm()
, когда_shouldSetFormVisible
равно true. -
Управление фокусом для последнего поля:
Убедитесь, что вNumberInputField
для последнего поля фокус не перемещается. Добавьте условие, чтобыonChanged
не менял фокус, когда последнее поле заполнено:onChanged: (val) { if (val.length == 1 && !isLastField) { FocusScope.of(context).requestFocus(nextFieldFocusNode); } },
-
Закрытие клавиатуры:
Чтобы закрыть клавиатуру, когда последнийTextFormField
заполнен, используйте методunfocus
для текущегоFocusNode
.Пример:
if (val.length == 1) { if (isLastField) { FocusScope.of(context).unfocus(); } else { FocusScope.of(context).requestFocus(nextFieldFocusNode); } }
-
Изменение цвета текста после отправки:
Обновите обработку изменения цвета текста в ответ на событие нажатия кнопки "Отправить". Проверьте, чтобы состояние отображалось правильно. Вы можете использоватьsetState
для изменения переменной состояния.
Применение указанных оптимизаций должно помочь решить проблему с возвратом курсора и открытием клавиатуры. Убедитесь, что вы тестируете каждое изменение, чтобы гарантировать правильность работы формы.