Flutter-秘籍-二-

144 阅读47分钟

Flutter 秘籍(二)

原文:Flutter Recipes

协议:CC BY-NC-SA 4.0

四、小部件基础

在构建 Flutter 应用时,大部分时间你都在处理小部件。本章提供了关于 Flutter 中窗口小部件的基本背景信息。它还介绍了几个显示文本、图像、图标、按钮和占位符的基本小部件。

4.1 了解小部件

问题

你想知道如何在 Flutter 中使用组件。

解决办法

在 Flutter 中,小部件无处不在。

讨论

如果你已经参与了用户界面的开发,你应该熟悉像部件或组件这样的概念。这些概念代表了创建用户界面的可重用构件。一个好的用户界面库应该有大量的高质量且易于使用的组件。按钮、图标、图像、菜单、对话框和表单输入都是组件的例子。组件可大可小。复杂组件通常由小组件组成。您可以通过遵循组件模型来创建自己的组件。您还可以选择将您的组件共享给社区。良好的组件生态系统是用户界面库成功的关键因素。

Flutter 使用小部件来描述用户界面中可重用的构件。与其他库相比,Flutter 中的小部件是一个更广泛的概念。不仅按钮和表单输入等常见组件是窗口小部件,布局约束在 Flutter 中也表示为窗口小部件。例如,如果您想将一个小部件放在一个框的中央,您只需将这个小部件包装成一个Center小部件。小部件也用于检索上下文数据。例如,DefaultTextStyle小部件得到的TextStyle应用于未样式化的Text小部件。

Flutter 中的小部件是用户界面一部分的不可变描述。小部件类的所有字段都是最终的,并在构造函数中设置。小部件构造函数只有命名参数。一个部件可以有一个或多个部件作为子部件。Flutter 应用的小部件创建了一个树状层次结构。Flutter 应用入口点文件的main()方法使用runApp()方法启动应用。runApp()的唯一参数是一个Widget对象。这个Widget对象是应用小部件树的根。小部件只是静态配置,描述如何配置层次结构中的子树。为了实际运行这个应用,我们需要一种方法来管理小部件的实例化。

Flutter 使用Element来表示树中特定位置的Widget的实例化。一个Widget可以被实例化零次或多次。将 ?? 转变为 ?? 的过程叫做膨胀。Widget类有一个createElement()方法来将小部件膨胀为Element的具体实例。Flutter 框架负责管理元素的生命周期。与元素相关联的窗口小部件可能会随着时间而改变。框架更新元素以使用新的配置。

当运行应用时,Flutter framework 负责渲染元素以创建渲染树,因此最终用户可以实际看到用户界面。渲染树由根为RenderViewRenderObject组成。如果你使用的是 Android Studio,你可以在 Flutter Inspector 视图中看到窗口小部件树和渲染树。选择查看➤工具窗口➤Flutter 检查器打开 Flutter 检查器视图。图 4-1 显示了 Flutter Inspector 中的 widgets 树。顶部面板显示小部件树,而底部面板显示小部件的详细信息。

img/479501_1_En_4_Fig1_HTML.jpg

图 4-1

Flutter 检查器中的 Widgets 树

图 4-2 显示了 Flutter 检查器中的渲染树。根是一个RenderView

img/479501_1_En_4_Fig2_HTML.jpg

图 4-2

在 Flutter 检查器中渲染树

4.2 了解构建上下文

问题

您想要访问与小部件树中的小部件相关的信息。

解决办法

WidgetBuilder函数有一个BuildContext参数,用于访问小部件树中与小部件相关的信息。你可以在StatelessWidget.build()State.build()方法中看到BuildContext

讨论

当构建小部件时,小部件在小部件树中的位置可能决定其行为,特别是当它有一个InheritedWidget作为其祖先时。BuildContext类提供了访问位置相关信息的方法;见表 4-1 。

表 4-1

构建上下文的方法

|

名字

|

描述

| | --- | --- | | ancestorInheritedElementForWidgetOfExactType | 获取与给定类型的InheritedWidget的最近祖先小部件对应的InheritedElement。 | | ancestorRenderObjectOfType | 获取最近的祖先RenderObjectWidget小部件的RenderObject。 | | ancestorStateOfType | 获取最近祖先StatefulWidget小部件的State对象。 | | rootAncestorStateOfType | 获取最远祖先StatefulWidget小部件的State对象。 | | ancestorWidgetOfExactType | 获取最近的祖先Widget。 | | findRenderObject | 获取小部件的当前RenderObject。 | | inheritFromElement | 用给定的祖先InheritedElement注册这个BuildContext,以便当祖先的小部件改变时,这个BuildContext被重建。 | | inheritFromWidgetOfExactType | 获取给定类型的最接近的InheritedWidget并注册这个BuildContext,以便当小部件改变时,这个BuildContext被重建。 | | visitAncestorElements | 访问祖先元素。 | | visitChildElements | 访问子元素。 |

BuildContext其实是Element类的接口。在StatelessWidget.build()State.build()方法中,BuildContext对象表示当前小部件膨胀的位置。在清单 4-1 中,ancestorWidgetOfExactType()方法用于获取类型Column的祖先小部件。

class WithBuildContext extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    Column column = context.ancestorWidgetOfExactType(Column);
    return Text(column.children.length.toString());
  }
}

Listing 4-1Use BuildContext

4.3 了解无状态小部件

问题

您希望创建一个没有可变状态的小部件。

解决办法

StatelessWidget类扩展。

讨论

当使用一个 widget 来描述用户界面的一部分时,如果这个部分可以使用 widget 本身的配置信息和它所在的BuildContext来完整描述,那么这个 widget 应该从StatelessWidget扩展而来。当创建一个StatelessWidget类时,您需要实现接受一个BuildContext并返回一个Widgetbuild()方法。在清单 4-2 中,HelloWorld类从StatelessWidget类扩展而来,并在build()方法中返回一个Center小部件。

class HelloWorld extends StatelessWidget {
  const HelloWorld({Key key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Center(
      child: Text('Hello World!'),
    );
  }
}

Listing 4-2Example of StatelessWidget

4.4 了解有状态小部件

问题

您希望创建一个具有可变状态的小部件。

解决办法

StatefulWidget类扩展。

讨论

如果用户界面的一部分可能动态变化,你需要从StatefulWidget类扩展。对于由它们创建的State对象中管理的状态,它们本身是不可变的。一个StatefulWidget子类需要实现返回一个State<StatefulWidget>对象的createState()方法。当状态改变时,State对象要调用setState()方法通知框架触发更新。在清单 4-3 中,_CounterState类是Counter小部件的State对象。当按钮被按下时,值在setState()方法中被更新,该方法更新_CounterState小部件以显示新值。

class Counter extends StatefulWidget {
  @override
  _CounterState createState() => _CounterState();
}

class _CounterState extends State<Counter> {
  int value = 0;

  @override
  Widget build(BuildContext context) {
    return Row(
      children: <Widget>[
        Text('$value'),
        RaisedButton(
          child: Text('+'),
          onPressed: () {
            setState(() {
              value++;
            });
          },
        ),
      ],
    );
  }
}

Listing 4-3Example of StatefulWidget

4.5 了解继承的小部件

问题

您希望沿着小部件树向下传播数据。

解决办法

InheritedWidget类扩展。

讨论

当构建部件的子树时,您可能需要沿着部件树向下传播数据。例如,子树的根部件可能定义一些上下文数据,例如,从服务器检索的配置数据。子树中的其他小部件可能也需要访问上下文数据。一种可能的方法是将上下文数据添加到小部件的构造函数中,然后将数据作为子小部件的构造函数参数进行传播。这种解决方案的主要缺点是,您需要为子树中的所有小部件添加构造函数参数。尽管有些小部件可能实际上不需要数据,但是它们仍然需要将数据传递给它们的子部件。

更好的方法是使用InheritedWidget类。BuildContext类有一个inheritFromWidgetOfExactType()方法来获取特定类型InheritedWidget的最近实例。使用InheritedWidget,您可以将上下文数据存储在一个InheritedWiget实例中。如果小部件需要访问上下文数据,可以使用inheritFromWidgetOfExactType()方法获取实例并访问数据。如果一个继承的小部件改变了状态,它将导致它的消费者重新构建。

在清单 4-4 ,ConfigWidget类中有数据config。静态的of()方法为config值获取最近的祖先ConfigWidget实例。方法updateShouldNotify()确定何时应该通知消费者窗口小部件。

class ConfigWidget extends InheritedWidget {
  const ConfigWidget({
    Key key,
    @required this.config,
    @required Widget child,
  })  : assert(config != null),
        assert(child != null),
        super(key: key, child: child);

  final String config;

  static String of(BuildContext context) {
    final ConfigWidget configWidget =
        context.inheritFromWidgetOfExactType(ConfigWidget);
    return configWidget?.config ?? ";
  }

  @override
  bool updateShouldNotify(ConfigWidget oldWidget) {
    return config != oldWidget.config;
  }
}

Listing 4-4Example of InheritedWidget

在清单 4-5 中,ConfigUserWidget类使用ConfigWidget.of()方法获得config值。

class ConfigUserWidget extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Text('Data is ${ConfigWidget.of(context)}');
  }
}

