跳转至正文

用户输入

使用按钮和文本字段接受用户输入。

学习构建文本输入、使用 controller 管理文本,以及使用按钮处理用户操作。

你将完成的内容

使用 TextField 构建文本输入 widget
使用 TextEditingController 管理文本
控制输入焦点以改善用户体验
使用回调和按钮处理用户操作

步骤

1

介绍

应用会在 Tile widget 中显示用户的猜测,但还需要让用户输入这些猜测的方式。在本课中,使用两个交互 widget 构建该功能: TextFieldIconButton

2

实现回调函数

为了让用户输入猜测,你将创建一个名为 GuessInput 的专用 widget。首先,为 GuessInput widget 创建基本结构,该结构需要一个回调函数作为参数。将回调函数命名为 onSubmitGuess

将以下代码添加到你的 main.dart 文件中。

dart
class GuessInput extends StatelessWidget {
  GuessInput({super.key, required this.onSubmitGuess});

  final void Function(String) onSubmitGuess;

  @override
  Widget build(BuildContext context) {
    // You'll build the UI in the next steps.
    return Container(); // Placeholder
  }
}

final void Function(String) onSubmitGuess; 这一行声明了类中名为 onSubmitGuessfinal 成员,其类型为 void Function(String)。该函数接受单个 String 参数(用户的猜测),且不返回任何值(由 void 表示)。

此回调表明,实际处理用户猜测的逻辑将在别处编写。对于交互 widget,使用回调函数是良好实践,可保持处理交互的 widget 可复用且与任何具体功能解耦。

到本课结束时,当用户输入猜测时会调用传入的 onSubmitGuess 函数。首先,你需要构建此 widget 的视觉部分。 widget 将如下所示。

A screenshot of the Flutter property editor tool.
3

TextField widget

由于文本字段和按钮并排显示,将它们创建为 Row widget。将 build 方法中的 Container 占位符替换为包含 Expanded TextFieldRow

dart
class GuessInput extends StatelessWidget {
  GuessInput({super.key, required this.onSubmitGuess});

  final void Function(String) onSubmitGuess;

  @override
  Widget build(BuildContext context) {
    return Row(
      children: [
        Expanded(
          child: Padding(
            padding: const EdgeInsets.all(8.0),
            child: TextField(
              maxLength: 5,
              decoration: InputDecoration(
                border: OutlineInputBorder(
                  borderRadius: BorderRadius.all(Radius.circular(35)),
                ),
              ),
            ),
          ),
        ),
      ],
    );
  }
}

你在之前的课程中见过其中一些 widget: RowPadding。不过,Expanded widget 是新的。当 Row(或 Column)的子 widget 被 Expanded 包裹时,它会告诉该子 widget 沿主轴填满所有可用空间(Row 为水平方向,Column 为垂直方向),前提是其他子 widget 尚未占用。这使 TextField 拉伸以占据行中 其他 widget 占用空间外的所有空间。

TextField widget 在本课中也是新的,并且是重头戏。这是 Flutter 用于文本输入的基本 widget。

到目前为止,TextField 具有以下配置。

  • 它使用圆角边框装饰。请注意,装饰配置与 Container 和盒子的装饰方式非常相似。

  • maxLength 属性设置为 5,因为游戏仅允许 5 个字母的单词猜测。

4

使用 TextEditingController 处理文本

接下来,你需要一种方式来管理用户输入到输入框中的文本。为此,请使用 TextEditingController

dart
class GuessInput extends StatelessWidget {
  GuessInput({super.key, required this.onSubmitGuess});

  final void Function(String) onSubmitGuess;

  // NEW
  final TextEditingController _textEditingController = TextEditingController();

  @override
  Widget build(BuildContext context) {
    return Row(
      children: [
        Expanded(
          child: Padding(
            padding: const EdgeInsets.all(8.0),
            child: TextField(
              maxLength: 5,
              decoration: InputDecoration(
                border: OutlineInputBorder(
                  borderRadius: BorderRadius.all(Radius.circular(35)),
                ),
              ),
            ),
          ),
        ),
        //
      ],
    );
  }
}

