跳转至正文

用户输入

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

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

你将完成的内容

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

步骤

1

介绍

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

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; 这一行声明了类中名为 onSubmitGuess 的 final 成员,其类型为 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 TextField 的 Row:

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: Row 和 Padding。不过,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,例如 TextButton、 ElevatedButton,以及你现在将使用的 IconButton。所有这些按钮(以及许多其他交互 widget)都需要两个参数(除可选参数外):

  • 传给 onPressed 的回调函数。

  • 构成按钮内容的 widget(通常是 Text 或 Icon)。

在 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

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

使用 TextEditingController 管理了文本

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

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

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

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

为响应用户输入,你指定了 onSubmitted 和 onPressed 等回调函数。将回调函数作为构造函数参数传入可保持 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 才是正确方式。