Listing 4-5Use of ConfigWidget

在清单 4-6 中,ConfigWidget实例的config值为“Hello!”还有一个后裔ConfigUserWidget的实例。

ConfigWidget(
  config: 'Hello!',
  child: Center(
    child: ConfigUserWidget(),
  ),
);

Listing 4-6Complete example

4.6 显示文本

问题

你想显示一些文本。

解决办法

使用TextRichText小部件。

讨论

几乎所有的应用都需要向终端用户显示一些文本。Flutter 提供了几个与文本相关的类。TextRichText是显示文本的两个小部件。事实上,Text内部使用RichTextText小部件的build()方法返回一个RichText实例。TextRichText的区别在于Text使用最近的封闭DefaultTextStyle对象的样式,而RichText需要显式样式。

文本

文本有两个构造函数。第一个构造函数Text()接受一个String作为要显示的文本。另一个构造函数Text.rich()接受一个TextSpan对象来表示文本和样式。创建Text小部件最简单的形式是Text('Hello world'),它使用最近的封装DefaultTextStyle对象的样式显示文本。Text()Text.rich()构造函数都有几个命名参数来定制它们;参见表 4-2 。

表 4-2

Text()和 Text.rich()的命名参数

|

名字

|

类型

|

描述

| | --- | --- | --- | | style | TextStyle | 文本的样式。 | | textAlign | TextAlign | 文本应如何水平对齐。 | | textDirection | TextDirection | 文本的方向。 | | locale | Locale | 基于 Unicode 选择字体的区域设置。 | | softWrap | bool | 是否在软换行符处断开文本。 | | overflow | TextOverflow | 如何处理文本溢出? | | textScaleFactor | double | 缩放文本的因子。 | | maxLines | int | 最大行数。如果文本超出限制,它将根据溢出中指定的策略被截断。 | | semanticsLabel | String | 文本的语义标签。 |

TextAlign是一个枚举类型,其值如表 4-3 所示。

表 4-3

文本对齐值

|

名字

|

描述

| | --- | --- | | left | 将文本与其容器的左边缘对齐。 | | right | 将文本与其容器的右边缘对齐。 | | center | 将文本在其容器的中心对齐。 | | justify | 对于以软换行符结尾的文本行,拉伸这些行以填充容器的宽度;对于以硬换行符结尾的文本行,将它们向起始边缘对齐。 | | start | 将文本与其容器的前缘对齐。对于从左到右的文本,前导边缘是左边缘,而对于从右到左的文本,前导边缘是右边缘。 | | end | 将文本在其容器的后沿对齐。后缘与前缘相反。 |

建议始终使用TextAlignstartend,而不是leftright,以便更好地处理双向文本。TextDirection是具有值ltrrtl的枚举类型。TextOverflow是一个枚举类型,其值如表 4-4 所示。

表 4-4

文本溢出值

|

名字

|

描述

| | --- | --- | | clip | 剪裁溢出的文本。 | | fade | 将溢出的文本渐变为透明。 | | ellipsis | 在溢出的文本后添加省略号。 |

DefaultTextStyle是一个InheritedWidget,其属性styletextAlignsoftWrapoverflowmaxLines与表 4-2 中的命名参数含义相同。如果在构造函数Text()Text.rich()中提供了一个命名参数,那么提供的值会覆盖最近的祖先DefaultTextStyle对象中的值。清单 4-7 展示了几个使用Text小部件的例子。

Text('Hello World')

Text(
  'Bigger Bold Text',
  style: TextStyle(fontWeight: FontWeight.bold),
  textScaleFactor: 2.0,
);

Text(
  'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt',
  maxLines: 1,
  overflow: TextOverflow.ellipsis,
);

Listing 4-7Examples of Text

文本扫描

构造函数Text.rich()将一个TextSpan对象作为必需的参数。TextSpan代表一段不可变的文本。TextSpan()构造函数有四个命名参数;参见表 4-5 。TextSpan组织是有等级制度的。一个TextSpan对象可能有许多TextSpan对象作为子对象。子元素可以覆盖父元素的样式。

表 4-5

TextSpan()的命名参数

|

名字

|

类型

|

描述

| | --- | --- | --- | | style | TextStyle | 文本和子对象的样式。 | | text | String | 范围中的文本。 | | children | List<TextSpan> | 作为这个跨度的孩子。 | | recognizer | GestureRecognizer | 接收事件的手势识别器。 |

清单 4-8 展示了使用Text.rich()的例子。此示例使用不同的样式显示句子“快速的棕色狐狸跳过懒惰的狗”。

Text.rich(TextSpan(
  style: TextStyle(
    fontSize: 16,
  ),
  children: [
    TextSpan(text: 'The quick brown '),
    TextSpan(
        text: 'fox',
        style: TextStyle(
          fontWeight: FontWeight.bold,
          color: Colors.red,
        )),
    TextSpan(text: ' jumps over the lazy '),
    TextSpan(
        text: 'dog',
        style: TextStyle(
          color: Colors.blue,
        )),
  ],
));

Listing 4-8Example of Text.rich()

法官文本

RichText总是使用TextSpan对象来表示文本和样式。RichText()构造函数有一个TextSpan类型的必需命名参数text。它还有可选的命名参数textAligntextDirectionsoftWrapoverflowtextScaleFactormaxLineslocale。这些可选的命名参数与表 4-2 中的Text()构造函数含义相同。

显示在RichText中的文本需要明确的样式。您可以使用DefaultTextStyle.of()BuildContext对象中获取默认样式。这正是Text在内部做的事情。Text小部件获取默认样式,并与样式参数中提供的样式合并,然后创建一个RichText,用一个TextSpan包装文本和合并的样式。如果你发现你确实需要使用默认样式作为基础,你应该直接使用Text而不是RichText。清单 4-9 显示了一个使用RichText的例子。

RichText(
  text: TextSpan(
    text: 'Level 1',
    style: TextStyle(color: Colors.black),
    children: [
      TextSpan(
        text: 'Level 2',
        style: TextStyle(fontWeight: FontWeight.bold),
        children: [
          TextSpan(
            text: 'Level 3',
            style: TextStyle(color: Colors.red),
          ),
        ],
      ),
    ],
  ),
);

Listing 4-9Example of RichText

4.7 对文本应用样式

问题

您希望显示的文本具有不同的样式。

解决办法

TextStyle来描述风格。

讨论

TextStyle描述应用于文本的样式。TextStyle()构造函数有很多命名参数来描述样式;见表 4-6 。

表 4-6

TextStyle()的命名参数

|

名字

|

类型

|

描述

| | --- | --- | --- | | color | Color | 文本的颜色。 | | fontSize | Double | 字体大小。 | | fontWeight | FontWeight | 字体粗细。 | | fontStyle | FontStyle | 字体变体。 | | letterSpacing | Double | 每个字母之间的间隔。 | | wordSpacing | Double | 每个单词之间的空格。 | | textBaseLine | TextBaseLine | 将此文本范围与其父范围对齐的公共基线。 | | height | Double | 文本的高度。 | | locale | Locale | 用于选择区域特定标志符号的区域设置。 | | foreground | Paint | 文本的前景。 | | background | Paint | 文本的背景。 | | shadows | List<Shadow> | 文本下面画的阴影。 | | decoration | TextDecoration | 文本的修饰。 | | decorationColor | Color | 文本装饰的颜色。 | | decorationStyle | TextDecorationStyle | 文本装饰的样式。 | | debugLabel | String | 调试样式的描述。 | | fontFamily | String | 字体的名称。 | | package | String | 如果字体是在包中定义的,与fontFamily一起使用。 |

FontWeight类定义值w100w200w300w400w500w600w700w800w900FontWeight.w100最薄,而w900最厚。FontWeight.boldFontWeight.w700的别名,而FontWeight.normalFontWeight.w400的别名。FontStyle是具有两个值italicnormal的枚举类型。TextBaseline是具有值alphabeticideographic的枚举类型。

TextDecoration类定义了不同类型的文本装饰。您还可以使用构造函数TextDecoration.combine()通过组合一系列TextDecoration实例来创建一个新的TextDecoration实例。例如,TextDecoration.combine([TextDecoration.underline, TextDecoration.overline])实例在文本的上面和下面画线。表 4-7 显示了TextDecoration中的常数。

表 4-7

文本装饰常数

|

名字

|

描述

| | --- | --- | | none | 没有装饰。 | | underline | 在文本下面画一条线。 | | overline | 在文本上方画一条线。 | | lineThrough | 在文本中画一条线。 |

TextDecorationStyle是一个枚举类型,其值如表 4-8 所示。TextDecorationStyle定义由TextDecoration创建的线条的样式。

表 4-8

文本修饰 Style values

|

名字

|

描述

| | --- | --- | | solid | 画一条实线。 | | double | 画两条线。 | | dotted | 画一条虚线。 | | dashed | 画一条虚线。 | | wavy | 画一条正弦曲线。 |

清单 4-10 显示了一个使用TextDecorationTextDecorationStyle的例子。

Text(
  'Decoration',
  style: TextStyle(
    fontWeight: FontWeight.w900,
    decoration: TextDecoration.lineThrough,
    decorationStyle: TextDecorationStyle.dashed,
  ),
);

