Flutter-秘籍-三-

581 阅读20分钟

Flutter 秘籍(三)

原文:Flutter Recipes

协议:CC BY-NC-SA 4.0

六、表单小部件

在移动应用中,表单控件对于与用户进行交互非常重要。Flutter 提供了一组用于材质设计和 iOS 风格的表单小部件。这些表单小部件通常没有内部状态。它们的外观和行为完全由构造函数参数定义。有了祖先小部件中维护的状态,表单小部件被重新呈现以反映状态变化。本章介绍了表单小部件的基本用法。

6.1 收集文本输入

问题

您希望收集文本输入。

解决办法

材质设计使用 TextField,iOS 风格使用 CupertinoTextField。

讨论

要在 Flutter 应用中收集用户输入,可以使用 TextField widget 进行材质设计,或者使用 CupertinoTextField widget 进行 iOS 风格设计。这两种小部件具有相似的使用模式和行为。事实上,这两个小部件包装了相同的可编辑文本,该文本提供了基本的文本输入功能,并支持滚动、选择和光标移动。EditableText 是一个高度可定制的小部件,具有许多命名参数。这个菜谱重点介绍如何设置 TextField 或 CupertinoTextField 小部件的初始值,并从中获取文本。

可编辑文本小部件的文本由 TextEditingController 实例控制。创建新的 EditableText 小部件时,可以使用 controller 参数设置 TextEditingController 实例。控制器维护与相应的 EditableText 小部件的双向数据绑定。控制器有一个 text 属性来跟踪当前编辑的文本,还有一个 TextSelection 类型的 selection 属性来跟踪当前选定的文本。每当用户修改或选择 EditableText 小部件中的文本时,关联的 TextEditingController 实例的 text 和 selection 属性都会更新。如果修改 TextEditingController 实例的文本或选择属性,EditableText 小部件将会自我更新。TextEditingController 类是 ValueNotifier 的子类,因此您可以向控制器添加侦听器,以便在文本或选择发生变化时获得通知。创建新的 TextEditingController 实例时,可以用 text 参数传递一些文本,这些文本将成为相应的 EditableText 小部件的初始文本。

让我们看看从 EditableText 小部件获取文本的三种不同方式。

使用 TextEditingController

第一种方式是使用 TextEditingController。清单 6-1 中的 ReverseText 小部件用于反转输入字符串。使用初始文本“<输入>创建 TextEditingController 实例。当按下按钮时,_value 更新为从控制器中检索的文本。将显示反转的字符串。

class ReverseText extends StatefulWidget {
  @override
  _ReverseTextState createState() => _ReverseTextState();
}

class _ReverseTextState extends State<ReverseText> {
  final TextEditingController _controller = TextEditingController(
    text: "<input>",
  );
  String _value;

  @override
  Widget build(BuildContext context) {
    return Column(
      crossAxisAlignment: CrossAxisAlignment.start,
      children: <Widget>[
        Row(
          children: <Widget>[
            Expanded(
              child: TextField(
                controller: _controller,

              ),
            ),
            RaisedButton(
              child: Text('Go'),
              onPressed: () {
                this.setState(() {
                  _value = _controller.text;
                });
              },
            ),
          ],
        ),
        Text( (_value ?? "). split(").reversed.join()),
      ],
    );

  }
}

Listing 6-1Use TextEditingController to get text

图 6-1 显示了清单 6-1 中的代码截图。

img/479501_1_En_6_Fig1_HTML.jpg

图 6-1

使用 TextEditingController

使用 TextEditingController 的侦听器

TextEditingController 实例也是 ValueNotifier 的实例,因此您可以向它添加侦听器并对通知做出反应。在清单 6-2 中,监听器 function _handleTextChanged 在收到变更通知时调用 setState()函数来更新状态。侦听器在 initState()函数中添加,在 dispose()函数中删除,这确保资源得到正确清理。

class ReverseTextWithListener extends StatefulWidget {
  @override
  _ReverseTextWithListenerState createState() =>
      _ReverseTextWithListenerState();
}

class _ReverseTextWithListenerState extends State<ReverseTextWithListener> {
  TextEditingController _controller;
  String _value;

  @override
  void initState() {
    super.initState();
    _controller = TextEditingController(
      text: "<input>",
    );
    _controller.addListener(_handleTextChanged);
  }

  @override
  Widget build(BuildContext context) {
    return Column(
      crossAxisAlignment: CrossAxisAlignment.start,
      children: <Widget>[
        TextField(
          controller: _controller,
        ),
        Text( (_value ?? "). split(").reversed.join()),
      ],
    );
  }

  @override

  void dispose() {
    _controller.removeListener(_handleTextChanged);
    super.dispose();
  }

  void _handleTextChanged() {
    this.setState(() {
      this._value = _controller.text;
    });
  }
}

Listing 6-2Use TextEditingController listener

图 6-2 显示了清单 6-2 中的代码截图。

img/479501_1_En_6_Fig2_HTML.jpg

图 6-2

使用 TextEditingController 侦听器

使用回调

从 EditableText 小部件获取文本的最后一种方法是使用回调。与文本编辑相关的回调有三种类型;见表 6-1 。

表 6-1

可编辑文本回调

|

名字

|

类型

|

描述

| | --- | --- | --- | | onChanged | 值已更改 | 当文本改变时调用。 | | onEditingComplete | 无效回拨 | 当用户提交文本时调用。 | | onSubmitted | 值已更改 | 当用户完成编辑文本时调用。 |

如果你想主动观察文本的变化,你应该使用 onChanged 回调。当用户完成编辑文本时,onEditingComplete 和 onSubmitted 回调都将被调用。区别在于 onEditingComplete 回调不提供对提交文本的访问。

在清单 6-3 中,不同的消息被记录在不同的回调中。所有日志消息都显示在 RichText 小部件中。

class TextFieldCallbacks extends StatefulWidget {
  @override
  _TextFieldCallbacksState createState() => _TextFieldCallbacksState();
}

class _TextFieldCallbacksState extends State<TextFieldCallbacks> {
  List<String> _logs = List();

  void _log(String value) {
    this.setState(() {
      this._logs.add(value);
    });
  }

  @override

  Widget build(BuildContext context) {
    return Column(
      crossAxisAlignment: CrossAxisAlignment.start,
      children: <Widget>[
        TextField(
          onChanged: (text) => _log('changed: $text'),
          onEditingComplete: () => _log('completed'),
          onSubmitted: (text) => _log('submitted: $text'),
        ),
        Text.rich(TextSpan(
          children: this._logs.map((log) => TextSpan(text: '$log\n')).toList(),
        )),
      ],
    );
  }
}

Listing 6-3EditableText callbacks

图 6-3 显示了清单 6-3 中的代码截图。

img/479501_1_En_6_Fig3_HTML.jpg

图 6-3

可编辑文本回调

尽管清单 6-1 、 6-2 和 6-3 中的例子使用了 TextField,但同样的模式也可以应用于 CupertinoTextField。

6.2 自定义文本输入键盘

问题

您想要自定义用于编辑文本的键盘。

解决办法

使用 keyboardType、textInputAction 和 keyboardAppearance 参数。

讨论

EditableText 小部件允许定制用于编辑文本的键盘。可以使用 TextInputType 类的 keyboardType 参数来设置适合文本的键盘类型。例如,如果 EditableText 小部件用于编辑电话号码,那么 TextInputType.phone 是 keyboardType 参数的更好选择。表 6-2 显示了 TextInputType 中的常量。TextInputType.number 常量用于不带小数点的无符号数字。对于其他类型的数字,可以使用 textinputtype . numberwithoptions({ bool signed:false,bool decimal: false })构造函数来设置数字是应该带符号还是应该包含小数点。

表 6-2

TextInputType 常量

|

名字

|

描述

| | --- | --- | | text | 纯文本。 | | multiline | 多行文本。 | | number | 不带小数点的无符号数。 | | phone | 电话号码。 | | datetime | 日期和时间。 | | emailAddress | 电子邮件地址。 | | url | 网址。 |

TextInputAction 枚举类型的 textInputAction 参数设置用户提交文本时要执行的逻辑操作。例如,如果文本字段用于输入搜索查询,那么 TextInputAction.search 值使键盘显示文本“search”。用户可以期望在点击动作按钮之后执行搜索动作。TextInputAction 枚举定义了一组操作。这些操作的按钮在不同平台或同一平台的不同版本上可能有不同的外观。Android 和 iOS 都支持这些操作。它们被映射到 Android 上的 IME 输入类型和 iOS 上的键盘返回类型。表 6-3 显示了 TextInputAction 的值及其在 Android 和 iOS 上的映射。某些操作可能仅在 Android 或 iOS 上受支持。使用不支持的操作将导致在调试模式下引发错误。但是在发布模式下,不支持的动作会分别映射到 Android 上的 IME _ 动作 _ 未指定和 iOS 上的 UIReturnKeyDefault。

表 6-3

TextInputAction 值

|

名字

|

安卓 IME 输入类型

|

iOS 键盘返回类型

| | --- | --- | --- | | none | IME_ACTION_NONE(无) | 不适用的 | | unspecified | IME _ 行动 _ 未指明 | UIReturnKeyDefault 默认 | | done | IME _ 行动 _ 完成 | UIReturnKeyDone | | search | IME _ 行动 _ 搜索 | UIReturnKeySearch | | send | IME _ 行动 _ 发送 | UIReturnKeySend | | next | IME _ 行动 _ 下一步 | UIReturnKeyNext | | previous | IME _ 行动 _ 先前 | 不适用的 | | continueAction | 不适用的 | UIReturnKeyContinue | | join | 不适用的 | UIReturnKeyJoin | | route | 不适用的 | UIReturnKeyRoute | | emergencyCall | 不适用的 | UIReturnKeyEmergencyCall | | newline | IME_ACTION_NONE(无) | UIReturnKeyDefault 默认 |

Brightness 类型的最后一个 keyboardAppearance 参数设置键盘的外观。亮度枚举有两个值,暗和亮。该参数仅用于 iOS。

清单 6-4 显示了 textInputAction 和 last keyboardAppearance 参数的用法。

清单 6-4。键盘类型和键盘外观参数

TextField(
  keyboardType: TextInputType.phone,
)

TextField(
  keyboardType: TextInputType.numberWithOptions(
    signed: true,
    decimal: true,
  ),
)

TextField(
  textInputAction: TextInputAction.search,
  keyboardAppearance: Brightness.dark,
)

6.3 在材质设计中为文本输入添加装饰

问题

您希望在材质设计中为文本字段添加前缀和后缀等装饰。

解决办法

使用 InputDecoration 类型的装饰参数。

讨论