TextEditingController 用于读取、清除和修改 TextField 中的文本。使用时,将其传入 TextField

dart
class GuessInput extends StatelessWidget {
  GuessInput({super.key, required this.onSubmitGuess});

  final void Function(String) onSubmitGuess;

  final TextEditingController _textEditingController = TextEditingController();

  @override
  Widget build(BuildContext context) {
    return Row(
      children: [
        Expanded(
          child: Padding(
            padding: const EdgeInsets.all(8.0),
            child: TextField(
              maxLength: 5,
              decoration: const InputDecoration(
                border: OutlineInputBorder(
                  borderRadius: BorderRadius.all(Radius.circular(35)),
                ),
              ),
              controller: _textEditingController, // NEW
            ),
          ),
        ),
      ],
    );
  }
}

现在,当用户输入文本时,你可以通过 _textEditingController 捕获它,但你需要知道 何时 捕获。响应输入的最简单方式是使用 TextField.onSubmitted 参数。该参数接受回调,每当文本字段获得焦点时用户在键盘上按下「Enter」键,就会触发该回调。

目前,通过将以下回调添加到 TextField.onSubmitted 来确保其正常工作:

dart
class GuessInput extends StatelessWidget {
  GuessInput({super.key, required this.onSubmitGuess});

  final void Function(String) onSubmitGuess;

  final TextEditingController _textEditingController = TextEditingController();

  @override
  Widget build(BuildContext context) {
    return Row(
      children: [
        Expanded(
          child: Padding(
            padding: const EdgeInsets.all(8.0),
            child: TextField(
              maxLength: 5,
              decoration: const InputDecoration(
                border: OutlineInputBorder(
                  borderRadius: BorderRadius.all(Radius.circular(35)),
                ),
              ),
              controller: _textEditingController,
              onSubmitted: (input) {
                // NEW
                print(_textEditingController.text); // Temporary
              },
            ),
          ),
        ),
      ],
    );
  }
}

在这种情况下,你可以直接打印传给 onSubmitted 回调的 input,但更好的用户体验是在每次猜测后清除文本:你需要 TextEditingController 来实现这一点。按如下方式更新代码:

dart
class GuessInput extends StatelessWidget {
  GuessInput({super.key, required this.onSubmitGuess});

  final void Function(String) onSubmitGuess;

  final TextEditingController _textEditingController = TextEditingController();

  @override
  Widget build(BuildContext context) {
    return Row(
      children: [
        Expanded(
          child: Padding(
            padding: const EdgeInsets.all(8.0),
            child: TextField(
              maxLength: 5,
              decoration: const InputDecoration(
                border: OutlineInputBorder(
                  borderRadius: BorderRadius.all(Radius.circular(35)),
                ),
              ),
              controller: _textEditingController,
              onSubmitted: (_) {
                // UPDATED
                print(_textEditingController.text); // Temporary
                _textEditingController.clear(); // NEW
              },
            ),
          ),
        ),
      ],
    );
  }
}
5

获取输入焦点

通常,你希望特定输入或 widget 在用户无需操作的情况下自动获得焦点。例如,在本应用中,用户唯一能做的就是输入猜测,因此应用启动时 TextField 应自动获得焦点。用户输入猜测后,焦点应保持在 TextField 中,以便输入下一次猜测。

要解决第一个焦点问题,请在 TextField 上设置 autofocus 属性。

dart
class GuessInput extends StatelessWidget {
  GuessInput({super.key, required this.onSubmitGuess});

  final void Function(String) onSubmitGuess;

  final TextEditingController _textEditingController = TextEditingController();

  @override
  Widget build(BuildContext context) {
    return Row(
      children: [
        Expanded(
          child: Padding(
            padding: const EdgeInsets.all(8.0),
            child: TextField(
              maxLength: 5,
              decoration: const InputDecoration(
                border: OutlineInputBorder(
                  borderRadius: BorderRadius.all(Radius.circular(35)),
                ),
              ),
              controller: _textEditingController,
              autofocus: true, // NEW
              onSubmitted: (input) {
                print(input); // Temporary
                _textEditingController.clear();
              },
            ),
          ),
        ),
      ],
    );
  }
}