Listing 4-10Example of using TextDecoration and TextDecorationStyle

如果您想要创建一个更新了一些属性的TextStyle实例的副本,那么使用copyWith()方法。apply()方法也创建了一个新的TextStyle实例,但是它允许使用 factor 和 delta 更新一些属性。例如,命名参数fontSizeFactorfontSizeDelta可以更新字体大小。用"fontSize * fontSizeFactor + fontSizeDelta"计算fontSize的更新值。您也可以使用相同的模式更新heightletterSpacingwordSpacing的值。对于fontWeight,仅支持fontWeightDelta。在清单 4-11 中,应用于文本的TextStyle更新了fontSizedecoration的值。

Text(
  'Scale',
  style: DefaultTextStyle.of(context).style.apply(
        fontSizeFactor: 2.0,
        fontSizeDelta: 1,
        decoration: TextDecoration.none,
      ),
);

Listing 4-11Update TextStyle

4.8 显示图像

问题

您想要显示从网络加载的图像。

解决办法

使用带有图像 URL 的Image.network()来加载和显示图像。

讨论

如果您在自己的服务器或其他地方托管了图像,您可以使用Image.network()构造函数来显示它们。Image.network()构造函数只需要加载图片的 URL。应该使用命名参数widthheight为图像小部件指定特定的尺寸,或者将其放在设置严格布局约束的上下文中。这是因为加载图像时,图像的尺寸可能会改变。如果没有严格的大小限制,图像小部件可能会影响其他小部件的布局。在清单 4-12 中,图像小部件的大小由命名参数widthheight指定。

Image.network(
  'https://picsum.photos/400/300',
  width: 400,
  height: 300,
);

Listing 4-12Example of Image.network()

所有下载的图像都被缓存,不管 HTTP 头。这意味着所有 HTTP 缓存控制头都将被忽略。您可以使用缓存克星来强制刷新缓存的图像。例如,您可以向图像 URL 添加一个随机字符串。

如果加载图像需要额外的 HTTP 头,您可以指定类型为Map<String, String>headers参数来提供这些头。一个典型的用例是加载需要 HTTP 头进行身份验证的受保护图像。

如果一个图像不能覆盖一个盒子的整个区域,你可以使用类型ImageRepeatrepeat参数来指定图像如何重复。ImageRepeat是一个枚举类型,其值如表 4-9 所示。默认值为noRepeat

表 4-9

图像重复值

|

名字

|

描述

| | --- | --- | | Repeat | 在 x 和 y 两个方向重复。 | | repeatX | 仅在 x 方向重复。 | | repeatY | 仅在 y 方向重复。 | | noRepeat | 不重复。未覆盖的区域将是透明的。 |

在清单 4-13 中,图像被放入一个比图像大的SizedBox中。通过使用ImageRepeat.repeat,该框被该图像填充。

SizedBox(
  width: 400,
  height: 300,
  child: Image.network(
    'https://picsum.photos/300/200',
    alignment: Alignment.topLeft,
    repeat: ImageRepeat.repeat,
  ),
);

Listing 4-13Repeated images

4.9 显示图标

问题

你想用图标。

解决办法

使用图标显示材质设计中的图标或社区中的图标包。

讨论

图标在移动应用中被广泛使用。与文本相比,图标在表达同样的语义时占用更少的屏幕空间。图标可以由字体符号或图像创建。Icon小部件是用字体字形绘制的。一个字体字形用IconData类描述。要创建一个IconData实例,字体中该图标的 Unicode 码位是必需的。

Icons类有许多预定义的IconData常量用于材质设计中的图标(Material . io/tools/icons/)。例如,Icons.call是名为“通话”的图标的IconData常量。如果应用使用材质设计,那么这些图标可以开箱即用。CupertinoIcons类有许多为 iOS 风格图标预定义的IconData常量。

Icon()构造函数已经命名了参数sizecolor来分别指定图标的大小和颜色。图标总是正方形的,宽度和高度都与大小相等。size 的默认值是 24。清单 4-14 创建一个大小为 100 的红色Icons.call图标。

Icon(
  Icons.call,
  size: 100,
  color: Colors.red,
);

Listing 4-14Example of Icon()