TextField widget 支持添加不同的装饰来向用户呈现各种信息。例如,如果文本输入的值无效,您可以在文本输入下方添加红色边框和一些文本来表明这一点。您还可以添加文本或图标作为前缀或后缀。如果 TextField 小部件用于编辑货币值,您可以添加一个货币符号作为前缀。TextField 的 InputDecoration 类型的修饰参数用于添加此信息。InputDecoration 类有许多命名参数,我们将在接下来查看这些参数。

边界

让我们从给文本输入小部件添加边框开始。InputDecoration 构造函数有几个与边框相关的 InputBorder 类型的参数,包括 errorBorder、disabledBorder、focusedBorder、focusedErrorBorder 和 enabledBorder。这些参数的名称表示这些边界何时会根据状态显示。还有一个边框参数,但是这个参数只用来提供边框的形状。

InputBorder 类是抽象的,因此应该使用它的一个子类 UnderlineInputBorder 或 OutlineInputBorder。UnderlineInputBorder 类只有底边有边框。UnderlineInputBorder 构造函数具有 borderSide 类型的参数 BorderSide 和 borderRadius 类型的参数 BorderRadius。BorderSide 类定义边框一边的颜色、宽度和样式。边框样式由 BorderStyle 枚举定义,其值为 none 和 solid。具有样式 BorderStyle.none 的 BorderSide 将不被呈现。BorderRadius 类为矩形的每个角定义了一组半径。拐角的半径是使用半径类创建的。半径的形状可以是圆形或椭圆形。可以分别使用构造函数 Radius.circular(double radius)和 radius . elliptic(double x,double y)创建圆形或椭圆形半径。BorderRadius 具有 Radius 类型的 topLeft、topRight、bottomLeft 和 bottomRight 属性来表示这四个角的半径。可以使用 BorderRadius.only()为每个角指定不同的 Radius 实例,或者使用 BorderRadius.all()为所有角使用单个 Radius 实例。

OutlineInputBorder 类在小部件周围绘制一个矩形。OutlineInputBorder 构造函数也有参数 borderSide 和 borderRadius。它还具有 gapPadding 参数,用于指定在边框间隙中显示的标签文本的水平填充。

在清单 6-5 中,两个 TextField 小部件都声明了当它们使用 focusedBorder 参数获得焦点时呈现的边框。

TextField(
  decoration: InputDecoration(
    enabledBorder: UnderlineInputBorder(
      borderSide: BorderSide(color: Colors.red),
      borderRadius: BorderRadius.all(Radius.elliptical(5, 10)),
    ),
  ),
)

TextField(
  decoration: InputDecoration(
    labelText: 'Username',
    focusedBorder: OutlineInputBorder(
      borderSide: BorderSide(color: Colors.blue),
      borderRadius: BorderRadius.circular(10),
      gapPadding: 2,
    ),
  ),
)

Listing 6-5Examples of InputDecoration

图 6-4 显示了清单 6-5 中的代码截图。第二个 TextField 被聚焦,因此显示聚焦的边框。

img/479501_1_En_6_Fig4_HTML.jpg

图 6-4

边界

前缀和后缀

文本输入中的前缀和后缀可以提供在编辑文本时有用的信息和动作。前缀和后缀都可以是纯文本或小部件。使用文本时,您可以自定义文本的样式。InputDecoration 构造函数具有参数 prefix、prefixIcon、prefixText 和 prefixStyle 来自定义前缀。它还有参数 suffix、suffixIcon、suffixText 和 suffixStyle 来自定义后缀。不能同时为 prefix 和 prefixText 指定非空值。此限制也适用于后缀和后缀 Text。您只能提供一个小部件或文本,但不能同时提供两者。

TextField(
  decoration: InputDecoration(
    prefixIcon: Icon(Icons.monetization_on),
    prefixText: 'Pay ',
    prefixStyle: TextStyle(fontStyle: FontStyle.italic),
    suffixText: '.00',
  ),
)

Listing 6-6Example of prefix and suffix

图 6-5 是清单 6-6 的截图。

img/479501_1_En_6_Fig5_HTML.jpg

图 6-5

前缀和后缀

文本

您可以添加不同类型的文本作为装饰,并自定义它们的样式。表 6-4 中显示了五种类型的文本。

表 6-4

不同类型的文本

|

类型

|

文本

|

风格

|

描述

| | --- | --- | --- | --- | | 标签 | 标签文本 | 标签样式 | 标签显示在输入字段的上方。 | | 助手 | 帮助文本 | 帮助者风格 | 帮助文本显示在输入字段下方。 | | 暗示 | 提示文本 | dintstyle | 当输入字段为空时,会在其中显示提示。 | | 错误 | error text-错误文字 | 错误类型 | 错误显示在输入字段下方。 | | 计数器 | 对抗文本 | 反风格 | 计数器显示在输入字段的下方,但向右对齐。 |

如果 errorText 值不为空,则输入字段被设置为错误状态。

TextField(
  keyboardType: TextInputType.emailAddress,
  decoration: InputDecoration(
    labelText: 'Email',
    labelStyle: TextStyle(fontWeight: FontWeight.bold),
    hintText: 'Email address for validation',
    helperText: 'For receiving validation emails',
    counterText: '10',
  ),
)

Listing 6-7Example of text

图 6-6 显示了清单 6-7 中的代码截图。

img/479501_1_En_6_Fig6_HTML.jpg

图 6-6

文本字段的文本

6.4 设置文本限制

问题

你想要控制文本的长度。

解决办法

使用 maxLength 参数。

讨论

要设置 TextField 和 CupertinoTextField 中文本的最大长度,可以使用 maxLength 参数。maxLength 参数的默认值为 null,这意味着对字符数没有限制。如果设置了 maxLength 参数,文本输入下方会显示一个字符计数器,显示输入的字符数和允许的字符数。如果 maxLength 参数设置为 TextField.noMaxLength,则只显示输入的字符数。设置 maxLength 时,如果字符数达到限制,则行为取决于 maxLengthEnforced 参数的值。如果 maxLengthEnforced 为 true(默认值),则不能再输入任何字符。如果 maxLengthEnforced 为 false,则可以输入额外的字符,但小部件会切换到错误样式。

TextField(
  maxLength: TextField.noMaxLength,
)

TextField(
  maxLength: 10,
  maxLengthEnforced: false,
)

CupertinoTextField(
  maxLength: 10,
)

Listing 6-8Examples of maxLength

图 6-7 显示了清单 6-8 中两个 TextField 小部件的截图。

img/479501_1_En_6_Fig7_HTML.jpg

图 6-7

文本限制

6.5 选择文本

问题

您希望在文本输入中选择一些文本。

解决办法

使用 TextEditingController 的 selection 属性。

讨论

在 Recipe 6-1 中,你已经看到了使用 TextEditingController 来获取和设置使用 EditableText 的小部件的文本的例子。TextEditingController 也可用于获取用户选择的文本并选择文本。这是通过获取或设置 TextSelection 类型的 selection 属性值来实现的。

TextSelection 是 TextRange 的子类。您可以使用 TextRange.textInside()来获取选定的文本。TextSelection 类使用 baseOffset 和 extentOffset 属性分别表示选定内容的起始和终止位置。baseOffset 的值可能大于、小于或等于 extentOffset。如果 baseOffset 等于 extentOffset,则选定内容将被折叠。折叠的文本选择包含零个字符,但它们用于表示文本插入点。TextSelection.collapsed()构造函数可以在指定的偏移量处创建一个折叠的选择。

在清单 6-9 中,当文本选择改变时,显示选中的文本。第一个按钮选择了[0,5]范围内的文本,而 thp7e 第二个按钮将光标移动到偏移 1。

class TextSelectionExample extends StatefulWidget {
  @override
  _TextSelectionExampleState createState() => _TextSelectionExampleState();
}

class _TextSelectionExampleState extends State<TextSelectionExample> {
  TextEditingController _controller;
  String _selection;

  @override
  void initState() {
    super.initState();
    _controller = new TextEditingController();
    _controller.addListener(_handleTextSelection);
  }

  @override
  void dispose() {
    _controller.removeListener(_handleTextSelection);
    super.dispose();
  }

  @override

  Widget build(BuildContext context) {
    return Column(
      crossAxisAlignment: CrossAxisAlignment.start,
      children: <Widget>[
        TextField(
          controller: _controller,
        ),
        Row(
          children: <Widget>[
            RaisedButton(
              child: Text('Select text [0, 5]'),
              onPressed: () {
                setState(() {
                  _controller.selection =
                      TextSelection(baseOffset: 0, extentOffset: 5);
                });
              },
            ),
            RaisedButton(
              child: Text('Move cursor to offset 1'),
              onPressed: () {
                setState(() {
                  _controller.selection = TextSelection.collapsed(offset: 1);
                });
              },
            ),
          ],
        ),
        Text.rich(TextSpan(
          children: [
            TextSpan(
              text: 'Selected:',
              style: TextStyle(fontWeight: FontWeight.bold),
            ),
            TextSpan(text: _selection ?? "),
          ],
        )),
      ],
    );
  }

  _handleTextSelection() {
    TextSelection selection = _controller.selection;
    if (selection != null) {
      setState(() {
        _selection = selection.textInside(_controller.text);
      });
    }
  }
}

Listing 6-9
Text selection

图 6-8 显示了清单 6-9 中的代码截图。

img/479501_1_En_6_Fig8_HTML.jpg

图 6-8

文本选择

6.6 格式化文本

问题

您想要格式化文本。

解决办法

将 TextInputFormatter 与 EditableText 一起使用。

讨论

当用户键入文本输入时,您可能希望验证和格式化输入的文本。一个常见的要求是删除黑名单中的字符。这是通过提供 TextInputFormatter 实例的列表作为 TextField 和 CupertinoTextField 的 inputFormatters 参数来实现的。

TextInputFormatter 是一个抽象类,只需实现 formatEditUpdate(TextEditingValue old value,TextEditingValue newValue)即可。oldValue 和 newValue 参数分别表示以前的文本和新文本。返回值是另一个表示格式化文本的 TextEditingValue 实例。可以链接 TextInputFormatter 实例。链接时,调用 formatEditUpdate 方法的 oldValue 的值始终是前一个文本,而 newValue 的值是调用链中前一个 TextInputFormatter 实例的 formatEditUpdate 方法的返回值。

TextInputFormatter 已经有三个内置的实现类,如表 6-5 所示。这些类用于实现 TextField 和 CupertinoTextField。例如,当 maxLines 参数的值为 1 时,会将 blacklingtextinputformatter . singleline formatter 添加到 TextInputFormatter 实例的列表中,以过滤掉“\n”字符。

表 6-5

TextInputFormatter 的实现

|

名字

|

描述

| | --- | --- | | LengthLimitingTextInputFormatter | 限制可以输入的字符数。 | | BlacklistingTextInputFormatter | 用给定字符串替换匹配正则表达式模式的字符。 | | WhitelistingTextInputFormatter | 只允许匹配给定正则表达式模式的字符。 |

不需要声明 TextInputFormatter 的新子类,更简单的方法是将 TextInputFormatter . with function()方法与 formatEditUpdate()方法类型匹配的函数一起使用。

在清单 6-10 中,输入文本被格式化为使用大写字母。

TextField(
  inputFormatters: [
    TextInputFormatter.withFunction((oldValue, newValue) {
      return newValue.copyWith(text: newValue.text?.toUpperCase());
    }),

  ],
)

Listing 6-10Format text

6.7 选择单个值

问题

您希望从值列表中选择一个值。

解决办法

使用一组单选小部件。

讨论

单选按钮通常用于需要单项选择的情况。一个组中只能选择一个单选按钮。Radio 类有一个表示值的类型的类型参数 T。创建 Radio 实例时,需要提供必需的参数,包括 value、groupValue 和 onChanged。单选小部件不维护任何状态。它的外观完全由 value 和 groupValue 参数决定。当单选按钮组的选择发生变化时,onChanged listener 将使用所选的值进行调用。表 6-6 显示了无线电构造器的命名参数。

表 6-6

无线电的命名参数

|

名字

|

类型

|

描述

| | --- | --- | --- | | value | T | 此单选按钮的值。 | | groupValue | T | 这组单选按钮的选定值。groupValue 单选按钮处于选中状态。 | | onChanged | ValueChanged<T> | 选择改变时的监听器功能。 | | activeColor | Color | 选中此单选按钮时的颜色。 |

在清单 6-11 中,Fruit.allFruits 变量是所有水果实例的列表。_selectedFruit 是当前选择的水果实例。对于每个水果实例,创建一个单选按钮<水果>小部件,并将 groupValue 设置为 _selectedFruit。

class FruitChooser extends StatefulWidget {
  @override
  _FruitChooserState createState() => _FruitChooserState();
}

class _FruitChooserState extends State<FruitChooser> {
  Fruit _selectedFruit;