第二个问题需要你使用 FocusNode 管理键盘焦点。你可以使用 FocusNode 请求 TextField 获得焦点(在移动端使键盘出现),或了解字段何时具有焦点。

首先,在 GuessInput 类中创建 FocusNode

dart
class GuessInput extends StatelessWidget {
  GuessInput({super.key, required this.onSubmitGuess});

  final void Function(String) onSubmitGuess;

  final TextEditingController _textEditingController = TextEditingController();

  final FocusNode _focusNode = FocusNode(); // NEW

  @override
  Widget build(BuildContext context) {
    // ...
    return Container();
  }
}

然后,在 controller 清除后提交 TextField 时,使用 FocusNode 请求焦点:

dart
class GuessInput extends StatelessWidget {
  GuessInput({super.key, required this.onSubmitGuess});

  final void Function(String) onSubmitGuess;

  final TextEditingController _textEditingController = TextEditingController();

  final FocusNode _focusNode = FocusNode();

  @override
  Widget build(BuildContext context) {
    return Row(
      children: [
        Expanded(
          child: Padding(
            padding: const EdgeInsets.all(8.0),
            child: TextField(
              maxLength: 5,
              decoration: const InputDecoration(
                border: OutlineInputBorder(
                  borderRadius: BorderRadius.all(Radius.circular(35)),
                ),
              ),
              controller: _textEditingController,
              autofocus: true,
              focusNode: _focusNode, // NEW
              onSubmitted: (input) {
                print(input); // Temporary
                _textEditingController.clear();
                _focusNode.requestFocus(); // NEW
              },
            ),
          ),
        ),
      ],
    );
  }
}

现在,输入文本后按 Enter,你可以继续输入。

6

使用输入

最后,你需要处理用户输入的文本。回想一下,GuessInput 的构造函数需要一个名为 onSubmitGuess 的回调。在 GuessInput 中,你需要使用该回调。将 print 语句替换为对该函数的调用。

dart
class GuessInput extends StatelessWidget {
  GuessInput({super.key, required this.onSubmitGuess});

  final void Function(String) onSubmitGuess;

  final TextEditingController _textEditingController = TextEditingController();

  final FocusNode _focusNode = FocusNode();

  @override
  Widget build(BuildContext context) {
    return Row(
      children: [
        Expanded(
          child: Padding(
            padding: const EdgeInsets.all(8.0),
            child: TextField(
              maxLength: 5,
              decoration: const InputDecoration(
                border: OutlineInputBorder(
                  borderRadius: BorderRadius.all(Radius.circular(35)),
                ),
              ),
              controller: _textEditingController,
              autofocus: true,
              focusNode: _focusNode,
              onSubmitted: (input) {
                onSubmitGuess(_textEditingController.text.trim());
                _textEditingController.clear();
                _focusNode.requestFocus();
              },
            ),
          ),
        ),
      ],
    );
  }
}

其余功能由父 widget GamePage 处理。在该类的 build 方法中,在 Column widget 的 children 中 Row widget 下方,添加 GuessInput widget:

dart
class GamePage extends StatelessWidget {
  GamePage({super.key});

  final Game _game = Game();

  @override
  Widget build(BuildContext context) {
    return Padding(
      padding: const EdgeInsets.all(8.0),
      child: Column(
        spacing: 5.0,
        children: [
          for (final guess in _game.guesses)
            Row(
              spacing: 5.0,
              children: [
                for (final letter in guess) Tile(letter.char, letter.type),
              ],
            ),
          GuessInput(
            onSubmitGuess: (guess) {
              // TODO, handle guess
              print(guess); // Temporary
            },
          ),
        ],
      ),
    );
  }
}

目前,这只会打印猜测以证明连接正确。提交猜测需要使用 StatefulWidget 的功能,你将在下一课中完成。

7

按钮

为改善移动端 UX 并体现常见的 UI 实践,还应有可提交猜测的按钮。