要使用流行的字体牛逼图标,可以使用包font_awesome_flutter ( https://pub.dartlang.org/packages/font_awesome_flutter )。将包依赖关系添加到pubspec.yaml文件后,您可以导入该文件以使用FontAwesomeIcons类。类似于Icons类,FontAwesomeIcons类有许多IconData常量,用于 Awesome 字体中的不同图标。清单 4-15 创建一个大小为 80 的蓝色FontAwesomeIcons.angry图标。

Icon(
  FontAwesomeIcons.angry,
  size: 80,
  color: Colors.blue,
);

Listing 4-15Use Font Awesome icon

4.10 使用带文本的按钮

问题

你想使用带有文本的按钮。

解决办法

使用按钮部件FlatButtonRaisedButtonOutlineButtonCupertinoButton

讨论

Flutter 有不同类型的按钮用于材质设计和 iOS。这些按钮部件都有一个必需的参数onPressed来指定按下时的处理函数。如果onPressed处理器是null,按钮被禁用。按钮的内容由类型为Widget的参数child指定。FlatButtonRaisedButtonOutlineButton对触摸有不同的风格和行为反应:

  • 一个FlatButton有零海拔和没有可见的边界。它通过填充由highlightColor指定的颜色来对触摸做出反应。

  • 一个RaisedButton有高程,用颜色填充。它通过将仰角增加到highlightElevation来对触摸做出反应。

  • 一个OutlineButton有边界,初始高度为 0.0,背景透明。它对触摸的反应是用颜色使其背景不透明,并将其高度增加到highlightElevation

应该用在工具栏、对话框、卡片上,或者内嵌在其他有足够空间让按钮显而易见的地方。RaisedButton s 应该用在使用空间不足以让按钮突出的地方。OutlineButtonRaisedButtonFlatButton的杂交。OutlineButton s 可以在FlatButton s 和RaisedButton s 都不合适的情况下使用。

如果你更喜欢 iOS 风格的按钮,你可以使用CupertinoButton小部件。CupertinoButton通过渐出和渐入对触摸做出反应。清单 4-16 展示了创建不同类型按钮的例子。

FlatButton(
  child: Text('Flat'),
  color: Colors.white,
  textColor: Colors.grey,
  highlightColor: Colors.red,
  onPressed: () => {},
);

RaisedButton(
  child: Text('Raised'),
  color: Colors.blue,
  onPressed: () => {},
);

OutlineButton(
  child: Text('Outline'),
  onPressed: () => {},
);

CupertinoButton(
  child: Text('Cupertino'),
  color: Colors.green,
  onPressed: () => {},
);

Listing 4-16Different types of buttons

4.11 使用带图标的按钮

问题

你想使用带有图标的按钮。

解决办法

使用IconButton控件、FlatButton.icon()RaisedButton.icon()OutlineButton.icon()

讨论

创建带有图标的按钮有两种方法。如果只有图标就够了,使用IconButton widget。如果图标和文本都需要,使用构造函数FlatButton.icon()RaisedButton.icon()OutlineButton.icon()

IconButton构造函数需要icon参数来指定图标。FlatButton.icon()RaisedButton.icon()OutlineButton.icon()分别使用参数iconlabel来指定图标和文本。清单 4-17 显示了使用IconButton()RaisedButton.icon()的例子。

IconButton(
  icon: Icon(Icons.map),
  iconSize: 50,
  tooltip: 'Map',
  onPressed: () => {},
);

RaisedButton.icon(
  icon: Icon(Icons.save),
  label: Text('Save'),
  onPressed: () => [],
);

Listing 4-17Examples of IconButton() and RaisedButton.icon()

4.12 添加占位符

问题

您希望添加占位符来表示稍后将添加的小部件。

解决办法

使用占位符。

讨论

在实现一个应用的界面之前,你通常对这个应用的外观有一个基本的概念。您可以从将界面分解成许多小部件开始。您可以在开发过程中使用占位符来表示未完成的小部件,这样您就可以测试其他小部件的布局。例如,如果您需要创建两个小部件,一个显示在顶部,而另一个显示在底部。如果您选择首先创建底部小部件,并为顶部小部件使用占位符,您可以在所需位置看到底部小部件。

Placeholder()构造函数接受命名参数colorstrokeWidthfallbackWidthfallbackHeight。占位符被绘制为一个矩形和两条对角线。参数colorstrokeWidth分别指定线条的颜色和宽度。默认情况下,占位符适合其容器。然而,如果占位符的容器是无界的,它使用给定的fallbackWidthfallbackHeight来确定大小。fallbackWidthfallbackHeight都有默认值400.0。清单 4-18 显示了一个Placeholder小部件的例子。

Placeholder(
  color: Colors.red,
  strokeWidth: 1,
  fallbackHeight: 200,
  fallbackWidth: 200,
);

Listing 4-18Example of Placeholder

4.13 总结

微件在 Flutter 应用中无处不在。本章提供了对 Flutter 中控件的基本介绍,包括StatelessWidgetStatefulWidgetInheritedWidget。本章还介绍了显示文本、图像、图标、按钮和占位符的常用基本小部件的用法。下一章将讨论 Flutter 中的布局。

五、布局小部件

在构建用户界面时,布局总是一项具有挑战性的任务。就移动应用而言,考虑到设备的大量不同屏幕分辨率,布局要复杂得多。本章介绍了 Flutter 布局的相关方法。

5.1 了解 Flutter 中的布局

问题

你想知道在 Flutter 中布局是如何工作的。

解决办法

Flutter 中的布局是由一组小部件实现的。这些布局小部件包装其他小部件,以应用不同的布局约束。

讨论

对于移动应用,布局必须能够适应不同的屏幕分辨率,而无需编写大量难以维护的代码。幸运的是,随着布局技术的发展,现在构建响应式布局更容易了。如果你有使用 CSS 进行 web 开发的经验,你可能听说过 W3C 的 CSS 灵活框布局模块规范(https:// www.w3.org/TR/css-flexbox-1/ )。flex 布局模型非常强大,因为它允许开发人员表达布局应该是什么样子,而不是如何实现实际的布局。这种声明式方法将繁重的工作转移到底层框架。结果布局代码更容易理解和维护。

例如,如果您想在容器的中心放置一个盒子,旧的方法可能需要计算盒子和容器的大小来确定盒子的位置。使用 flex 布局时,布局可以简化为清单 5-1 中的 CSS 代码。

.container {
  display: flex;
  width: 400px;
  height: 400px;
  justify-content: center;
  align-items: center;
  border: 1px solid green;
}

.item {
  width: 200px;
  height: 200px;
  border: 1px solid red;
}

Listing 5-1CSS code to center an item

flex 布局的理念现在不仅用于网页设计,也用于移动应用。React Native 使用 flex 布局( https://facebook.github.io/react-native/docs/flexbox )。Flutter 也采用了 flex 布局的思想。正如菜谱 4-1 中所讨论的,布局是作为小部件实现的。你可以在 Flutter 中看到像FlexRowColumnFlexible这样的小部件类,它们的名字来源于 flex 布局概念。CSS 中的 flex 布局模型超出了本书的范围。然而,理解这个 W3C 规范仍然是有价值的,它可以帮助您更好地理解 Flutter 中的 flex 布局。

渲染对象

Flutter 中的布局算法负责确定渲染树中每个RenderObject实例的尺寸和位置。RenderObject class 非常灵活,可以使用任何坐标系或布局协议。RenderObject类用layout()方法定义了基本的布局协议。layout()方法有一个必需的Constraints类型的位置参数。类指定了孩子必须遵守的布局约束。对于一个特定的Constraints实例,可能有多个结果可以满足它。只要允许,孩子可以自由使用这些结果。有时,一个Constraints实例可能只给子进程留下一个有效的结果。这种Constraints实例据说很紧。严格约束通常不太灵活,但是它们提供了更好的性能,因为具有严格约束的小部件不需要重新布局。

layout()方法有一个命名参数parentUsesSize来指定父节点是否需要使用子节点计算的布局信息。如果parentUsesSize为真,意味着父布局依赖于子布局。在这种情况下,每当孩子需要布局时,家长可能也需要布局。布局完成后,每个RenderObject实例将有一些字段被设置为包含布局信息。实际存储的信息取决于布局实现。这条布局信息存储在parentData属性中。

默认情况下,Flutter 使用由RenderBox类实现的 2D 笛卡尔坐标系。RenderBox类用BoxConstraints类实现了盒子布局模型。在盒子布局模型中,每个RenderBox实例被视为一个矩形,其大小被指定为一个Size实例。每个盒子都有自己的坐标系。左上角的坐标是(0,0),右下角的坐标是(宽度,高度)。RenderBox类使用BoxParentData作为布局数据的类型。BoxParentData.offset属性指定在父坐标系中绘制子对象的偏移量。

框约束

一个BoxConstraints实例由四个命名的双参数指定:minWidthmaxWidthminHeightmaxHeight。这些值必须满足以下规则。double.infinity是约束的有效值:

  • 0.0 <= minWidth < = maxWidth < = double.infinity

  • 0.0 <= minHeight < = maxHeight < = double.infinity

在框布局之后,RenderBox实例的大小必须满足应用于它的BoxConstraints实例的约束:

  • minWidth < = Size.width < = maxWidth

  • minHeight < = Size.height < = maxHeight

如果轴中的最小约束和最大约束相同,则该轴被严格约束。例如,如果minWidthmaxWidth的值相同,那么 width 是紧的。当宽度和高度都很紧的时候,一个BoxConstraints实例被称为是紧的。如果一个轴上的最小约束是0.0,那么这个轴是松散的。如果最大约束在一个轴上不是无限的,那么这个轴是有界的;否则,这个轴是无界的。

布局算法

在长方体布局模型中,布局是使用渲染树一次完成的。它首先通过传递约束来遍历渲染树。在此阶段,渲染对象使用其父对象传递的约束进行布局。在第二阶段,它通过传递确定每个渲染对象的大小和偏移的具体结果来遍历渲染树。

布局小部件

Flutter 为不同的布局需求提供了一组布局小部件。这些小部件有两类。第一类是包含单个子组件的小部件,它们是SingleChildRenderObjectWidget类的子类。第二类是可以包含多个孩子的小部件,这些孩子是MultiChildRenderObjectWidget类的子类。这些小部件的构造函数也有类似的模式。第一个命名参数是类型为Keykey。单个子布局小部件构造器的最后一个命名参数是Widget类型的child,而多个子布局小部件构造器的最后一个命名参数是List<Widget>类型的children

这些布局小部件是RenderObjectWidget类的子类。RenderObjectWidget类用于配置RenderObjectElementRenderObjectElementRenderObject

5.2 将小部件放在中央

问题

您希望将一个小部件放在另一个小部件的中心。

解决办法

用一个Center小部件包装这个小部件。

讨论

要将一个小部件放在另一个小部件的中央,只需将该小部件包装在一个Center小部件中。这个小部件将被水平和垂直放置在Center小部件的中心。这个Center小部件将是原始父小部件的子部件。中心构造函数有两个命名参数widthFactorheightFactor,分别指定宽度和高度的尺寸因子。清单 5-2 展示了一个使用Center小部件的例子。

Center(
  widthFactor: 2.0,
  heightFactor: 2.0,
  child: Text("Center"),
)

Listing 5-2Example of Center widget

Center widget 实际上是Align widget 的子类,带有Alignment.centeralignment集合。Center微件的行为与配方 5-3 中讨论的Align微件相同。

5.3 对齐小部件

问题

您希望将一个小部件与其父小部件的不同位置对齐。

解决办法

用一个Align小部件包装这个小部件。

讨论

使用Align微件,您可以在不同位置对齐子微件。Align小部件构造器有一个AlignmentGeometry类型的命名参数 alignment 来指定对齐。Center widget 实际上是一种特殊的Align widget,其alignment总是设置为Alignment.centerAlign widget 构造器也有命名参数widthFactorheightFactor

class 有两个子类用于不同的情况。Alignment类代表视觉坐标中的对齐。Alignment有两个属性xy来表示 2D 坐标系矩形中的位置。属性xy分别指定水平和垂直方向上的位置。Alignment(0.0, 0.0)表示矩形的中心。单位 1.0 表示从矩形的中心到一边的距离。单位 2.0 表示矩形在特定方向上的长度。例如,x 的值 2.0 表示矩形的宽度。x的正值表示位置在中心的右侧,而x的负值表示位置在左侧。同样的规则也适用于y的值。Align有几个常用位置的常量;见表 5-1 。

表 5-1

对齐常数

|

名字

|

价值

|

描述

| | --- | --- | --- | | bottomCenter | Alignment(0.0, 1.0) | 底边的中心点。 | | bottomLeft | Alignment(-1.0, 1.0) | 底边最左边的点。 | | bottomRight | Alignment(1.0, 1.0) | 底边最右边的点。 | | center | Alignment(0.0, 0.0) | 水平和垂直居中。 | | centerLeft | Alignment(-1.0, 0.0) | 左边缘的中心点。 | | centerRight | Alignment(1.0, 0.0) | 右边缘的中心点。 | | topCenter | Alignment(0,0, -1.0) | 顶边的中心点。 | | topLeft | Alignment(-1.0, -1.0) | 顶边最左边的点。 | | topRight | Alignment(1.0, -1.0) | 顶边的最右点。 |

如果要在对齐时考虑文本方向,需要使用AlignmentDirectional类而不是Alignment类。AlignmentDirectional类拥有start属性而非xstart值的增长方向与文本方向相同。当文本方向为从左向右时,start 的值与Alignment中的x含义相同。如果文本方向是从右向左,则start的值与Alignment中的x相反。AlignmentDirectional类也有几个常量用于常用的位置;见表 5-2 。这些常量用startend代替leftright来表示不同的方向。

表 5-2

alignmentdireactional 常数

|

名字

|

价值

|

描述

| | --- | --- | --- | | bottomCenter | AlignmentDirectional (0.0, 1.0) | 底边的中心点。 | | bottomStart | AlignmentDirectional(-1.0, 1.0) | 起点侧的底角。 | | bottomEnd | AlignmentDirectional(1.0, 1.0) | 端侧的底角。 | | center | AlignmentDirectional (0.0, 0.0) | 水平和垂直居中。 | | centerStart | AlignmentDirectional(-1.0, 0.0) | 起始边的中心点。 | | centerEnd | AlignmentDirectional(1.0, 0.0) | 末端边缘的中心点。 | | topCenter | AlignmentDirectional(0,0, -1.0) | 顶边的中心点。 | | topStart | AlignmentDirectional(-1.0, -1.0) | 起点侧的顶角。 | | topEnd | AlignmentDirectional(1.0, -1.0) | 端侧的顶角。 |

AlignmentGeometryresolve()方法接受一个类型为TextDirection的参数,并返回一个Alignment实例。您可以使用这个方法将一个AlignmentDirectional实例转换成一个Alignment实例。

传递给其子对象的 constrained 是在这个小部件的 constraints 对象上调用loosen()方法的结果。这意味着孩子可以选择不超过这个部件的尺寸。小部件本身的大小取决于参数widthFactorheightFactor的值及其约束对象。对于宽度,如果widthFactor不为空或者constraints.maxWidthdouble.infinity,则宽度是受约束条件约束的最接近childWidth * (widthFactory ?? 1.0)的值。否则,宽度由约束决定。同样的规则也适用于身高。

清单 5-3 展示了一个使用Align小部件的例子。

Align(
  alignment: Alignment.topLeft,
  child: SizedBox(
    width: 200,
    height: 200,
    child: Center(
      child: Text("TopLeft"),
    ),
  ),
)

Listing 5-3Example of Align widget

5.4 对小部件施加约束

问题

您希望对小部件施加布局约束。

解决办法

使用ConstrainedBoxSizedBox

讨论

如配方 5-1 所述,ConstraintsBoxContraints实例通常分别用于RenderObjectRenderBoxlayout()方法中。构建小部件树时,您可能还想对小部件施加布局约束。在这种情况下,可以使用ConstrainedBox widget。ConstrainedBox构造函数有一个必需的类型为BoxConstraints的命名参数约束,用于指定施加在子对象上的约束。

SizedBox widget 可以被视为一种特殊的ConstrainedBoxSizedBox已经命名了参数widthheight,用于使用BoxConstraints.tightFor()方法创建一个紧约束。SizedBox(width: width, height: height, child: child)ConstrainedBox(constraints: BoxConstraints.tightFor(width: width, height: height), child: child)相同。如果你想施加严格的约束,那么SizedBoxConstrainedBox更方便。SizedBox有其他常用用例的命名构造函数;见表 5-3 。

表 5-3

SizedBox 构造函数

|

名字

|

意义

|

描述

| | --- | --- | --- | | SizedBox.expand() | SizedBox(width: double.infinity, height: double.infinity) | 只要它的父节点允许。 | | SizedBox.shrink() | SizedBox(width: 0.0, height: 0.0) | 尽可能小。 | | SizedBox.fromSize() | SizedBox(width: size.width; height: size.height) | 具有指定大小的盒子。 |

应用于子部件的实际约束是提供的constraints参数和由ConstrainedBoxSizedBox的父部件提供的约束的组合。通过调用providedContraints.enforce(parentContraints)完成组合。结果约束尊重父约束,并尽可能接近所提供的约束。ConstrainedBoxSizedBox的大小是布局后子控件的大小。

清单 5-4 展示了使用ConstrainedBoxSizedBox的四个例子。第一个例子是典型的SizedBox使用模式。带有SizedBox.shrink()的第二个示例导致图像不显示。第三个例子是典型的ConstrainedBox使用模式。最后一个例子显示了一个ConstrainedBox实例如何考虑来自父实例的约束。

SizedBox(
  width: 100,
  height: 100,
  child: Text('SizedBox'),
)

SizedBox.shrink(
  child: Image.network('https://picsum.photos/50'),
)

ConstrainedBox(
  constraints: BoxConstraints(
    maxWidth: 50,
    minHeight: 50,
  ),
  child: Text('ConstrainedBox'),
)

ConstrainedBox(
  constraints: BoxConstraints(
    maxWidth: 200,
  ),
  child: ConstrainedBox(
    constraints: BoxConstraints(
      maxHeight: 200,
    ),
    child: Image.network('https://picsum.photos/300'),
  ),
)

Listing 5-4Examples of ConstrainedBox and SizedBox

5.5 不对小部件施加任何限制

问题

您希望对小部件施加约束,以允许它们以自然大小呈现。

解决办法

使用UnconstrainedBox

讨论

UnconstrainedBox是配方 5-4 中ConstrainedBox的反义词。UnconstrainedBox对其子不加约束。孩子可以在UnconstrainedBox实例提供的无限空间上自由渲染。UnconstrainedBox将通过遵循自身约束的限制,尝试使用子部件的大小来确定自身的大小。

如果子微件的尺寸大于UnconstrainedBox所能提供的最大尺寸,子微件将被裁剪。否则,子小部件将根据类型AlignmentGeometry的参数 alignment 的值进行对齐。如果子级溢出了父级,则在调试模式下会显示一条警告。使用UnconstrainedBox时,仍然可以使用Axis类型的参数constrainedAxis向一个轴添加约束。则仅允许子对象在另一个轴上不受约束地进行渲染。

在清单 5-5 中,UnconstrainedBox小部件被放置在一个具有固定宽度和高度的SizedBox小部件中。UnconstrainedBox小部件被限制在水平轴上,这意味着最小和最大宽度都是 100 像素。图像的宽度是 200 像素,因此它被缩小到 100 像素以满足宽度限制。这导致图像高度缩小到 150 像素,超过了父SizedBox小部件的最大高度 100 像素。在调试模式下运行时,您可以看到警告消息,上面和下面溢出了 25px。

SizedBox(
  width: 100,
  height: 100,
  child: UnconstrainedBox(
    constrainedAxis: Axis.horizontal,
    child: Image.network('https://picsum.photos/200/300'),
  ),
)

Listing 5-5Example of UnconstrainedBox

5.6 忽略父窗口时对窗口小部件施加约束

问题

无论小部件放在哪里,您都希望施加约束。

解决办法

使用OverflowBox

讨论

当对小部件施加约束时,通常会考虑父小部件的约束。尊重父约束使得小部件的布局灵活,以适应不同的用例。有时,您可能希望小部件只考虑显式提供的约束,而忽略父约束。在这种情况下,可以使用OverflowBox

OverflowBox构造器已经命名了参数alignmentminWidthmaxWidthminHeightmaxHeight。如果任何约束相关参数为空,则使用父约束的相应值。如果为所有四个约束相关的参数提供非空值,那么OverflowBox的子元素的布局与当前的小部件完全无关。

在清单 5-6 中,OverflowBox小部件是用所有四个约束相关参数的非空值创建的,所以即使它被放在SizedBox小部件中,它的大小也总是Size(200, 200)

SizedBox(
  width: 100,
  height: 100,
  child: OverflowBox(
    minWidth: 200,
    minHeight: 200,
    maxWidth: 200,
    maxHeight: 200,
    child: Image.network('https://picsum.photos/300'),
  ),
)

Listing 5-6Example of OverflowBox

5.7 限制大小以允许子部件溢出

问题

您希望小部件有一个大小,并允许子小部件溢出。

解决办法

使用SizedOverflowBox

讨论

SizedOverflowBox是用尺寸创建的。小部件的实际大小符合其约束条件,并尽可能接近请求的大小。子布局仅使用SizedOverflowBox小部件的约束。

在清单 5-7 中,SizedOverflowBox小部件被放置在带有约束BoxConstraints.loose(Size(100, 100))ConstrainedBox小部件中。SizedOverflowBox小工具的请求大小为Size(50, 50)SizedOverflowBox的实际尺寸也是Size(50, 50)。子Image小部件只使用SizedOverflowBox的约束。结果是图像小部件的大小为Size(100, 100),溢出了它的父对象。

ConstrainedBox(
  constraints: BoxConstraints.loose(Size(100, 100)),
  child: SizedOverflowBox(
    size: Size(50, 50),
    child: Image.network('https://picsum.photos/400'),
  ),
)

Listing 5-7Example of SizedOverflowBox

5.8 无界时限制小部件的大小

问题

您有一个通常匹配其父级大小的小部件,但是您希望它用在需要大小约束的其他地方。

解决办法

使用LimitedBox

讨论

一些部件通常被设计得尽可能大,以匹配它们父母的尺寸。但是这些小部件需要在其他地方进行约束。例如,当这些小部件被添加到垂直列表中时,需要限制高度。LimitedBox构造函数已经命名了参数maxWidthmaxHeight来指定限制。如果LimitedBox小部件的最大宽度没有限制,那么其子部件的宽度限制为maxWidth。如果这个LimitedBox的最大高度是无界的,那么它的子级高度被限制为maxHeigth

在清单 5-8 中,一个LimitedBox小部件的maxHeight被设置为 100,所以孩子的最大高度是 100px。

LimitedBox(
  maxHeight: 100,
  child: Image.network('https://picsum.photos/400'),
)

Listing 5-8Example of LimitedBox

5.9 缩放和定位小部件

问题

您想要缩放和定位一个小部件。

解决办法

使用不同装配模式和校准的FittedBox

讨论

配方 5-3 中的对齐部件可以使用不同的对齐方式来定位其子部件。FittedBox小部件支持其子部件的缩放和定位。使用BoxFit类型的参数fit指定适合模式。BoxFit是枚举类型,其值如表 5-4 所示。

表 5-4

BoxFit 值

|

名字

|

描述

| | --- | --- | | fill | 填充目标框。源的纵横比被忽略。 | | contain | 尽可能大以将源完全包含在目标框中。 | | cover | 尽可能小以覆盖整个目标框。 | | fitWidth | 仅确保显示源的整个宽度。 | | fitHeight | 仅确保显示源的完整高度。 | | none | 在目标框内对齐源,并丢弃框外的任何内容。 | | scaleDown | 将源与目标框对齐,必要时缩小以确保源适合目标框。如果源被收缩,这与 contain 相同;否则和没有一样。 |

FittedBox通常在显示图像时使用。清单 5-9 显示了一个示例,演示了BoxFit的不同值。ImageBox小部件使用SizedBox小部件来限制其大小,并将图像放在FittedBox小部件中。DecoratedBox小部件创建一个红色边框来显示ImageBox小部件的边界。

class FitPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Fit'),
      ),
      body: Center(
        child: Wrap(
          spacing: 20,
          runSpacing: 20,
          alignment: WrapAlignment.spaceAround,
          children: <Widget>[
            ImageBox(fit: BoxFit.fill),
            ImageBox(fit: BoxFit.contain),
            ImageBox(fit: BoxFit.cover),
            ImageBox(fit: BoxFit.fitWidth),
            ImageBox(fit: BoxFit.fitHeight),
            ImageBox(fit: BoxFit.none),
            ImageBox(fit: BoxFit.scaleDown),
          ],
        ),
      ),
    );
  }
}