  @override
  Widget build(BuildContext context) {
    return Column(
      crossAxisAlignment: CrossAxisAlignment.start,
      children: <Widget>[
        Column(
          children: Fruit.allFruits.map((fruit) {
            return Row(
              children: <Widget>[
                Radio<Fruit>(
                  value: fruit,
                  groupValue: _selectedFruit,
                  onChanged: (value) {
                    setState(() {
                      _selectedFruit = value;

                    });
                  },
                ),
                Expanded(
                  child: Text(fruit.name),
                ),
              ],
            );
          }).toList(),
        ),
        Text(_selectedFruit != null ? _selectedFruit.name : ")
      ],
    );
  }
}

Listing 6-11Example of using Radio

图 6-9 显示了清单 6-11 中示例的截图。

img/479501_1_En_6_Fig9_HTML.jpg

图 6-9

收音机部件

6.8 从下拉列表中选择单个值

问题

您希望从下拉列表中选择一个值。

解决办法

使用下拉按钮。

讨论

一个 DropdownButton 小部件在点击时会显示一个项目列表。DropdownButton 类是泛型的,其类型参数表示值的类型。使用 List < DropdownMenuItem>类型的 items 参数指定项目列表。DropdownMenuItem 小部件是一个简单的包装器,带有值和一个子小部件。当选择被改变时,onChanged 回调将使用所选项的值被调用。选定项的值作为值参数传递。如果值为 null,则显示提示小部件。

在清单 6-12 中,每个水果实例都被映射到一个 DropdownMenuItem 小部件。

class FruitChooser extends StatefulWidget {
  @override
  _FruitChooserState createState() => _FruitChooserState();
}

class _FruitChooserState extends State<FruitChooser> {
  Fruit _selectedFruit;

  @override
  Widget build(BuildContext context) {
    return Column(
      crossAxisAlignment: CrossAxisAlignment.start,
      children: <Widget>[
        DropdownButton(
          value: _selectedFruit,
          items: Fruit.allFruits.map((fruit) {
            return DropdownMenuItem(
              value: fruit,
              child: Text(fruit.name),
            );
          }).toList(),
          onChanged: (fruit) {
            setState(() {
              _selectedFruit = fruit;
            });
          },
          hint: Text('Select a fruit'),
        ),
      ],
    );
  }
}

Listing 6-12Example of DropdownButton

图 6-10 显示了一个展开的下拉按钮的截图。

img/479501_1_En_6_Fig10_HTML.jpg

图 6-10

展开的下拉按钮

6.9 选择多个值

问题

您想要选择多个值。

解决办法

使用复选框小工具。

讨论

复选框通常用于允许多重选择。如果创建复选框时将参数 tristate 设置为 true,则复选框可以显示三个值:true、false 和 null。否则,只允许值 true 和 false。如果该值为空,则显示一个破折号。复选框本身不维护任何状态。它的外观完全由 value 参数决定。当 checkbox 的值改变时,onChanged 回调用新状态的值调用。

在清单 6-13 中,选择的水果被维护在一个清单<水果>实例中。每个水果实例都映射到一个 Checkbox 小部件。复选框的值取决于相应的水果实例是否在 _selectedFruits 列表中。

class FruitSelector extends StatefulWidget {
  @override
  _FruitSelectorState createState() => _FruitSelectorState();
}

class _FruitSelectorState extends State<FruitSelector> {
  List<Fruit> _selectedFruits = List();

  @override
  Widget build(BuildContext context) {
    return Column(
      crossAxisAlignment: CrossAxisAlignment.start,
      children: <Widget>[
        Column(
          children: Fruit.allFruits.map((fruit) {
            return Row(
              children: <Widget>[
                Checkbox(
                  value: _selectedFruits.contains(fruit),
                  onChanged: (selected) {
                    setState(() {
                      if (selected) {
                        _selectedFruits.add(fruit);
                      } else {
                        _selectedFruits.remove(fruit);
                      }
                    });
                  },
                ),
                Expanded(
                  child: Text(fruit.name),
                )
              ],
            );
          }).toList(),
        ),
        Text(_selectedFruits.join(', ')),
      ],
    );
  }
}

Listing 6-13Example of Checkbox

图 6-10 显示了清单 6-13 中示例的截图。

img/479501_1_En_6_Fig11_HTML.jpg

图 6-11

检验盒

6.10 切换开/关状态

问题

您想要切换开/关状态。

解决办法

材质设计使用 Switch,iOS 风格使用 CupertinoSwitch。

讨论

Switch 是一个常用的 UI 控件,用于切换设置的开/关状态。开关部件用于材质设计。开关小部件可以有两种状态,活动和非活动。开关部件本身不维护任何状态。它的行为和外观完全由构造函数参数的值决定。如果 value 参数为真,则开关部件处于活动状态;否则,它处于非活动状态。当 Switch 小部件的开/关状态改变时,调用 onChanged 回调函数,新状态为。您可以使用参数 activeColor、activeThumbImage、activeTrackColor、inactiveThumbColor、inactiveThumbImage 和 inactiveTrackColor 自定义不同状态下的 Switch 小部件的外观。

在清单 6-14 中,Switch 小部件用于控制另一个 TextField 小部件的状态。

class NameInput extends StatefulWidget {
  @override
  _NameInputState createState() => _NameInputState();
}

class _NameInputState extends State<NameInput> {
  bool _useCustomName = false;

  _buildNameInput() {
    return TextField(
      decoration: InputDecoration(labelText: 'Name'),
    );
  }

  _buildToggle() {
    return Row(
      children: <Widget>[
        Switch(
          value: _useCustomName,
          onChanged: (value) {
            setState(() {
              _useCustomName = value;
            });
          },
          activeColor: Colors.green,
          inactiveThumbColor: Colors.grey.shade200,
        ),
        Expanded(
          child: Text('Use custom name'),
        ),
      ],
    );
  }

  @override

  Widget build(BuildContext context) {
    return Column(
      crossAxisAlignment: CrossAxisAlignment.start,
      children: _useCustomName
          ? [_buildToggle(), _buildNameInput()]
          : [_buildToggle()],
    );
  }
}

Listing 6-14Example of Switch

图 6-12 为清单 6-14 中示例的截图。

img/479501_1_En_6_Fig12_HTML.jpg

图 6-12

转换

CupertinoSwitch 小部件创建了一个 iOS 风格的开关,其工作方式与 Switch 相同,但它只支持活动颜色的定制。Switch 小部件具有构造函数 Switch.adaptive()来创建 Switch 小部件或 CupertinoSwitch 小部件,这取决于目标平台。当使用 Switch.adaptive()创建 CupertinoSwitch 小部件时,只使用 CupertinoSwitch()接受的构造函数参数;其他参数被忽略。

清单 6-15 展示了使用 CupertinoSwitch 和 Switch.adaptive()的例子。

CupertinoSwitch(
  value: true,
  onChanged: (value) => {},
  activeColor: Colors.red.shade300,
)

Switch.adaptive(
  value: true,
  onChanged: (value) => {},
)

Listing 6-15Example of CupertinoSwitch

6.11 从一系列值中选择

问题

您希望从一组连续或离散的值中进行选择。

解决办法

使用滑块进行材质设计,或使用 CupertinoSlider 进行 iOS 风格设计。

讨论

滑块通常用于从一组连续或离散的值中进行选择。可以使用 Slider widget 进行材质设计,或者使用 CupertinoSlider 进行 iOS 风格设计。这两个小部件具有相同的行为,但视觉外观不同。创建滑块时,需要使用最小和最大参数提供有效的值范围。如果非空值用于分割参数,将选择一组离散值。否则,将选择连续的值范围。例如,如果“最小值”中的值为 0.0,“最大值”为 10.0,且“分段”设置为 5,则选择的值为 0.0、2.0、4.0、6.0、8.0 和 10.0。滑块小部件不保持任何状态。它的行为和外观完全由构造函数参数决定。当滑块的值发生变化时,onChanged 回调函数将使用选定的值进行调用。还可以使用 onChangeStart 和 onChangeEnd 回调分别在值开始改变和改变完成时获得通知。您可以使用 label、activeColor 和 inactiveColor 进一步自定义滑块的外观。CupertinoSlider 仅支持 activeColor 参数。如果 onChanged 为 null 或范围为空,则 slider 小部件将被禁用。

在清单 6-16 中,使用给定的 divisions 参数值创建一个 Slider 小部件,并显示当前值。

class SliderValue extends StatefulWidget {
  SliderValue({Key key, this.divisions}) : super(key: key);

