Несколько полей textFormField: при нажатии “готово” на клавиатуре клавиатура не скрывается и курсор возвращается к первому полю textFormField.

Вопрос или проблема

У меня есть сценарий, в котором мне нужна форма с несколькими 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, которые появляются только после завершения определенной асинхронной операции. Однако, при нажатии кнопки "Готово" на клавиатуре, курсор возвращается к первому полю ввода, и клавиатура снова открывается. Давайте разберем причину этой проблемы и предложим решения.

Причины проблемы

  1. Вторичное использование setState:
    Вы используете вложенные вызовы setState. Это может вызывать ненужные перестройки виджета и потерю фокуса. В вашем коде изменения состояния происходят несколько раз, что приводит к сбоям в управлении фокусом.

  2. Управление фокусом:
    Каждое нажатие кнопки "Готово" вызывает изменения в состоянии формы, что приводит к потере фокуса на текущем TextFormField. При этом, курсор возвращается к первому полю.

  3. Перестройка виджета:
    При каждом вызове setState может произойти полная перестройка виджета, что также ведет к возвращению фокуса.

Решения

  1. Оптимизация setState:
    Убедитесь, что вы вызываете setState только один раз на каждое изменение. Это поможет избежать ненужных перестроек. Например, вы можете упростить обработчик нажатия кнопки следующим образом:

    onPressed: () => setState(() {
     _shouldSetFormVisible = true;
    }),
  2. Создание виджета формы в методе:
    Вместо динамического назначения формы в 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.

  3. Управление фокусом для последнего поля:
    Убедитесь, что в NumberInputField для последнего поля фокус не перемещается. Добавьте условие, чтобы onChanged не менял фокус, когда последнее поле заполнено:

    onChanged: (val) {
     if (val.length == 1 && !isLastField) {
       FocusScope.of(context).requestFocus(nextFieldFocusNode);
     }
    },
  4. Закрытие клавиатуры:
    Чтобы закрыть клавиатуру, когда последний TextFormField заполнен, используйте метод unfocus для текущего FocusNode.

    Пример:

    if (val.length == 1) {
     if (isLastField) {
       FocusScope.of(context).unfocus();
     } else {
       FocusScope.of(context).requestFocus(nextFieldFocusNode);
     }
    }
  5. Изменение цвета текста после отправки:
    Обновите обработку изменения цвета текста в ответ на событие нажатия кнопки "Отправить". Проверьте, чтобы состояние отображалось правильно. Вы можете использовать setState для изменения переменной состояния.

Применение указанных оптимизаций должно помочь решить проблему с возвратом курсора и открытием клавиатуры. Убедитесь, что вы тестируете каждое изменение, чтобы гарантировать правильность работы формы.

Оцените материал
Добавить комментарий

Капча загружается...