class ImageBox extends StatelessWidget {
  const ImageBox({
    Key key,
    this.boxWidth = 150,
    this.boxHeight = 170,
    this.imageWidth = 200,
    this.fit,
  });

  final double boxWidth;
  final double boxHeight;
  final double imageWidth;
  final BoxFit fit;

  @override
  Widget build(BuildContext context) {
    return DecoratedBox(
      decoration: BoxDecoration(border: Border.all(color: Colors.red)),
      child: SizedBox(
        width: boxWidth,
        height: boxHeight,
        child: FittedBox(
          fit: fit,
          child: SizedBox(
            width: imageWidth,
            height: imageWidth,
            child: Image.network('https://dummyimage.com/${imageWidth.toInt()}'
                '&text=${fit.toString().substring(7)}'),
          ),
        ),
      ),
    );
  }
}

Listing 5-9Different values of BoxFit

图 5-1 显示了清单 5-9 中的代码截图。图像中的文本显示了这个ImageBox小部件中使用的BoxFit值。

img/479501_1_En_5_Fig1_HTML.jpg

图 5-1

BoxFit 的不同值

5.10 旋转部件

问题

你想旋转一个部件。

解决办法

使用RotatedBox

讨论

RotatedBox小组件在布局前旋转其子组件。旋转由带有quarterTurns参数的int类型的顺时针四分之一圈指定。quarterTurns参数值 1 表示顺时针旋转 90 度。