  final int divisions;

  @override
  _SliderValueState createState() => _SliderValueState(divisions);
}

class _SliderValueState extends State<SliderValue> {
  _SliderValueState(this.divisions);

  final int divisions;
  double _value = 0.0;

  @override
  Widget build(BuildContext context) {
    return Row(
      children: <Widget>[
        Expanded(
          child: Slider(
            value: _value,
            min: 0.0,
            max: 10.0,
            divisions: divisions,
            onChanged: (value) {
              setState(() {
                _value = value;
              });
            },
          ),
        ),
        Text(_value.toStringAsFixed(2)),
      ],
    );
  }
}

Listing 6-16Example of Slider

CupertinoSlider 的用法与 Slider 类似。您可以简单地用清单 6-16 中的 CupertinoSlider 替换 Slider。图 6-13 显示了滑块和 CupertinoSlider 的截图。

img/479501_1_En_6_Fig13_HTML.jpg

图 6-13

滑块和吸盘滑块

6.12 使用芯片

问题

您希望有简洁的替代方案来表示不同类型的实体。

解决办法

使用不同类型的芯片。

讨论

当空间有限时,按钮、单选按钮和复选框等传统小部件可能不适合。在这种情况下,可以使用材质设计中的芯片来表示相同的语义,但使用较少的空间。

芯片小部件是通用的芯片实现,它有一个必需的标签和一个可选的头像。在设置非空 onDeleted 回调时,它还可以包含一个 delete 按钮。

InputChip 小部件比 Chip 小部件更强大。InputChip 小部件可以通过设置 onSelected 回调来选择,也可以通过设置 onPressed 回调来按压。但是,不能对 onSelected 和 onPressed 回调都设置非 null 值。当使用 onSelected 时,InputChip 小部件的行为类似于复选框。您可以使用选定的参数来设置状态。当使用 onPressed 时,InputChip 小部件的行为就像一个按钮。

ChoiceChip 小部件的行为就像一个单选按钮,用选定的参数来设置其状态,用选定的回调来通知状态变化。但是,ChoiceChip 小部件没有与 Radio 小部件中的 groupValue 类似的参数,所以您必须手动设置选择的状态。

FilterChip 小部件的行为类似于复选框。FilterChip 构造函数与 ChoiceChip 构造函数具有相同的参数。

ActionChip 小部件的行为类似于带有 onPressed 参数的按钮。动作芯片和按钮的区别在于,不能通过将 onPressed 参数设置为 null 来禁用动作芯片。如果行动芯片的行动不适用,则应将其移除。这种行为与使用芯片减少空间的目标是一致的。

事实上,所有这些芯片小部件都通过仅使用 RawChip 构造函数支持的参数子集来包装 RawChip 小部件。

在清单 6-17 中,ChoiceChip 小部件用于实现单选。

class FruitChooser extends StatefulWidget {
  @override
  _FruitChooserState createState() => _FruitChooserState();
}

class _FruitChooserState extends State<FruitChooser> {
  Fruit _selectedFruit;

  @override
  Widget build(BuildContext context) {
    return Column(
      crossAxisAlignment: CrossAxisAlignment.start,
      children: <Widget>[
        Wrap(
          spacing: 5,
          children: Fruit.allFruits.map((fruit) {
            return ChoiceChip(
              label: Text(fruit.name),
              selected: _selectedFruit == fruit,
              onSelected: (selected) {
                setState(() {
                  _selectedFruit = selected ? fruit : null;
                });
              },
              selectedColor: Colors.red.shade200,
            );
          }).toList(),

        ),
        Text(_selectedFruit != null ? _selectedFruit.name : ")
      ],
    );
  }
}

Listing 6-17Example of ChoiceChip

在清单 6-18 中,FilterChip 小部件用于实现多重选择。

class FruitSelector extends StatefulWidget {
  @override
  _FruitSelectorState createState() => _FruitSelectorState();
}

class _FruitSelectorState extends State<FruitSelector> {
  List<Fruit> _selectedFruits = List();

  @override
  Widget build(BuildContext context) {
    return Column(
      crossAxisAlignment: CrossAxisAlignment.start,
      children: <Widget>[
        Wrap(
          spacing: 5,
          children: Fruit.allFruits.map((fruit) {
            return FilterChip(
              label: Text(fruit.name),
              selected: _selectedFruits.contains(fruit),
              onSelected: (selected) {
                setState(() {
                  if (selected) {
                    _selectedFruits.add(fruit);
                  } else {
                    _selectedFruits.remove(fruit);
                  }
                });
              },
              selectedColor: Colors.blue.shade200,
            );
          }).toList(),

        ),
        Text(_selectedFruits.join(', ')),
      ],
    );
  }
}

Listing 6-18Example of FilterChip

图 6-14 为清单 6-17 和 6-18 中实例的截图。

img/479501_1_En_6_Fig14_HTML.jpg

图 6-14

选择芯片和滤波器芯片

6.13 选择日期和时间

问题

您想要选择日期和时间。

解决办法

使用 showDatePicker()和 showTimePicker()函数进行材质设计,或使用 CupertinoDatePicker 和 CupertinoTimerPicker 进行 iOS 样式设计。

讨论

对于材质设计,可以使用 YearPicker、MonthPicker、DayPicker 或 showDatePicker()函数等小部件来允许用户选择日期。showTimePicker()函数用于选择时间。小部件很少用于选择日期。大多数情况下,showDatePicker()和 showTimePicker()函数用于显示对话框。

YearPicker 小部件显示了要选择的年份列表。创建 YearPicker 小部件时,需要分别使用 selected date、firstDate 和 lastDate 参数为所选日期、最早日期和最晚日期提供 DateTime 实例。当选择被更改时,onChanged 回调将使用选定的 DateTime 实例调用。

MonthPicker 小部件显示要选择的月份列表。MonthPicker 构造函数与 YearPicker 具有相同的参数 selectedDate、firstDate、lastDate 和 onChanged。它还有一个谓词函数 selectableDayPredicate,用于定制哪些天是可选的。

DayPicker 小部件显示给定月份中要选择的日期。DayPicker 构造函数具有 MonthPicker 的所有参数和 displayedMonth 参数,用于设置要选取的月份。

如果你想显示一个对话框让用户选择日期,showDatePicker()函数比创建你自己的对话框更容易使用。您需要为参数 initialDate、firstDate 和 lastDate 传递 DateTime 实例。BuildContext 类型的上下文参数也是必需的。该函数可以在 DatePickerMode 枚举中定义的两种模式下工作。DatePickerMode.day 表示每天选择一个月,DatePickerMode.year 表示选择一年。showDatePicker()函数的返回值是代表所选日期的未来日期。

在清单 6-19 中,TextField 小部件有一个 IconButton 作为后缀。当按钮被按下时,showDatePicker()函数被调用以显示日期选择器对话框。选定的日期显示在 TextField 小工具中。

class PickDate extends StatefulWidget {
  @override
  _PickDateState createState() => _PickDateState();
}

class _PickDateState extends State<PickDate> {
  DateTime _selectedDate = DateTime.now();
  TextEditingController _controller = TextEditingController();

  @override
  Widget build(BuildContext context) {
    return TextField(
      controller: _controller,
      decoration: InputDecoration(
        labelText: 'Date',
        suffix: IconButton(
          icon: Icon(Icons.date_range),
          onPressed: () {
            showDatePicker(
              context: context,
              initialDate: _selectedDate,
              firstDate: DateTime.now().subtract(Duration(days: 30)),
              lastDate: DateTime.now().add(Duration(days: 30)),)
            .then((selectedDate) {
              if (selectedDate != null) {
                _selectedDate = selectedDate;
                _controller.text = DateFormat.yMd().format(_selectedDate);
              }
            });
          },
        ),
      ),
    );
  }
}

Listing 6-19Pick date

函数的作用是:显示一个对话框来选择时间。您需要传递 TimeOfDay 类型的 initialTime 参数作为要显示的初始时间。返回值是代表所选时间的未来实例。清单 6-20 中的代码使用与清单 6-19 相似的模式来显示时间选择器对话框。

class PickTime extends StatefulWidget {
  @override
  _PickTimeState createState() => _PickTimeState();
}

class _PickTimeState extends State<PickTime> {
  TimeOfDay _selectedTime = TimeOfDay.now();
  TextEditingController _controller = TextEditingController();