Flutter 内置了许多按钮 widget,例如 TextButtonElevatedButton,以及你现在将使用的 IconButton。所有这些按钮(以及许多其他交互 widget)都需要两个参数(除可选参数外):

  • 传给 onPressed 的回调函数。

  • 构成按钮内容的 widget(通常是 TextIcon)。

GuessInput widget 中将图标按钮添加到 row widget 的 children 列表,并为其提供要显示的 Icon widget。 Icon widget 需要配置;在本例中, padding 属性将按钮边缘与其包裹的图标之间的内边距设置为零。这会移除默认内边距并使按钮更小。

dart
class GuessInput extends StatelessWidget {
  GuessInput({super.key, required this.onSubmitGuess});

  final void Function(String) onSubmitGuess;

  final TextEditingController _textEditingController = TextEditingController();
  final FocusNode _focusNode = FocusNode();

  @override
  Widget build(BuildContext context) {
    return Row(
      children: [
        Expanded(child: Container()),
        IconButton(
          padding: EdgeInsets.zero,
          icon: const Icon(Icons.arrow_circle_up),
          onPressed: null,
        ),
      ],
    );
  }
}

IconButton.onPressed 回调应该看起来很熟悉:

dart
class GuessInput extends StatelessWidget {
  GuessInput({super.key, required this.onSubmitGuess});

  final void Function(String) onSubmitGuess;

  final TextEditingController _textEditingController = TextEditingController();
  final FocusNode _focusNode = FocusNode();

  @override
  Widget build(BuildContext context) {
    return Row(
      children: [
        Expanded(child: Container()),
        IconButton(
          padding: EdgeInsets.zero,
          icon: const Icon(Icons.arrow_circle_up),
          onPressed: () {
            onSubmitGuess(_textEditingController.text.trim());
            _textEditingController.clear();
            _focusNode.requestFocus();
          },
        ),
      ],
    );
  }
}

此方法与 TextField 上的 onSubmitted 回调作用相同。

8

回顾

你完成的内容

以下是你本课构建与学习内容的摘要。
使用 TextField 构建了文本输入 widget

你创建了带有用于文本输入的 TextFieldGuessInput widget。你为其配置了圆角边框、字符限制,并使用 Expanded 使其填满行中的可用空间。

使用 TextEditingController 管理了文本

TextEditingController 让你读取和修改文本字段内容。你使用 .text 捕获用户输入,并在提交后使用 .clear() 清除字段。

控制了输入焦点以打造精致的 UX

你使用 autofocus 在启动时聚焦文本字段,并使用 FocusNode 配合 requestFocus() 在每次猜测后保持焦点。这些细节让你的应用感觉响应迅速且制作精良。

使用回调和按钮处理了用户操作

为响应用户输入,你指定了 onSubmittedonPressed 等回调函数。将回调函数作为构造函数参数传入可保持 widget 可复用且与具体逻辑解耦。

9

自测

用户输入测验

1 / 2
如何以编程方式读取或清除 TextField 中的文本?
  1. 直接访问 TextField 的 text 属性。

    不正确。

    TextField 不暴露 text 属性;你需要 controller。

  2. 使用附加到 TextField 的 TextEditingController。

    正确!

    TextEditingController 提供 text 属性以读取值,并提供 clear() 方法以重置。

  3. 监听 onChanged 回调并将值存储在变量中。

    不正确。

    onChanged 可用于读取,但清除需要 TextEditingController。

  4. 调用 TextField.getText() 方法。

    不正确。

    TextField 没有 getText 方法;请改用 TextEditingController。

如何以编程方式将焦点移到特定 TextField?
  1. 直接调用 TextField.focus()

    不正确。

    TextField 没有 focus 方法;你需要使用 FocusNode。

  2. 在运行时将 autofocus 属性设置为 true。

    不正确。

    autofocus 属性仅在初始构建时有效,不能用于之后移动焦点。

  3. 使用 FocusNode 并对其调用 requestFocus()

    正确!

    FocusNode 让你控制焦点,调用 requestFocus() 会将焦点移到其关联的 widget。

  4. 将 TextField 包裹在 GestureDetector 中并以编程方式点击。

    不正确。

    焦点不是这样管理的;FocusNode 才是正确方式。