在清单 5-10 中,Text小部件旋转了四分之一圈。

RotatedBox(
  quarterTurns: 1,
  child: Text(
    'Hello World',
    textScaleFactor: 2,
  ),
)

Listing 5-10Example of RotatedWidget

5.11 显示部件时添加填充

问题

您希望在小部件周围添加填充。

解决办法

使用Padding

讨论

填充小部件在其子部件周围创建空白空间。传递给其子级的布局约束是小部件通过填充缩小后的约束,这会导致子级以较小的尺寸布局。填充是在类型EdgeInsetsGeometry的必需padding参数中指定的。

AlignmentGeometry类似,EdgeInsetsGeometry有两个子类EdgeInsetsEdgeInsetsDirectionalEdgeInsets类表示视觉坐标中的偏移量。偏移值是针对leftrighttopbottom边缘指定的。表 5-5 显示了EdgeInsets类的构造函数。

表 5-5

EdgeInsets 构造函数

|

名字

|

描述

| | --- | --- | | EdgeInsets.all() | 所有的偏移量都有给定值。 | | EdgeInsets.fromLTRB() | 指定左、上、右和下边缘的偏移值。 | | EdgeInsets.only() | 它具有命名参数 left、top、right 和 bottom,默认值为 0.0。 | | EdgeInsets.symmetric() | 它已命名参数垂直和水平,以创建对称的偏移。 |

考虑到文字方向,应该用EdgeInsetsDirectional类代替EdgeInsetsEdgeInsetsDirectional级用startend代替leftright。它有EdgeInsetsDirectional.fromSTEB()构造器来从starttopendbottom的偏移量创建 insets。EdgeInsetsDirectional.only()构造器与EdgeInsets.only()类似。

清单 5-11 展示了一个Padding小部件的例子。

Padding(
  padding: EdgeInsets.all(20),
  child: Image.network('https://picsum.photos/200'),
)

Listing 5-11Example of Padding

5.12 根据纵横比确定部件的大小

问题

您希望调整小部件的大小以保持特定的纵横比。

解决办法

使用AspectRatio

讨论

AspectRatio构造函数有必需的参数aspectRatio来指定宽高比的值。例如,4:3 的纵横比使用 4.0/3.0 的值。AspectRatio widget 试图找到最佳的尺寸来保持纵横比,同时遵守其布局约束。

该过程从将宽度设置为约束的最大宽度开始。如果最大宽度是有限的,那么高度由width / aspectRatio计算。否则,高度设置为约束的最大高度,宽度设置为height * aspectRatio。可能需要额外的步骤来确保结果的宽度和高度符合布局约束。例如,如果高度小于约束的最小高度,则高度设置为该最小值,宽度根据高度和纵横比计算。一般规则是先检查宽度再检查高度,先检查最大值再检查最小值。最终尺寸可能不满足比例要求,但必须满足布局限制。

在清单 5-12 中,AspectRatio小部件被放在一个ConstrainedBox中,带有一个Size(200, 200)的松散约束。长宽比是4.0/3.0,所以高度是根据200 / (4.0 / 3.0) = 150.0计算的。ApsectRatio的结果大小为Size(200.0, 150.0)

ConstrainedBox(
  constraints: BoxConstraints.loose(Size(200, 200)),
  child: AspectRatio(
    aspectRatio: 4.0 / 3.0,
    child: Image.network('https://picsum.photos/400/300'),
  ),
)

Listing 5-12Example of AspectRatio

5.13 转换小部件

问题

您想要在小部件上应用变换。

解决办法

使用Transform

讨论

变换小部件可以在绘制之前对其子部件应用变换。使用Matrix4实例来表示转换。Transform构造器有命名参数,如表 5-6 所示。

表 5-6

转换的命名参数

|

名字

|

类型

|

描述

| | --- | --- | --- | | transform | Matrix4 | 矩阵来变换子对象。 | | origin | Offset | 要应用变换的坐标系的原点。 | | alignment | AlignmentGeometry | 原点对齐。 | | transformHitTests | bool | 执行点击测试时是否应应用转换。 |

Transform类有其他构造函数来创建公共转换:

  • Tranform.rotate()–通过旋转指定角度来变换孩子。

  • Transform.scale()–用指定的比例因子均匀缩放,变换子对象。

  • Transform.translate()–通过平移指定的偏移量来变换子对象。

清单 5-13 展示了使用Transform的命名构造函数的例子。

Transform.rotate(
  angle: pi / 4.0,
  origin: Offset(10, 10),
  child: Text('Hello World'),
)

Transform.translate(
  offset: Offset(50, 50),
  child: Text('Hello World'),
)

Listing 5-13Examples of Transform

5.14 控制小部件上不同的布局方面

问题

您希望为一个小部件定义不同的布局方面。

解决办法

使用容器。

讨论

Flutter 有许多小部件来控制布局的不同方面。比如SizedBox widget 控制大小,Align widget 控制对齐。如果您想在同一个小部件上控制不同的布局,您可以用嵌套的方式包装这些小部件。实际上,Flutter 提供了一个Container小部件,使得定义不同的布局方面变得更加容易。

表 5-7 显示了Container构造器的命名参数。您不能同时为colordecoration提供非空值,因为颜色只是用值BoxDecoration(color: color)创建装饰的一种速记。如果widthheight不为空,它们的值用于收紧约束。

表 5-7

容器的命名参数

|

名字

|

类型

|

描述

| | --- | --- | --- | | alignment | 对齐几何图形 | 子对象的对齐方式。 | | padding | EdgeInsetsGeometry | 装饰内部的空白空间。 | | color | Color | 背景颜色。 | | decoration | Decoration | 装饰要画在孩子背后。 | | foregroundDecoration | Decoration | 装饰在孩子面前画画。 | | width | double | 子对象的宽度。 | | height | double | 孩子的身高。 | | constraints | BoxConstraints | 附加约束。 | | margin | EdgeInsetsGeometry | 装饰周围的空白空间。 | | transform | Matrix4 | 应用于容器的转换。 |