  @override
  Widget build(BuildContext context) {
    return TextField(
      controller: _controller,
      decoration: InputDecoration(
          labelText: 'Time',
          suffix: IconButton(
            icon: Icon(Icons.access_time),
            onPressed: () {
              showTimePicker(
                context: context,
                initialTime: _selectedTime,
              ).then((selectedTime) {
                if (selectedTime != null) {
                  _selectedTime = selectedTime;
                  _controller.text = _selectedTime.format(context);
                }
              });
            },
          )),
    );
  }
}

Listing 6-20Pick time

对于 iOS 风格,您可以使用 CupertinoDatePicker 和 CupertinoTimerPicker 小部件分别选择日期和时间。根据枚举 CupertinoDatePickerMode 的模式参数,CupertinoDatePicker 可以有不同的模式,包括日期、时间以及日期和时间。与材质设计中的小部件类似,CupertinoDatePicker 构造函数具有参数 initialDateTime、minimumDate、maximumDate 和 onDateTimeChanged。根据 enum CupertinoTimerPickerMode 的 mode 参数,CupertinoTimerPicker 还可以有不同的模式,包括 hm、ms 和 hms。不同之处在于,CupertinoTimerPicker 使用 Duration 实例来设置初始值和作为 onTimerDurationChanged 回调中的值。

6.14 包装表单字段

问题

您希望将表单小部件包装成表单字段。

解决办法

使用 FormField 或 TextFormField。

讨论

表单小部件可以像普通小部件一样使用。但是,这些表单小部件不维护任何状态;您总是需要将它们包装在有状态的小部件中来保持状态。典型的使用模式是使用 onChanged 回调来更新状态并触发表单小部件的重建。因为这是使用表单小部件的典型模式,所以 Flutter 有一个内置的 FormField 小部件来维护表单小部件的当前状态。它处理更新和验证错误。

FormField 类是泛型的,类型参数 T 表示值的类型。FormField 可以用作独立的小部件,也可以作为表单小部件的一部分。本食谱仅讨论独立使用。表 6-7 显示了 FormField 构造函数的命名参数。

表 6-7

表单域的命名参数

|

名字

|

类型

|

描述

| | --- | --- | --- | | builder | FormFieldBuilder<T> | 构建表示该表单字段的小部件。 | | onSaved | FormFieldSetter<T> | 保存表单时回调。 | | validator | FormFieldValidator<T> | 表单域的验证器。 | | initialValue | T | 初始值。 | | autovalidate | boolean | 每次更改后是否自动验证。 | | enabled | boolean | 此表单域是否已启用。 |

FormFieldBuilder 类型是一个小部件形式的 typedef(formfield state字段)。FormFieldState 类从 State 类扩展而来,代表表单域的当前状态。FormFieldBuilder 负责根据状态构建小部件。从 FormFieldState 中,可以获得表单域的当前值和错误文本。也可以使用表 6-8 中 FormFieldState 的方法。FormFieldValidator < T >也是字符串(T 值)形式的 typedef。它将当前值作为输入,如果验证失败,则返回一个非空字符串作为错误消息。FormFieldSetter < T > type 是一个 void(T newValue)形式的 typedef。

表 6-8

FormFieldState

|

名称

|

描述

| | --- | --- | | save() | 用当前值调用 onSaved()方法。 | | validate() | 如果验证失败,调用验证器并设置 errorText。 | | didChange(T value) | 将字段的状态更新为新值。 | | reset() | 将字段重置为初始值。 |

当在 FormFields 中包装 TextFields 时,最好使用内置的 TextFormField。TextFormField 小部件已经使用 TextEditingController 处理设置文本,并使用 FormFieldValidator 返回的错误文本更新输入装饰。TextFormField 构造函数支持来自 TextField 和 FormField 构造函数的参数。清单 6-21 中的 TextFormField 有一个验证器来验证文本长度。

class NameInput extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return TextFormField(
      decoration: InputDecoration(
        labelText: 'Name',
      ),
      validator: (value) {
        if (value == null || value.isEmpty) {
          return 'Name is required.';
        } else if (value.length < 6) {
          return 'Minimum length is 6.';
        } else {
          return null;
        }
      },
      autovalidate: true,
    );
  }
}

Listing 6-21
TextFormField

图 6-15 显示了清单 6-21 中的代码截图。

img/479501_1_En_6_Fig15_HTML.jpg

图 6-15

TextFormField

FormFieldState 实例只能在 FormField 的 builder 函数中访问。如果需要从其他地方访问状态,可以传递一个 GlobalKey 作为 FormField 的 Key 参数,然后使用 currentState 属性访问当前状态。

在清单 6-22 中,FormField 的状态是一个列表< PizzaTopping >实例。使用 GlobalKey,当按下按钮时,可以检索当前值。

class PizzaToppingsSelector extends StatelessWidget {
  final GlobalKey<FormFieldState<List<PizzaTopping>>> _formFieldKey =
      GlobalKey();

  @override
  Widget build(BuildContext context) {
    return Column(
      children: <Widget>[
        FormField<List<PizzaTopping>>(
          key: _formFieldKey,
          initialValue: List(),
          builder: (state) {
            return Wrap(
              spacing: 5,
              children: PizzaTopping.allPizzaToppings.map((topping) {
                return ChoiceChip(
                  label: Text(topping.name),
                  selected: state.value.contains(topping),
                  onSelected: state.value.length < 2 ||
                          state.value.contains(topping)
                      ? (selected) {
                          List<PizzaTopping> newValue = List.of(state.value);
                          if (selected) {
                            newValue.add(topping);
                          } else {
                            newValue.remove(topping);
                          }
                          state.didChange(newValue);
                        }
                      : null,

                );
              }).toList(),
            );
          },
        ),
        RaisedButton(
          child: Text('Get toppings'),
          onPressed: () => print(_formFieldKey.currentState?.value),
        ),
      ],
    );
  }
}

Listing 6-22FormField

6.15 创建表单

问题

您希望创建一个包含多个表单域的表单。

解决办法

使用表单。

讨论

使用表单域时,通常您会尝试构建一个包含多个表单域的表单。当处理多个表单域时,单独管理表单域是一项繁琐的任务。Form 是多个表单域的方便包装器。您需要将所有表单域包装在表单域小部件中,并使用一个表单小部件作为所有这些表单域小部件的共同祖先。表单小部件是一个有状态的小部件,其状态由关联的 FormState 实例管理。FormState 类有 save()、validate()和 reset()方法。这些方法调用后代 FormField 小部件的所有 FormFieldState 实例上的相应函数。

有两种方法可以获得 FormState 实例,这取决于小部件想要使用 FormState 的位置。如果小部件是 Form 小部件的后代,使用 Form.of(BuildContext context)是获得最接近的 FormState 实例的简单方法。第二种方法是在创建表单小部件时使用 GlobalKey 实例,然后使用 GlobalKey.currentState 获取表单状态。

清单 6-23 显示了一个登录表单的代码。使用 GlobalKey 实例创建了两个 TextFormField 小部件。

class LoginForm extends StatefulWidget {
  @override
  _LoginFormState createState() => _LoginFormState();
}

class _LoginFormState extends State<LoginForm> {
  final GlobalKey<FormFieldState<String>> _usernameFormFieldKey = GlobalKey();
  final GlobalKey<FormFieldState<String>> _passwordFormFieldKey = GlobalKey();

  _notEmpty(String value) => value != null && value.isNotEmpty;

  get _value => ({
        'username': _usernameFormFieldKey.currentState?.value,
        'password': _passwordFormFieldKey.currentState?.value
      });

  @override

  Widget build(BuildContext context) {
    return Form(
      child: Column(
        children: <Widget>[
          TextFormField(
            key: _usernameFormFieldKey,
            decoration: InputDecoration(
              labelText: 'Username',
            ),
            validator: (value) =>
                !_notEmpty(value) ? 'Username is required' : null,
          ),
          TextFormField(
            key: _passwordFormFieldKey,
            obscureText: true,
            decoration: InputDecoration(
              labelText: 'Password',
            ),
            validator: (value) =>
                !_notEmpty(value) ? 'Password is required' : null,
          ),
          Builder(builder: (context) {
            return Row(
              mainAxisAlignment: MainAxisAlignment.end,
              children: <Widget>[
                RaisedButton(
                  child: Text('Log In'),
                  onPressed: () {
                    if (Form.of(context).validate()) {
                      print(_value);
                    }
                  },
                ),
                FlatButton(
                  child: Text('Reset'),
                  onPressed: () => Form.of(context).reset(),
                )
              ],
            );
          }),
        ],
      ),
    );
  }
}

Listing 6-23
Login form

图 6-16 显示了登录表单的截图。

img/479501_1_En_6_Fig16_HTML.jpg

图 6-16

登录表单

6.16 摘要

表单小部件对于与用户交互很重要。本章涵盖了材质设计和 iOS 风格的表单小部件,包括文本输入、单选按钮、复选框、下拉菜单、开关、芯片和滑块。在下一章,我们将讨论应用脚手架的部件。

七、常见小部件

在 Flutter 应用中,一些小部件被广泛用于不同的目的。本章讨论一些常见的小部件。

7.1 显示项目列表

问题

您希望显示一个可滚动的项目列表。

解决办法

使用 ListView 小部件作为项目的容器。

讨论

像 Flex、Row 和 Column 这样的 Flutter 布局小部件不支持滚动,并且这些小部件不是设计用来在需要滚动时显示项目的。如果你想显示大量的条目,你应该使用 ListView 小部件。您可以将 ListView 视为 Flex 小部件的可滚动对应物。

使用不同的构造函数创建 ListView 小部件有三种不同的方法:

  • 从子部件的静态列表中创建。

  • 通过基于滚动位置按需构建子项来创建。

  • 创建自定义实现。

    这个食谱着重于前两种方法。

带有静态子视图的 ListView

如果您有一个可能超过其父小部件大小的子部件静态列表,您可以将它们包装在一个 ListView 小部件中以支持滚动。这是通过使用 Widget[]类型的 children 参数调用 ListView()构造函数来实现的。滚动方向由 Axis 类型的 scrollDirection 参数确定。默认的滚动方向是 Axis.vertical。如果要以相反的顺序显示子项,可以将 reverse 参数设置为 true。清单 7-1 显示了一个带有三个子控件的 ListView 小部件。

ListView(
  children: <Widget>[
    ExampleWidget(name: 'Box 1'),
    ExampleWidget(name: 'Box 2'),
    ExampleWidget(name: 'Box 3'),
  ],
)

Listing 7-1ListView with static children

默认的 ListView()构造函数应该只在你有少量孩子的时候使用。将创建所有子对象,即使其中一些子对象在视口中不可见。这可能会对性能产生影响。

带有项目生成器的 ListView

如果你有大量的条目或者条目需要动态创建,你可以使用 ListView.builder()和 ListView.separated()构造函数。您需要提供 IndexedWidgetBuilder 类型的构建器函数来按需构建项目,而不是静态的小部件列表。IndexedWidgetBuilder 是小部件的 typedef(build context 上下文,int index)。index 参数是要生成的项的索引。ListView 小部件确定视窗中项目的索引,并调用构建器函数来构建要呈现的项目。如果项目总数是已知的,您应该将这个数字作为 itemCount 参数传递。如果 itemCount 为非空,则只在索引大于或等于零且小于 itemCount 的情况下调用构建器函数。如果 itemCount 为 null,则构建器函数需要返回 null,以表明没有更多的项目可用。

使用 ListView.builder()构造函数时,只需要提供 IndexedWidgetBuilder 类型的 itemBuilder 参数。对于 ListView.separated()构造函数,除了 itemBuilder 参数之外,还需要提供 IndexedWidgetBuilder 类型的 separatorBuilder 参数来构建项之间的分隔符。使用 ListView.separated()时,itemCount 参数是必需的。清单 7-2 展示了使用 ListView.builder()和 ListView.separated()的例子。

ListView.builder(
  itemCount: 20,
  itemBuilder: (context, index) {
    return ExampleWidget(name: 'Dynamic Box ${index + 1}');
  },
);

ListView.separated(
  itemBuilder: (context, index) {
    return ExampleWidget(name: 'Separated Box ${index + 1}');
  },
  separatorBuilder: (context, index) {
    return Divider(
      height: 8,
    );

  },
  itemCount: 20,
);

Listing 7-2ListView with item builders

如果项在滚动方向上的范围是已知的,则应将该值作为 itemExtent 参数传递。itemExtent 参数的非空值使滚动更有效。

listfile(列表文件)

您可以使用任何小部件作为 ListView 的子部件。如果你的项目包括文本、图标和其他控件,你可以使用 ListTile 及其子类。列表框包含一到三行文本以及文本周围的前导和尾随小部件。表 7-1 显示

表 7-2

CheckboxListTile 参数

|

名字

|

类型

|

描述

| | --- | --- | --- | | 副手 | 小部件 | 显示在磁贴另一侧的小部件。 | | 控制亲和力 | ListTileControlAffinity | 在图块中放置控件的位置。 |

表 7-1

列表文件的参数

|

名字

|

类型

|

描述

| | --- | --- | --- | | 标题 | 小部件 | 列表框的标题。 | | 小标题 | 小部件 | 标题下方显示的可选内容。 | | 是三线 | 弯曲件 | 列表框是否有三行文本。 | | 主要的 | 小部件 | 标题前显示的小部件。 | | 蔓延的 | 小部件 | 标题后显示的小部件。 | | 使能够 | 弯曲件 | 列表框是否已启用。 | | 挑选 | 弯曲件 | 列表框是否被选中。选中时,图标和文本以相同的颜色呈现。 | | 数据库 | GestureTapCallback | 点击标题时回调。 | | 又没有长按 | gesturelongpressscallback | 长按标题时回调。 | | 稠密的 | 弯曲件 | 为 true 时,平铺的大小会减小。 | | 内容填充 | 边缘镶嵌几何学 | 瓷砖内部的填充。 |

清单 7-3 展示了一个使用 ListTile 的例子。

ListTile(
  title: Text('Title'),
  subtitle: Text('Description'),
  leading: Icon(Icons.shop),
  trailing: Icon(Icons.arrow_right),
)

Listing 7-3Example of ListTile

如果您想在列表框中有一个复选框,您可以使用 checkboxListTile 小部件,它结合了 list tile 和 Checkbox。CheckboxListTile 构造函数与 ListTile 构造函数具有相同的参数 title、subtitle、isThreeLine、selected 和 dense。它还有用于复选框构造函数的参数 value、onChanged 和 activeColor。

ListTileControlAffinity 枚举定义列表框中控件的位置。它有三个值,前导、尾随和平台。当指定了控件的位置时,辅助小部件总是放在对面。

class CheckboxInListTile extends StatefulWidget {
  @override
  _CheckboxInListTileState createState() => _CheckboxInListTileState();
}

class _CheckboxInListTileState extends State<CheckboxInListTile> {
  bool _value = false;