Container是基于参数值的不同小部件的组合。清单 5-14 显示了Container使用的不同部件的嵌套结构以及这些部件可能使用的参数。如果参数的值为 null,那么相应的小部件可能不存在。

Transform (transform)
  - Padding (margin)
    - ConstrainedBox (constraints, width, height)
      - DecoratedBox (foregroundDecoration)
        - DecoratedBox (decoration, color)
          - Padding (padding, decoration)
            - Align (alignment)
              - child

Listing 5-14Structure of Container

清单 5-15 展示了一个使用所有命名参数的Container小部件的例子。

Container(
  alignment: Alignment.bottomRight,
  padding: EdgeInsets.all(16),
  color: Colors.red.shade100,
  foregroundDecoration: BoxDecoration(
    image: DecorationImage(
      image: NetworkImage('https://picsum.photos/100'),
    ),
  ),
  width: 300,
  height: 300,
  constraints: BoxConstraints.loose(Size(400, 400)),
  margin: EdgeInsets.all(32),
  transform: Matrix4.rotationZ(0.1),
  child: Text(
    'Hello World',
    textScaleFactor: 3,
  ),
)

Listing 5-15Example of Container

图 5-2 显示了清单 5-15 中容器小部件的结构。您可以清楚地看到这些小部件是如何嵌套的。

img/479501_1_En_5_Fig2_HTML.jpg

图 5-2

容器的结构

5.15 实施柔性框布局

问题

您有多个小部件要布局,并且您希望它们能够占据额外的空间。

解决办法

使用 Flex、Column、Row、Flexible 和 Expanded。

讨论

要使用 flex box 模型布局多个小部件,可以使用 Flutter 提供的一组小部件,包括 flex、Column、Row、Flexible、Expanded 和 Spacer。事实上,只有 Flex 和灵活的小部件才是理解的重点。Flex 小部件用作布局容器,而 Flexible 小部件用于将子小部件包装在容器内。Flex 小部件在一维数组中显示其子部件。它支持水平和垂直两个方向的子布局。Row 和 Column 是 Flex 的子类,分别只在水平和垂直方向放置子元素。Flex 容器的灵活小部件可以控制子容器如何伸缩以占用额外的空间。Flex 小部件的子部件可以是灵活的,也可以不是。如果你想让一个孩子变得灵活,你可以简单地把它包在一个灵活的小部件里。

与 CSS flex 框布局相同,flex 小部件使用两个轴进行布局。儿童被放置的轴是主轴。另一根轴是横轴。使用轴类型的方向参数配置主轴。如果值是 Axis.horizontal,则主轴是水平轴,而横轴是垂直轴。如果值是 Axis.vertical,则主轴是垂直轴,而横轴是水平轴。Row 小工具总是以横轴为主轴,Column 小工具总是以纵轴为主轴。如果主轴是已知的,那么应该使用行或列小部件,而不是 Flex 小部件。

柔性框布局算法

Flex 子项的布局很复杂,需要多个步骤。第一步是用 null 或零伸缩因子来布局每个子元素。这些是不灵活的孩子。用于布局这些子项的约束取决于 crossAxisAlignment 的值。如果 crossAxisAlignment 的值为 CrossAxisAlignment.stretch,则约束将是横轴上最大尺寸的紧横轴约束。否则,约束仅设置横轴的最大值。例如,如果方向是 Axis.horizontal,而 crossAxisAlignment 是 CrossAxisAlignment.stretch,则这些非柔性子级的约束会将 minHeight 和 maxHeight 都设置为 Flex 约束的 maxHeight。这使得这些孩子占据了横轴上的所有空间。在第一步中,记录为这些子节点分配的总大小和跨轴大小的最大值。

第二步是用一个弹性系数来布置每个孩子。这些是灵活的孩子。从第一步开始,主轴的分配尺寸是已知的。可以根据主轴的最大大小和分配大小来计算空闲空间。自由空间根据弹性系数在所有弹性子节点之间分配。弹性系数为 2.0 的孩子将获得两倍于弹性系数为 1.0 的孩子的可用空间。假设有三个伸缩因子为 1.0、2.0 和 3.0 的子节点,如果可用空间为 120px,那么这些子节点将分别获得 20px、40px 和 60px 的空间。基于每个子项的弹性系数计算的值将是主轴上的最大约束。主轴上的最小约束取决于子项的 FlexFit 值。如果配合值为 FlexFit.tight,则最小值与最大值相同,这将在主轴上创建紧密约束。如果拟合值为 FlexFit.loose,则最小值为 0.0,这将在主轴上创建松散约束。横轴上的约束与 Flex 小部件的约束相同。最终约束用于布局这些 flex 子元素。

第三步是确定主轴和横轴的范围。如果 mainAxisSize 的值为 MainAxisSize.max,则主轴范围是当前 Flex 小部件的最大约束。否则,主轴范围就是为所有子级分配的大小。横轴的范围是所有子级的横轴约束的最大值。

最后一步是根据 mainAxisAlignment 和 crossAxisAlignment 的值确定每个子元素的位置。

表 5-8 显示了 enum MainAxisAlignment 的值。

表 5-8

MainAxisAlignment 值

|

名字

|

描述

| | --- | --- | | start | 将孩子放在靠近主轴起点的地方。水平方向的起始位置由 TextDirection 确定,垂直方向的起始位置由 VerticalDirection 确定。 | | end | 把孩子们放在靠近主轴末端的地方。使用与开始相同的方式确定结束位置。 | | center | Place the children close to the middle. | | spaceBetween | 在孩子之间平均分配空闲空间。 | | spaceAround | 在第一个和最后一个孩子前后各一半的空间,在孩子之间平均分配可用空间。 | | spaceEvenly | 在子节点之间均匀分布可用空间,包括第一个和最后一个子节点的前后。 |

表 5-9 显示了枚举交叉轴分配的值。

表 5-9

交叉轴对齐值

|

名字

|

描述

| | --- | --- | | start | 放置子对象,使起始边与横轴的起始边对齐。水平方向的起始位置由 TextDirection 确定,垂直方向的起始位置由 VerticalDirection 确定。 | | end | 放置子对象,使其端边与横轴的端边对齐。使用与开始相同的方式确定结束位置。 | | center | 将孩子放在与横轴中心对齐的位置。 | | stretch | 要求孩子们填写横轴。 | | baseline | 在横轴上匹配子项的基线。 |

灵活的

Flexible 使用 flex 参数来指定伸缩因子,使用 fit 参数来指定 BoxFit 值。flex 参数的默认值为 1,而 Fit 的默认值为 box fit . loose。Expanded 是 Flexible 的子类,其 fit 参数设置为 BoxFit.tight。

在清单 5-16 中,Column 小部件被放在一个 LimitedBox 小部件中以限制其高度。Column 小部件的所有子部件都是非灵活的。

LimitedBox(
  maxHeight: 320,
  child: Column(
    crossAxisAlignment: CrossAxisAlignment.end,
    mainAxisAlignment: MainAxisAlignment.spaceAround,
    children: <Widget>[
      Image.network('https://picsum.photos/50'),
      Image.network('https://picsum.photos/70'),
      Image.network('https://picsum.photos/90'),
    ],
  ),
)

Listing 5-16Flex widget with non-flexible children

在清单 5-17 中,列小部件既有灵活的子部件,也有非灵活的子部件。可以通过包装灵活或扩展的小部件来创建灵活的小部件。

LimitedBox(
  maxHeight: 300,
  child: Column(
    mainAxisAlignment: MainAxisAlignment.spaceBetween,
    children: <Widget>[
      Flexible(
        child: Image.network('https://picsum.photos/50'),
      ),
      Image.network('https://picsum.photos/40'),
      Expanded(
        child: Image.network('https://picsum.photos/50'),
      ),
      Expanded(
        flex: 2,
        child: Image.network('https://picsum.photos/50'),
      ),
    ],
  ),
)

Listing 5-17Flex widget with flexible and non-flexible children

5.16 显示重叠的小部件

问题

您希望布局可能相互重叠的小部件。

解决办法

使用 Stack 或 IndexedStack。

讨论

堆栈小部件的子部件可以是定位的,也可以是非定位的。定位的子控件包装在一个定位的小部件中,至少有一个非空属性。堆栈小部件的大小由所有未定位的子部件决定。布局过程有两个阶段。

第一阶段是布局所有未定位的子节点。用于未定位子级的约束取决于 StackFit 类型的 fit 属性值:

  • stack fit . Loose–由 constraints.loosen()创建的松散约束

  • stack filt . expand–由 box constraints . Tight(constraints . maximum)创建的紧密约束

  • Stack filt . pass through–与堆栈小部件具有相同的约束

堆栈小部件的大小由所有未定位子部件的最大大小决定。

在第二阶段,根据对齐属性定位所有未定位的子对象。用于定位子对象的约束由堆栈小部件的大小及其属性决定。定位的小部件有六个属性:左、上、右、下、宽度和高度。属性 left、right 和 width 用于确定紧密宽度约束。“顶部”、“底部”和“高度”属性用于确定紧密高度约束。例如,如果 left 和 right 值都不为 null,则紧宽度约束为 width of stack–right–left。然后,基于两个轴上的左、右、上、下值定位定位的子对象。如果所有这些值都为空,则基于对齐方式对其进行定位。