  @override
  Widget build(BuildContext context) {
    return CheckboxListTile(
      title: Text('Checkbox'),
      subtitle: Text('Description'),
      value: _value,
      onChanged: (value) {
        setState(() {
          _value = value;
        });
      },
      secondary: Icon(_value ? Icons.monetization_on : Icons.money_off),
    );
  }
}

Listing 7-4Example of CheckboxListTile

如果想在列表框中添加单选按钮,可以使用 RadioListTile 小部件。对于 RadioListTile 构造函数的参数,value、groupValue、onChanged 和 activeColor 与 Radio 构造函数中的含义相同;title、subtitle、isThreeLine、dense、secondary、selected 和 controlAffinity 与 CheckboxListTile 构造函数中的含义相同。清单 7-5 显示了一个使用放射性同位素的例子。

enum CustomColor { red, green, blue }

class RadioInListTile extends StatefulWidget {
  @override
  _RadioInListTileState createState() => _RadioInListTileState();
}

class _RadioInListTileState extends State<RadioInListTile> {
  CustomColor _selectedColor;

  @override
  Widget build(BuildContext context) {
    return Column(
      children: CustomColor.values.map((color) {
        return RadioListTile<CustomColor>(
          title: Text(color.toString()),
          value: color,
          groupValue: _selectedColor,
          onChanged: (value) {
            setState(() {
              _selectedColor = value;
            });
          },
        );
      }).toList(),
    );
  }
}

Listing 7-5Example of RadioListTile

如果您想将开关添加到列表框中,您可以使用 SwitchListTile。SwitchListTile 构造函数的一些参数来自 Switch 构造函数,另一些参数来自 ListTile 构造函数。清单 7-6 展示了一个使用 SwitchListTile 的例子。

class SwitchInListTile extends StatefulWidget {
  @override
  _SwitchInListTileState createState() => _SwitchInListTileState();
}

class _SwitchInListTileState extends State<SwitchInListTile> {
  bool _value = false;

  @override
  Widget build(BuildContext context) {
    return SwitchListTile(
      title: Text('Switch'),
      subtitle: Text('Description'),
      value: _value,
      onChanged: (value) {
        setState(() {
          _value = value;
        });
      },
    );
  }
}

Listing 7-6Example of SwitchListTile

图 7-1 显示了不同 ListTiles 的截图。

img/479501_1_En_7_Fig1_HTML.jpg

图 7-1

列表文件

7.2 在网格中显示项目

问题

您希望在网格中显示项目。

解决办法

使用 GridView。

讨论

ListView 小工具以线性数组的形式显示项目。要在二维数组中显示小部件,可以使用 GridView。GridView 子级的实际布局被委托给 SliverGridDelegate 的一个实现。Flutter 提供了 SliverGridDelegate 的两个内置实现,slivergriddelegatewithfixedcrosaxiscount 和 slivergriddelegatewithmxcrosaxisextent。您还可以创建自己的 SliverGridDelegate 实现。

有三种方法可以提供 GridView 的子视图。您可以提供一个静态的小部件列表,或者使用 IndexedWidgetBuilder 类型的构建器函数,或者提供 SliverChildDelegate 的实现。

根据 SliverGridDelegate 和提供子级的选择,可以使用不同的 GridView 构造函数。表 7-3 显示了不同构造器的用法。

表 7-3

GridView 构造函数

|

名字

|

代表

|

孩子们

| | --- | --- | --- | | GridView() | silvergriddelegate | 小部件[] | | GridView.builder() | silvergriddelegate | IndexedWidgetBuilder | | GridView.count() | slivergriddelegatewithfixedcrosaxiscount | 小部件[] | | GridView.extent() | silvergriddelegatewithmxcrosaxisextent | 小部件[] | | GridView.custom() | silvergriddelegate | silverchilddelegate |

slivergriddelegatewithfixedcrosaxiscount 类使用 CrossAxisCount 参数来指定横轴中的固定平铺数。例如,如果 GridView 的滚动方向是垂直的,则 crossAxisCount 参数指定列数。清单 7-7 展示了一个使用 GridView.count()创建三列网格的例子。

GridView.count(
  crossAxisCount: 3,
  children: List.generate(10, (index) {
    return ExampleWidget(
      name: 'Fixed Count ${index + 1}',
    );
  }),
);

Listing 7-7Example of using Gridview.count()

slivergriddelegatewithmacrossaxisextent 类使用 maxCrossAxisExtent 参数指定横轴的最大范围。图块的实际横轴范围将尽可能大,以均匀划分 GridView 的横轴范围,并且不会超过指定的最大值。例如,如果 GridView 的横轴范围是 400,maxCrossAxisExtent 的值是 120,则图块的横轴范围是 100。如果 GridView 的滚动方向是垂直的,它将有四列。清单 7-8 展示了一个使用 GridView.extent()的例子。

GridView.extent(
  maxCrossAxisExtent: 250,
  children: List.generate(10, (index) {
    return ExampleWidget(
      name: 'Max Extent ${index + 1}',
    );
  }),
);

Listing 7-8Example of using GridView.extent()

要使用 builder 函数创建子级,需要使用 GridView.builder()构造函数和 SliverGridDelegate 实现。清单 7-9 展示了一个使用 GridView.builder()和 slivergriddelegatewithfixedcrosaxiscount 的例子。

GridView.builder(
  itemCount: 32,
  gridDelegate:
      SliverGridDelegateWithFixedCrossAxisCount(crossAxisCount: 3),
  itemBuilder: (context, index) {
    return ExampleWidget(
      name: 'Builder ${index + 1}',
    );
  },
);

Listing 7-9Example of using GridView.builder()

slivergriddelegatewithfixedcrosaxiscount 和 SliverGridDelegateWithMaxCrossAxisExtent 类都有其他命名参数来配置布局;参见表 7-4 。

表 7-4

内置 SliverGridDelegate 实现的参数

|

名字

|

类型

|

描述

| | --- | --- | --- | | 主轴空间 | 两倍 | 瓷砖沿主轴的间距。 | | 交叉轴间距 | 两倍 | 瓷砖沿横轴的间距。 | | 儿童保护 | 两倍 | 切片横轴与主轴范围的比率。 |

使用这两个 SliverGridDelegate 实现时,首先确定每个图块的横轴范围,然后由 childAspectRatio 参数确定主轴范围。如果 GridView 用于显示具有所需纵横比的图像,则可以使用与 childAspectRatio 参数的值相同的纵横比。GridView.count()和 GridView.extent()构造函数在表 7-4 中具有相同的命名参数,以将这些参数传递给底层 SliverGridDelegate 实现。清单 7-10 显示了显示图像时使用 childAspectRatio 参数的示例。

GridView.count(
  crossAxisCount: 3,
  childAspectRatio: 4 / 3,
  children: List.generate(10, (index) {
    return Image.network('https://picsum.photos/400/300');
  }),
);

Listing 7-10Using childAspectRatio parameter

就像在 ListView 中使用 ListTiles 一样,在 GridView 中也可以使用 GridTiles。grid tile 有一个必需的子部件和可选的 header 和 footer 部件。对于 grid tiles 的页眉和页脚,通常使用 GridTileBar 小部件。GridTileBar 与 ListTile 类似。GridTileBar 构造函数有 title、subtitle、leading、trailing 和 backgroundColor 参数。

GridView.count(
  crossAxisCount: 2,
  children: <Widget>[
    GridTile(
      child: ExampleWidget(name: 'Simple'),
    ),
    GridTile(
      child: ExampleWidget(name: 'Header & Footer'),
      header: GridTileBar(
        title: Text('Header'),
        backgroundColor: Colors.red,
      ),
      footer: GridTileBar(
        title: Text('Footer'),
        subtitle: Text('Description'),
        backgroundColor: Colors.blue,
      ),
    )
  ],
);

Listing 7-11Example of GridTile and GridTileBar

图 7-2 显示了清单 7-11 中的代码截图。

img/479501_1_En_7_Fig2_HTML.png

图 7-2

GridTile 和 GridTileBar

7.3 显示表格数据

问题

您希望显示表格数据或对孩子使用表格布局。

解决办法

使用表格小部件。

讨论

如果您想显示表格数据,使用数据表是一个自然的选择。表格也可以用于布局目的,以组织孩子。对于这两种使用场景,您可以使用表格小部件。

表格小部件可以有多行。表格行用 table row 小部件表示。表格小部件构造函数有 List 类型的子参数来提供行列表。TableRow 构造函数也有 List 类型的 children 参数来提供该行中的单元格列表。表中的每一行都必须有相同数量的子代。

表格的边框是使用 TableBorder 类定义的。TableBorder 与 Border 类似,但 TableBorder 有两条额外的边:

  • horizontal inside–行与行之间的内部水平边框

  • vertical inside–列之间的内部垂直边框

清单 7-12 显示了一个三行四列的简单表格的例子。

Table(
  border: TableBorder.all(color: Colors.red.shade200),
  children: [
    TableRow(children: [Text('A'), Text('B'), Text('C'), Text('D')]),
    TableRow(children: [Text('E'), Text('F'), Text('G'), Text('H')]),
    TableRow(children: [Text('I'), Text('J'), Text('K'), Text('L')]),
  ],
);

Listing 7-12Simple table

表中列的宽度由 TableColumnWidth 实现配置。类型 Map 的 columnWidths 参数定义了列索引与其 TableColumnWidth 实现之间的映射。表 7-5 显示了内置的 TableColumnWidth 实现。MinColumnWidth 和 MaxColumnWidth 类结合了其他 TableColumnWidth 实现。如果没有为列找到 TableColumnWidth 实现,则使用 defaultColumnWidth 参数来获取默认的 TableColumnWidth 实现。defaultColumnWidth 的默认值是 FlexColumnWidth(1.0),这意味着所有列共享相同的宽度。

表 7-5

表列宽实现

|

名称

|

性能

|

描述

| | --- | --- | --- | | 固定列宽 | 高 | | flex column width | Medium | 一旦调整完所有其他非灵活列的大小,就使用伸缩因子来划分剩余空间。 | | FractionColumnWidth | Medium | 使用表格最大宽度的一部分作为列宽。 | | IntrinsicColumnWidth | Low | 使用一列中所有单元格的内在尺寸来确定列宽。 | | min column width | | 最小的两个 TableColumnWidth 对象。 | | max column width | | 最多两个 TableColumnWidth 对象。 |

清单 7-13 显示了一个具有不同列宽的表格示例。

Table(
  border: TableBorder.all(color: Colors.blue.shade200),
  columnWidths: {
    0: FixedColumnWidth(100),
    1: FlexColumnWidth(1),
    2: FlexColumnWidth(2),
    3: FractionColumnWidth(0.2),
  },
  children: [
    TableRow(children: [Text('A'), Text('B'), Text('C'), Text('D')]),
    TableRow(children: [Text('E'), Text('F'), Text('G'), Text('H')]),
    TableRow(children: [Text('I'), Text('J'), Text('K'), Text('L')]),
  ],
);

Listing 7-13Table with different column width

单元格的垂直对齐是用 TableCellVerticalAlignment 枚举的值配置的。TableCellVerticalAlignment 枚举具有值 top、middle、bottom、baseline 和 fill。表构造函数的 defaultVerticalAlignment 参数指定默认的 TableCellVerticalAlignment 值。如果希望自定义单个单元格的垂直对齐,可以将单元格小部件包装在 TableCell 小部件中,并指定 vertical alignment 参数。清单 7-14 展示了一个为单元格指定垂直对齐的例子。

class VerticalAlignmentTable extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Table(
      border: TableBorder.all(color: Colors.green.shade200),
      defaultVerticalAlignment: TableCellVerticalAlignment.bottom,
      children: [
        TableRow(children: [
          TextCell('A'),
          TableCell(
            verticalAlignment: TableCellVerticalAlignment.middle,
            child: Text('B'),
          ),
          Text('C'),
          Text('D'),
        ]),
        TableRow(children: [Text('E'), Text('F'), Text('G'), Text('H')]),
        TableRow(children: [Text('I'), Text('J'), Text('K'), Text('L')]),
      ],
    );
  }
}

class TextCell extends StatelessWidget {
  TextCell(this.text, {this.height = 50});

  final String text;
  final double height;

  @override
  Widget build(BuildContext context) {
    return ConstrainedBox(
      constraints: BoxConstraints(
        minHeight: height,
      ),
      child: Text(text),
    );
  }
}

Listing 7-14Vertical alignment of table cells

图 7-3 为不同表格的截图。

img/479501_1_En_7_Fig3_HTML.png

图 7-3

桌子

7.4 脚手架材质设计页面

问题

你要脚手架材质设计页面。

解决办法

使用脚手架和其他相关部件。

讨论

材质设计应用具有通用的布局结构。Scaffold 小部件将其他常见的小部件放在一起,创建基本的页面结构。表 7-6 显示了可以包含在 Scaffold 小部件中的元素。指定为 drawer 和 endDrawer 的小部件最初是隐藏的,可以通过滑动来显示。滑动方向取决于文本方向。drawer 小部件使用与文本方向相同的方向,而 endDrawer 小部件使用相反的方向。例如,如果文本方向是从左到右,则通过从左到右滑动来打开抽屉小部件,通过从右到左滑动来打开 endDrawer 小部件。

表 7-6

脚手架元件

|

参数

|

小部件

|

描述

| | --- | --- | --- | | 打电话给我 | 打电话给我 | 显示在顶部的应用栏。 | | 浮动操作按钮 | 浮动操作按钮 | 一个按钮浮在身体上方的右下角。 | | 抽屉 | 抽屉 | 显示在机身侧面的隐藏面板。 | | 抽屉末端 | 抽屉 | 显示在机身侧面的隐藏面板。 | | 底部导航栏 | BottomAppBar 底部导航栏 | 导航栏显示在底部。 | | 底板 | 底板 | 持久的底层。 | | persistentFooterButtons | 列表 | 显示在底部的一组按钮。 | | 身体 | 小部件 | 主要内容。 |

表 7-6 中的第二列仅列出了这些元素的首选小部件类型。Scaffold 构造器实际上接受任何类型的小部件。例如,您可以使用 ListView 小部件作为抽屉。然而,这些首选的小部件更合适。

App Bar(应用栏)

AppBar 小工具显示当前屏幕的基本信息。它由一个工具栏和其他小部件组成。表 7-7 显示了 AppBar 小部件的元素。这些元素也是 AppBar 构造函数的命名参数。

表 7-7

AppBar 的参数

|

名字

|

描述

| | --- | --- | | 标题 | 工具栏中的主要小部件。 | | 主要的 | 在标题前显示的小工具。 | | 行动 | 标题后显示的小部件列表。 | | 底部 | 显示在底部的小部件。 | | 灵活的空间 | 要堆叠在工具栏和底部后面的小工具。 |

如果前导小部件为空,并且 automaticallyImplyLeading 参数为真,则从状态中推导出实际的前导小部件。如果脚手架有一个抽屉,那么主要的小部件是一个打开抽屉的按钮。如果最近的导航器有以前的路线,领先的小部件是返回到以前路线的后退按钮。

动作列表中的小部件通常是图标按钮。如果没有足够的空间来放置这些图标按钮,您可以使用 PopupMenuButton 作为最后一个操作,并将其他操作放在弹出菜单中。TabBar 小部件通常用作底部小部件。清单 7-15 展示了一个使用 AppBar 的例子。

AppBar(
  title: Text('Scaffold'),
  actions: <Widget>[
    IconButton(
      icon: Icon(Icons.search),
      onPressed: () {},
    ),
  ],
);

Listing 7-15Example of AppBar

浮动操作按钮

FloatingActionButton 小部件是一种特殊的按钮,用于提供对主要操作的快速访问。浮动操作按钮是一个圆形图标,通常显示在屏幕的右下角。在 Gmail 应用中,电子邮件列表屏幕有一个浮动的操作按钮,用于编写新邮件。

有两种浮动操作按钮。使用 FloatingActionButton()构造函数时,只需要提供子 widget 和 onPressed 回调。使用 FloatingActionButton.extend()构造函数时,需要提供图标和标签小部件以及 onPressed 回调。对于这两个构造函数,foregroundColor 和 backgroundColor 参数都可以自定义颜色。清单 7-16 展示了一个使用 FloatingActionButton 的例子。

FloatingActionButton(
  child: Icon(Icons.create),
  onPressed: () {},
);

Listing 7-16Example of FloatingActionButton

抽屉

Drawer 小部件是一个方便的面板包装器,滑动时显示在支架小部件的边缘。虽然你可以使用抽屉来包装任何小部件,但通常会在抽屉中显示应用徽标、当前用户的信息以及应用页面的链接。ListView 小部件通常用作 Drawer 小部件的子部件,以支持在抽屉中滚动。

要显示应用徽标和当前用户的信息,可以使用提供的 DrawerHeader 小部件及其子类 UserAccountsDrawerHeader。DrawerHeader 小部件包装了一个子小部件,并具有预定义的样式。UserAccountsDrawerHeader 是一个显示用户详细信息的特定小部件。表 7-8 显示了可以添加到 UserAccountsDrawerHeader 小部件中的部分。您还可以使用 onDetailsPressed 参数来添加在点击带有帐户名称和电子邮件的区域时的回拨。

表 7-8

UserAccountsDrawerHeader 中的节

|

名字

|

描述

| | --- | --- | | curreniaccountpicture | 当前用户帐户的图片。 | | 其他账户 | 当前用户的其他帐户的图片列表。你最多只能有三张这样的照片。 | | 帐户名 | 当前用户的帐户名称。 | | 帐户电子邮件 | 当前用户帐户的电子邮件。 |

清单 7-17 展示了一个使用 Drawer 和 UserAccountsDrawerHeader 的例子。

Drawer(
  child: ListView(
    children: <Widget>[
      UserAccountsDrawerHeader(
        currentAccountPicture: CircleAvatar(
          child: Text('JD'),
        ),
        accountName: Text('John Doe'),
        accountEmail: Text('john.doe@example.com'),
      ),
      ListTile(
        leading: Icon(Icons.search),
        title: Text('Search'),
      ),
      ListTile(
        leading: Icon(Icons.history),
        title: Text('History'),
      ),
    ],
  ),
);

Listing 7-17Example of Drawer

底部应用栏

BottomAppBar 小部件是 AppBar 的简化版本,显示在脚手架的底部。只在底部的应用栏中添加图标按钮是很常见的。如果脚手架也有一个浮动的动作按钮,底部的应用栏也会创建一个按钮停靠的凹口。清单 7-18 展示了一个使用 BottomAppBar 的例子。

BottomAppBar(
  child: Text('Bottom'),
  color: Colors.red,
);

Listing 7-18Example of BottomAppBar

底部导航栏

BottomNavigationBar 小部件提供了在不同视图之间导航的额外链接。表 7-9 显示了 BottomNavigationBar 构造函数的参数。

表 7-9

BottomNavigationBar 的参数

|

名字

|

类型

|

描述

| | --- | --- | --- | | 项目 | 列表< BottomNavigationBarItem> | 项目列表。 | | 当前值的索引 | (同 Internationalorganizations)国际组织 | 所选项目的索引。 | | 数据库 | 值已更改 | 当选择的项目改变时回调。 | | 类型 | BottomNavigationBarType | 导航栏的类型。 | | 固定颜色 | 颜色 | 键入 if bottomnavigationbartype . fixed 时选定项的颜色 | | 图标大小 | 两倍 | 图标的大小。 |

点击某个项目时,会调用带有所点击项目索引的 onTap 回调。根据项目的数量,可以有不同的方式来显示这些项目。项的布局由 BottomNavigationBarType 枚举的值定义。如果该值是固定的,则这些项目具有固定的宽度,并且总是显示文本标签。如果值正在移动,项目的位置可能会根据选定的项目而改变,并且仅显示选定项目的文本标签。BottomNavigationBar 有一个默认的策略来选择类型。当项目少于四个时,使用 BottomNavigationBarType.fixed 否则,将使用 BottomNavigationBarType.shifting。您可以使用 type 参数来重写默认行为。

表 7-10 显示了 BottomNavigationBarItem 构造器的参数。图标和标题参数都是必需的。如果 BottomNavigationBar 的类型是 BottomNavigationBarType.shifting,则导航栏的背景由所选项的背景颜色决定。您应该指定 backgroundColor 参数来区分各项。

表 7-10

BottomNavigationBarItem 的参数

|

名字

|

类型

|

描述

| | --- | --- | --- | | 图标 | 小部件 | 项目的图标。 | | 标题 | 小部件 | 项目的标题。 | | 激活 | 小部件 | 选择项目时显示的图标。 | | 背景颜色 | 颜色 | 项目的背景色。 |

清单 7-19 显示了一个使用 BottomNavigationBar 和 BottomNavigationBarItem 的例子。

 BottomNavigationBar(
  currentIndex: 1,
  type: BottomNavigationBarType.shifting,
  items: [
    BottomNavigationBarItem(
      icon: Icon(Icons.cake),
      title: Text('Cake'),
      backgroundColor: Colors.red.shade100,
    ),
    BottomNavigationBarItem(
      icon: Icon(Icons.map),
      title: Text('Map'),
      backgroundColor: Colors.green.shade100,
    ),
    BottomNavigationBarItem(
      icon: Icon(Icons.alarm),
      title: Text('Alarm'),
      backgroundColor: Colors.blue.shade100,
    ),
  ],

);

Listing 7-19Example of BottomNavigationBar

底部薄板

BottomSheet 微件显示在应用的底部,以提供附加信息。系统共享表是底层表的一个典型例子。有两种类型的底板:

  • 持久的底层总是可见的。可以使用 ScaffoldState.showBottomSheet 函数和 Scaffold 构造函数的 BottomSheet 参数创建持久的底部工作表。

  • 模态底层表单的行为类似于模态对话框。可以使用 showModalBottomSheet 函数创建模态底板。

BottomSheet 构造函数使用 WidgetBuilder 函数来创建实际内容。您还需要提供一个 onClosing 回调函数,当底部的工作表开始关闭时会调用这个回调函数。清单 7-20 显示了一个使用 BottomSheet 的例子。

BottomSheet(
  onClosing: () {},
  builder: (context) {
    return Text('Bottom');
  },
);

Listing 7-20Example of BottomSheet

脚手架状态

Scaffold 是一个有状态的小部件。您可以使用 Scaffold.of()方法从构建上下文中获取最近的 ScaffoldState 对象。ScaffoldState 有不同的方法与其他组件交互;参见表 7-11 。

表 7-11

脚手架搭设方法

|

名字

|

描述

| | --- | --- | | openDrawer() | 打开抽屉。 | | openEndDrawer() | 打开末端的抽屉。 | | 表演用酒吧(谈话用酒吧) | 展示零食吧。 | | hideCurrentSnackBar() | 隐藏当前的 SnackBar。 | | removeCurrentSnackBar() | 删除当前的 SnackBar。 | | showBottomSheet() | 显示持久的底层。 |

小吃吧

SnackBar 小部件在屏幕底部显示一条带有可选操作的消息。要创建 SnackBar 小部件,构造函数需要 content 参数来指定内容。duration 参数控制小吃店显示多长时间。要向小吃店添加操作,可以使用 SnackBarAction 类型的操作参数。当提供一个动作时,当按下该动作时,小吃店被解散。

要创建 SnackBarAction 实例,需要提供标签和 onPressed 回调。您可以使用 textColor 参数自定义按钮标签颜色。小吃店动作的按钮只能按一次。

ScaffoldState 的 showSnackBar()方法显示一个 SnackBar 小部件。一次最多只能显示一个小吃店。如果在另一个小吃店仍然可见时调用 ScaffoldState()方法,则给定的小吃店将被添加到一个队列中,并将在其他小吃店消失后显示。showSnackBar()方法的返回类型是 ScaffoldFeatureController 。SnackBarClosedReason 是一个定义小吃店可能关闭的原因的枚举。

清单 7-21 展示了一个开小吃店的例子。

Scaffold.of(context).showSnackBar(SnackBar(
  content: Text('This is a message.'),
  action: SnackBarAction(label: 'OK', onPressed: () {}),
));

Listing 7-21Example of SnackBar

7.5 搭建 iOS 页面

问题

你想搭建 iOS 页面。

解决办法

用 cupertinopagescaffold。

讨论

对于 iOS 应用,您可以使用 CupertinoPageScaffold 小部件来创建页面的基本布局。与材质设计中的支架相比,CupertinoPageScaffold 提供的定制是有限的。您只能指定导航栏、子级和背景色。

CupertinoNavigationBar 小部件在材质设计上与 AppBar 类似,但 CupertinoNavigationBar 只能有前导、中间和尾随小部件。中间小部件位于前导小部件和尾随小部件的中间。当 automaticallyImplyLeading 参数为 true 时,可以基于导航状态自动暗示前导小部件。当 automaticallyImplyMiddle 参数为真时,也可以自动隐含中间小部件。

清单 7-22 展示了一个使用 CupertinoPageScaffold 和 CupertinoNavigationBar 的例子。

CupertinoPageScaffold(
  navigationBar: CupertinoNavigationBar(
    middle: Text('App'),
    trailing: CupertinoButton(
      child: Icon(CupertinoIcons.search),
      onPressed: () {},
    ),

  ),
  child: Container(),
);

Listing 7-22Example of CupertinoPageScaffold

7.6 在材质设计中创建选项卡布局

问题

您想要创建标签栏和标签。

解决办法

使用 TabBar、Tab 和 TabController。

讨论

移动应用中广泛使用选项卡布局来组织一个页面中的多个部分。要在材质设计中实现选项卡布局,您需要使用几个小部件。TabBar 小部件是选项卡小部件的容器。TabController 小部件负责协调 TabBar 和 TabView。

选项卡小部件必须至少有一些文本、图标或子小部件,但不能同时有文本和子小部件。要创建 TabBar,您需要提供一个选项卡列表。您可以选择使用显式创建的 TabController 实例或使用共享的 DefaultTabController 实例。DefaultTabController 是一个继承的小部件。如果没有提供 TabController,TabBar 将尝试查找祖先 DefaultTabController 实例。

您可以选择提供 TabController 实例或使用继承的 DefaultTabController。要创建 TabController,您需要提供选项卡的数量和 TickerProvider 实例。

在清单 7-23 中,_TabPageState 的 mixin singletickerproviderstatemix in 是 TickerProvider 的一个实现,所以 _TabPageState 的当前实例作为 TabController 构造函数的 vsync 参数传递。TabController 实例由 TabBar 和 TabBarView 共享。

class TabPage extends StatefulWidget {
  @override
  _TabPageState createState() => _TabPageState();
}

class _TabPageState extends State<TabPage> with SingleTickerProviderStateMixin {
  final List<Tab> _tabs = [
    Tab(text: 'List', icon: Icon(Icons.list)),
    Tab(text: 'Map', icon: Icon(Icons.map)),
  ];
  TabController _tabController;

  @override
  void initState() {
    super.initState();
    _tabController = TabController(length: _tabs.length, vsync: this);
  }

  @override
  void dispose() {
    _tabController.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Tab'),
        bottom: TabBar(
          tabs: _tabs,
          controller: _tabController,
        ),
      ),
      body: TabBarView(
        children: _tabs.map((tab) {
          return Center(
            child: Text(tab.text),
          );
        }).toList(),
        controller: _tabController,
      ),
    );
  }
}

Listing 7-23TabBar with provided TabController

如果不需要和 TabController 交互,使用 DefaultTabController 是更好的选择。清单 7-24 中的代码使用 DefaultTabController 来实现与清单 7-23 中的代码相同的功能。

class DefaultTabControllerPage extends StatelessWidget {
  final List<Tab> _tabs = [
    Tab(text: 'List', icon: Icon(Icons.list)),
    Tab(text: 'Map', icon: Icon(Icons.map))
  ];

  @override
  Widget build(BuildContext context) {
    return DefaultTabController(
      length: _tabs.length,
      child: Scaffold(
        appBar: AppBar(
          bottom: TabBar(tabs: _tabs),
        ),
        body: TabBarView(
          children: _tabs.map((tab) {
            return Center(
              child: Text(tab.text),
            );
          }).toList(),
        ),
      ),
    );
  }

}

Listing 7-24DefaultTabController

7.7 在 iOS 中实现选项卡布局

问题

你想在 iOS 应用中实现标签布局。

解决办法

请使用 CupertinoTabScaffold、CupertinoTabBar 和 CupertinoTabView。

讨论

还可以使用小部件 CupertinoTabScaffold、CupertinoTabBar 和 CupertinoTabView 为 iOS 应用实现选项卡布局。创建 CupertinoTabScaffold 时,应该使用 CupertinoTabBar 作为 TabBar 参数的值。CupertinoTabBar 中的选项卡表示为 BottomNavigationBarItem 小部件。tabBuilder 参数指定为每个选项卡构建视图的构建器函数。清单 7-25 显示了一个实现标签布局的例子。

class CupertinoTabPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return CupertinoTabScaffold(
      tabBar: CupertinoTabBar(items: [
        BottomNavigationBarItem(icon: Icon(CupertinoIcons.settings)),
        BottomNavigationBarItem(icon: Icon(CupertinoIcons.info)),
      ]),
      tabBuilder: (context, index) {
        return CupertinoTabView(
          builder: (context) {
            return Center(
              child: Text('Tab $index'),
            );
          },
        );
      },
    );
  }

}

Listing 7-25Tab layout for iOS style

7.8 摘要

本章讨论了 Flutter 中常见的小部件,包括列表视图、网格视图、表格布局、页面搭建和选项卡布局。这些小部件创建了 Flutter 中页面的基本结构。在下一章,我们将讨论 Flutter 应用中的页面导航。