Stack 的子元素按顺序绘制,第一个子元素在底部。子数组中的顺序决定了子数组如何相互重叠。

IndexedStack 类是 Stack 的子类。IndexedStack 实例仅显示子级列表中的单个子级。IndexedStack 构造函数不仅具有与 Stack 构造函数相同的参数,还包括一个 int 类型的参数 index,用于指定要显示的子级的索引。如果参数 index 的值为 null,则不会显示任何内容。IndexedStack 的布局与 Stack 相同。IndexedStack 类只是用不同的方式来绘制自己。这意味着即使只显示了一个子元素,所有的子元素仍然需要像 Stack 一样进行布局。

清单 5-18 显示了一个带有定位子控件的堆栈小部件的例子。

Stack(
  children: <Widget>[
    Image.network('https://picsum.photos/200'),
    Image.network('https://picsum.photos/100'),
    Positioned(
      right: 0,
      bottom: 0,
      child: Image.network('https://picsum.photos/150'),
    ),
  ],
)

Listing 5-18Example of Stack

5.17 在多次运行中显示小部件

问题

您希望在多个水平或垂直方向上显示小部件。

解决办法

使用包装。

问题

Flex widget 不允许子项的大小超过主轴的大小。包装部件创建新的运行;没有足够的空间容纳孩子们。表 5-10 显示了包装构造器的命名参数。

表 5-10

Wrap 的命名参数

|

名字

|

价值

|

缺省值

|

描述

| | --- | --- | --- | --- | | direction | Axis | 轴.水平 | 主轴方向。 | | alignment | WrapAlignment | 环绕对齐. start | 在主轴上对齐一个行程内的子件。 | | spacing | Double | Zero | 在主轴上跑步的孩子之间的距离。 | | runAlignment | WrapAlignment | 环绕对齐. start | 横轴上的运行对齐。 | | runSpacing | Double | Zero | 横轴上运行之间的空间。 | | crossAxisAlignment | WrapCrossAlignment | WrapCrossAlignment.start | 在横轴上排列一个行程内的子件。 | | textDirection | TextDirection |   | 水平排列子项的顺序。 | | verticalDirection | VerticalDirection | 垂直方向.向下 | 垂直排列子对象的顺序。 | | children | List<Widget> | [] | 孩子们。 |

WrapAlignment 枚举与 MainAxisAlignment 具有相同的值。WrapCrossAlignment 枚举只有 start、end 和 center 值。

清单 5-19 展示了一个通过包装十个图像部件来包装部件的例子。

Wrap(
  spacing: 10,
  runSpacing: 5,
  crossAxisAlignment: WrapCrossAlignment.center,
  children: List.generate(
    10,
    (index) => Image.network('https://picsum.photos/${50 + index * 10}'),
  ),
)

Listing 5-19Example of Wrap

5.18 创建自定义单个子布局

问题

您希望为一个孩子创建自定义布局。

解决办法

使用 CustomSingleChildLayout。

讨论

如果那些针对单个子节点的内置布局小部件不能满足您的要求,您可以使用 CustomSingleChildLayout 创建一个自定义布局。CustomSingleChildLayout 小部件只是将布局委托给一个 SingleChildLayoutDelegate 实例。您需要创建自己的 SingleChildLayoutDelegate 子类来实现表 5-11 中所示的方法。

表 5-11

SingleChildLayoutDelegate 的方法

|

名字

|

描述

| | --- | --- | | getConstraintsForChild(BoxConstraints constraints) | 获取子对象的约束。 | | getPositionForChild(Size size, Size childSize) | 根据这个小部件和子部件的大小获取子部件的位置。 | | getSize(BoxConstraints constraints) | 获取此小部件的大小。 | | shouldRelayout() | 应该重新布局。 |

这个小部件的大小是应用约束后 delegate 的 getSize()方法返回的大小的结果。使用委托的 getConstraintsForChild()方法返回的约束完成子元素的布局。最后,用委托的 getPositionForChild()方法返回的值更新 child 的位置。

在清单 5-20 中,FixedPositionLayoutDelegate 类覆盖 getSize()方法来提供父小部件的大小。它还重写 getPositionForChild()方法来提供子级的位置。getConstraintsForChild()方法也被重写以返回紧缩约束。

class FixedPositionLayoutDelegate extends SingleChildLayoutDelegate {
  @override
  bool shouldRelayout(SingleChildLayoutDelegate oldDelegate) {
    return false;
  }

  @override
  Size getSize(BoxConstraints constraints) {
    return constraints.constrain(Size(300, 300));
  }

  @override
  BoxConstraints getConstraintsForChild(BoxConstraints constraints) {
    return constraints.tighten(width: 300, height: 300);
  }

  @override
  Offset getPositionForChild(Size size, Size childSize) {
    return Offset(50, 50);
  }
}

Listing 5-20Custom single child layout delegate

清单 5-21 展示了如何使用 FixedPositionLayoutDelegate。

CustomSingleChildLayout(
  delegate: FixedPositionLayoutDelegate(),
  child: Image.network('https://picsum.photos/100'),
)

Listing 5-21Example of FixedPositionLayoutDelegate

5.19 创建自定义多个子布局

问题

您想要为多个子项创建自定义布局。

解决办法

请使用 CustomMultiChildLayout 和 MultiChildLayoutDelegate。

讨论

如果这些用于多个孩子的内置小部件不能满足您的要求,您可以使用 CustomMultiChildLayout 创建一个自定义布局。与 CustomSingleChildLayout 类似,CustomMultiChildLayout 将布局逻辑委托给 MultiChildLayoutDelegate 实例。CustomMultiChildLayout 的所有子级必须包装在 LayoutId 小部件中,以便为它们提供唯一的 Id。在表 5-12 中显示的所有方法中,必须实现 performLayout()和 shouldRelayout()方法。所有其他方法都有默认实现。在 performLayout()方法的实现中,layoutChild()方法必须为每个子级调用一次。

表 5-12

MultiChildLayoutDelegate 的方法

|

名字

|

描述

| | --- | --- | | hasChild(Object childId) | 检查具有给定 id 的子级是否存在。 | | layoutChild(Object childId, BoxConstraints constraints) | 使用提供的约束布局子对象。 | | positionChild(Object childId, Offset offset) | 用给定的偏移量定位子对象。 | | getSize(BoxConstraints constraints) | 获取此小部件的大小。 | | performLayout(Size size) | 实际布局逻辑。 | | shouldRelayout() | 应该重新布局。 |

清单 5-22 显示了一个定制的多子布局委托。此委托使用递增的 int 值作为布局 id。子项的布局 id 必须从 0 开始。在 performLayout()方法中,对每个子元素调用 layoutChild()方法,从具有宽松约束的第一个子元素开始,这允许第一个子元素采用自然大小。记录第一个孩子的实际大小。然后用 Offset.zero 调用 positionChild()方法,将第一个孩子放在左上角。在第一个子级之后,对所有其他子级调用 layoutChild()和 positionChild()方法,分别增加大小和位置偏移量。

class GrowingSizeLayoutDelegate extends MultiChildLayoutDelegate {
  @override
  void performLayout(Size size) {
    int index = 0;
    Size childSize = layoutChild(index, BoxConstraints.loose(size));
    Offset offset = Offset.zero;
    positionChild(index, offset);
    index++;

    while (hasChild(index)) {
      double sizeFactor = 1.0 + index * 0.1;
      double offsetFactor = index * 10.0;
      childSize = layoutChild(
          index,
          BoxConstraints.tight(Size(
              childSize.width * sizeFactor, childSize.height * sizeFactor)));
      offset = offset.translate(offsetFactor, offsetFactor);
      positionChild(index, offset);
      index++;
    }
  }

  @override
  bool shouldRelayout(MultiChildLayoutDelegate oldDelegate) {
    return false;
  }

  @override
  Size getSize(BoxConstraints constraints) =>
      constraints.constrain(Size(400, 400));
}

Listing 5-22Custom multiple children layout delegate

清单 5-23 显示了 GrowingSizeLayoutDelegate 的用法。CustomMultiChildLayout 的子级是 SizedBox 中嵌套的六个图像。包装 LayoutId 小部件需要将布局 Id 传递给委托。

CustomMultiChildLayout(
  delegate: GrowingSizeLayoutDelegate(),
  children: List.generate(
    6,
    (index) => LayoutId(
          id: index,
          child: DecoratedBox(
            decoration:
                BoxDecoration(border: Border.all(color: Colors.red)),
            child: SizedBox(
              width: 70,
              height: 70,
              child: Image.network(
                  'https://dummyimage.com/${50 + index * 10}'),
            ),
          ),
        ),
  ),
)

Listing 5-23Example of GrowingSizeLayoutDelegate

图 5-3 显示了使用 GrowingSizeLayoutDelegate 的结果。

img/479501_1_En_5_Fig3_HTML.jpg

图 5-3

使用 GrowingSizeLayoutDelegate 的结果

5.20 摘要

有了 Flutter 中的布局小部件,很容易满足构建 Flutter 应用的常见布局需求。本章涵盖了许多针对单个子节点和多个子节点的布局小部件。在下一章,我们将讨论表单小部件。