Flutter 布局备忘录

你是否需要了解 Flutter 布局的案例?

这里我将展示我在使用 Flutter 布局的代码片段。我将通过精美的代码片段结合可视化的图形来举例。

本文注重 Flutter 部件中比较有用的一些来展示,而不是走马观花展示一大推的部件内容。

Row and Column

行(Row)和列(Column)的布局

MainAxisAlignment

RowColumn

Row /*or Column*/(
  mainAxisAlignment: MainAxisAlignment.start,
  children: <Widget>[
    Icon(Icons.star, size: 50),
    Icon(Icons.star, size: 50),
    Icon(Icons.star, size: 50),
  ],
),
RowColumn

Row /*or Column*/(
  mainAxisAlignment: MainAxisAlignment.center,
  children: <Widget>[
    Icon(Icons.star, size: 50),
    Icon(Icons.star, size: 50),
    Icon(Icons.star, size: 50),
  ],
),
RowColumn

Row /*or Column*/(
  mainAxisAlignment: MainAxisAlignment.end,
  children: <Widget>[
    Icon(Icons.star, size: 50),
    Icon(Icons.star, size: 50),
    Icon(Icons.star, size: 50),
  ],
),
RowColumn

Row /*or Column*/(
  mainAxisAlignment: MainAxisAlignment.spaceBetween,
  children: <Widget>[
    Icon(Icons.star, size: 50),
    Icon(Icons.star, size: 50),
    Icon(Icons.star, size: 50),
  ],
),
复制代码
RowColumn

Row /*or Column*/(
  mainAxisAlignment: MainAxisAlignment.spaceEvenly,
  children: <Widget>[
    Icon(Icons.star, size: 50),
    Icon(Icons.star, size: 50),
    Icon(Icons.star, size: 50),
  ],
),
复制代码
RowColumn

Row /*or Column*/(
  mainAxisAlignment: MainAxisAlignment.spaceAround,
  children: <Widget>[
    Icon(Icons.star, size: 50),
    Icon(Icons.star, size: 50),
    Icon(Icons.star, size: 50),
  ],
),
复制代码

CrossAxisAlignment

如果你需要文本是针对基线对齐,那么你应该使用 CrossAxisAlignment.baseline

Row(
  crossAxisAlignment: CrossAxisAlignment.baseline,
  textBaseline: TextBaseline.alphabetic,
  children: <Widget>[
    Text(
      'Baseline',
      style: Theme.of(context).textTheme.headline2,
    ),
    Text(
      'Baseline',
      style: Theme.of(context).textTheme.bodyText2,
    ),
  ],
),
复制代码
RowColumn

Row /*or Column*/(
  crossAxisAlignment: CrossAxisAlignment.start,
  children: <Widget>[
    Icon(Icons.star, size: 50),
    Icon(Icons.star, size: 200),
    Icon(Icons.star, size: 50),
  ],
),
复制代码
RowColumn

Row /*or Column*/(
  crossAxisAlignment: CrossAxisAlignment.center,
  children: <Widget>[
    Icon(Icons.star, size: 50),
    Icon(Icons.star, size: 200),
    Icon(Icons.star, size: 50),
  ],
),
复制代码
RowColumn

Row /*or Column*/(
  crossAxisAlignment: CrossAxisAlignment.end,
  children: <Widget>[
    Icon(Icons.star, size: 50),
    Icon(Icons.star, size: 200),
    Icon(Icons.star, size: 50),
  ],
),
复制代码
RowColumn

Row /*or Column*/(
  crossAxisAlignment: CrossAxisAlignment.stretch,
  children: <Widget>[
    Icon(Icons.star, size: 50),
    Icon(Icons.star, size: 200),
    Icon(Icons.star, size: 50),
  ],
),
复制代码

MainAxisSize

RowColumn

Row /*or Column*/(
  mainAxisSize: MainAxisSize.max,
  children: <Widget>[
    Icon(Icons.star, size: 50),
    Icon(Icons.star, size: 50),
    Icon(Icons.star, size: 50),
  ],
),
复制代码
RowColumn

Row /*or Column*/(
  mainAxisSize: MainAxisSize.min,
  children: <Widget>[
    Icon(Icons.star, size: 50),
    Icon(Icons.star, size: 50),
    Icon(Icons.star, size: 50),
  ],
),
复制代码

IntrinsicWidth and IntrinsicHeight

在行列布局中,如何使得所有的部件跟宽度/高度最大的部件同宽/同高呢?如下:

我们假有下面的布局:

Widget build(BuildContext context) {
  return Scaffold(
    appBar: AppBar(title: Text('IntrinsicWidth')),
    body: Center(
      child: Column(
        children: <Widget>[
          RaisedButton(
            onPressed: () {},
            child: Text('Short'),
          ),
          RaisedButton(
            onPressed: () {},
            child: Text('A bit Longer'),
          ),
          RaisedButton(
            onPressed: () {},
            child: Text('The Longest text button'),
          ),
        ],
      ),
    ),
  );
}
复制代码

那么,你想所有的按钮的宽度都跟最宽的按钮那么宽,那就使用 IntrinsicWidth

Widget build(BuildContext context) {
  return Scaffold(
    appBar: AppBar(title: Text('IntrinsicWidth')),
    body: Center(
      child: IntrinsicWidth(
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.stretch,
          children: <Widget>[
            RaisedButton(
              onPressed: () {},
              child: Text('Short'),
            ),
            RaisedButton(
              onPressed: () {},
              child: Text('A bit Longer'),
            ),
            RaisedButton(
              onPressed: () {},
              child: Text('The Longest text button'),
            ),
          ],
        ),
      ),
    ),
  );
}
复制代码

同理,如果你想所有的部件的高度跟最高的部件一样高,你需要结合 IntrinsicHeightRow 来实现。

Stack

Stack 很适合小部件相互叠加。

Widget build(BuildContext context) {
  Widget main = Scaffold(
    appBar: AppBar(title: Text('Stack')),
  );

  return Stack(
    fit: StackFit.expand,
    children: <Widget>[
      main,
      Banner(
        message: "Top Start",
        location: BannerLocation.topStart,
      ),
      Banner(
        message: "Top End",
        location: BannerLocation.topEnd,
      ),
      Banner(
        message: "Bottom Start",
        location: BannerLocation.bottomStart,
      ),
      Banner(
        message: "Bottom End",
        location: BannerLocation.bottomEnd,
      ),
    ],
  );
}
复制代码

使用自己的部件,你需要将它们放在 Positioned 部件中。

Widget build(BuildContext context) {
  return Scaffold(
    appBar: AppBar(title: Text('Stack')),
    body: Stack(
      fit: StackFit.expand,
      children: <Widget>[
        Material(color: Colors.yellowAccent),
        Positioned(
          top: 0,
          left: 0,
          child: Icon(Icons.star, size: 50),
        ),
        Positioned(
          top: 340,
          left: 250,
          child: Icon(Icons.call, size: 50),
        ),
      ],
    ),
  );
}
复制代码

如果你不想猜测顶部/底部的值,你可以使用 LayoutBuilder 部件来检索它们的值。

Widget build(BuildContext context) {
  const iconSize = 50;
  return Scaffold(
    appBar: AppBar(title: Text('Stack with LayoutBuilder')),
    body: LayoutBuilder(
      builder: (context, constraints) =>
        Stack(
          fit: StackFit.expand,
          children: <Widget>[
            Material(color: Colors.yellowAccent),
            Positioned(
              top: 0,
              child: Icon(Icons.star, size: iconSize),
            ),
            Positioned(
              top: constraints.maxHeight - iconSize,
              left: constraints.maxWidth - iconSize,
              child: Icon(Icons.call, size: iconSize),
            ),
          ],
        ),
    ),
  );
}
复制代码

Expanded

Expanded 配合 Flex\Flexbox 布局实现,它对于多项目分配空间很棒。

Row(
  children: <Widget>[
    Expanded(
      child: Container(
        decoration: const BoxDecoration(color: Colors.red),
      ),
      flex: 3,
    ),
    Expanded(
      child: Container(
        decoration: const BoxDecoration(color: Colors.green),
      ),
      flex: 2,
    ),
    Expanded(
      child: Container(
        decoration: const BoxDecoration(color: Colors.blue),
      ),
      flex: 1,
    ),
  ],
),
复制代码

ConstrainedBox

默认的,很多部件多尽量使用小空间,比如:

Card(
  child: const Text('Hello World!'),
  color: Colors.yellow,
),
复制代码

ConstrainedBox 允许小部件根据需要使用剩下的空间。

ConstrainedBox(
  constraints: BoxConstraints.expand(),
  child: const Card(
    child: const Text('Hello World!'),
    color: Colors.yellow,
  ),
),
复制代码

使用 BoxConstraints,你可以指定一个小部件可以有多少空间,你可以指定高度/宽度的最小/最大值。

除非指定值,否则 BoxConstraints.expand 使用无限的空间量(也就是使用剩下的所有空间):

ConstrainedBox(
  constraints: BoxConstraints.expand(height: 300),
  child: const Card(
    child: const Text('Hello World!'),
    color: Colors.yellow,
  ),
),
复制代码

上面👆的写法等同下面👇的写法:

ConstrainedBox(
  constraints: BoxConstraints(
    minWidth: double.infinity,
    maxWidth: double.infinity,
    minHeight: 300,
    maxHeight: 300,
  ),
  child: const Card(
    child: const Text('Hello World!'),
    color: Colors.yellow,
  ),
),
复制代码

Align

有时候,我们很难设置我们的小部件到正确的大小 -- 比如,它们自由伸展,但是这不是你想要的。

当你在 Column 中使用 CrossAxisAlignment.stretch 的时候,上面的现象就会发生,而你想要的是这个按钮不伸展。

Column(
  crossAxisAlignment: CrossAxisAlignment.stretch,
  children: <Widget>[
    Align(
      child: RaisedButton(
        onPressed: () {},
        child: const Text('Button'),
      ),
    ),
  ],
),
复制代码

当你的小部件并不受限你设定的约束时,那么你可以尝试使用 Align 部件包裹它。

Container

Container 是最常用的部件之一 -- 有如下的好处:

Container as a layout tool

当你没有指定 Container 的高度 height 或者宽度 width 的时候,它会自动适配 child 子部件的大小。

Widget build(BuildContext context) {
  return Scaffold(
    appBar: AppBar(title: Text('Container as a layout')),
    body: Container(
      color: Colors.yellowAccent,
      child: Text("Hi"),
    ),
  );
}
复制代码

如果你想伸展 Container 来适配它的父部件,请为属性高度 height 或宽度 width 设定值 double.infinity

Widget build(BuildContext context) {
  return Scaffold(
    appBar: AppBar(title: Text('Container as a layout')),
    body: Container(
      height: double.infinity,
      width: double.infinity,
      color: Colors.yellowAccent,
      child: Text("Hi"),
    ),
  );
}
复制代码

Container as decoration

你可以使用 Containercolor 属性来更改其背景颜色,但是你也可以使用 decorationforegroundDecoration 来更改。(使用这两个属性,你完全可以更改 Container 的样子,这个我们迟点说)。

decoration 总是在 child 属性的后面,而 foregroundDecoration 总是在 child 属性的后面。(这也不一定)

Widget build(BuildContext context) {
  return Scaffold(
    appBar: AppBar(title: Text('Container.decoration')),
    body: Container(
      height: double.infinity,
      width: double.infinity,
      decoration: BoxDecoration(color: Colors.yellowAccent),
      child: Text("Hi"),
    ),
  );
}
复制代码

Widget build(BuildContext context) {
  return Scaffold(
    appBar: AppBar(title: Text('Container.foregroundDecoration')),
    body: Container(
      height: double.infinity,
      width: double.infinity,
      decoration: BoxDecoration(color: Colors.yellowAccent),
      foregroundDecoration: BoxDecoration(
        color: Colors.red.withOpacity(0.5),
      ),
      child: Text("Hi"),
    ),
  );
}
复制代码

Container as Transform

如果你不想使用 Transform 部件来更改布局,你可以直接使用 Container 中的 transform 属性。

Widget build(BuildContext context) {
  return Scaffold(
    appBar: AppBar(title: Text('Container.transform')),
    body: Container(
      height: 300,
      width: 300,
      transform: Matrix4.rotationZ(pi / 4),
      decoration: BoxDecoration(color: Colors.yellowAccent),
      child: Text(
        "Hi",
        textAlign: TextAlign.center,
      ),
    ),
  );
}
复制代码

BoxDecoration

decoration 通常用于更改 Container 部件的外观。

image: DecorationImage

图片作为背景:

Widget build(BuildContext context) {
  return Scaffold(
    appBar: AppBar(title: Text('image: DecorationImage')),
    body: Center(
      child: Container(
        height: 200,
        width: 200,
        decoration: BoxDecoration(
          color: Colors.yellow,
          image: DecorationImage(
            fit: BoxFit.fitWidth,
            image: NetworkImage(
              'https://flutter.dev/images/catalog-widget-placeholder.png', // 地址已经无效
            ),
          ),
        ),
      ),
    ),
  );
}
复制代码

border: Border

指定 Container 的边框看起来该怎样。

Widget build(BuildContext context) {
  return Scaffold(
    appBar: AppBar(title: Text('border: Border')),
    body: Center(
      child: Container(
        height: 200,
        width: 200,
        decoration: BoxDecoration(
          color: Colors.yellow,
          border: Border.all(color: Colors.black, width: 3),
        ),
      ),
    ),
  );
}
复制代码

borderRadius: BorderRadius

使得边框角变圆。

如果装饰中 shape 属性的值是 BoxShape.circle,那么 borderRadius 不会起作用。

Widget build(BuildContext context) {
  return Scaffold(
    appBar: AppBar(title: Text('borderRadius: BorderRadius')),
    body: Center(
      child: Container(
        height: 200,
        width: 200,
        decoration: BoxDecoration(
          color: Colors.yellow,
          border: Border.all(color: Colors.black, width: 3),
          borderRadius: BorderRadius.all(Radius.circular(18)),
        ),
      ),
    ),
  );
}
复制代码

shape: BoxShape

BoxDecoration 可以是矩形/正方形或者椭圆/圆形。

对于其他形状,你可以使用 ShapeDecoration 代替 BoxDecoration

Widget build(BuildContext context) {
  return Scaffold(
    appBar: AppBar(title: Text('shape: BoxShape')),
    body: Center(
      child: Container(
        height: 200,
        width: 200,
        decoration: BoxDecoration(
          color: Colors.yellow,
          shape: BoxShape.circle,
        ),
      ),
    ),
  );
}
复制代码

boxShadow: List

Container 添加阴影。

这个参数是一个列表,你可以指定多个不同的阴影并将它们合并在一起。

Widget build(BuildContext context) {
  return Scaffold(
    appBar: AppBar(title: Text('boxShadow: List<BoxShadow>')),
    body: Center(
      child: Container(
        height: 200,
        width: 200,
        decoration: BoxDecoration(
          color: Colors.yellow,
          boxShadow: const [
            BoxShadow(blurRadius: 10),
          ],
        ),
      ),
    ),
  );
}
复制代码

gradient

有三种类型的渐变:LinearGradientRadialGradientSweepGradient

Widget build(BuildContext context) {
  return Scaffold(
    appBar: AppBar(title: Text('gradient: LinearGradient')),
    body: Center(
      child: Container(
        height: 200,
        width: 200,
        decoration: BoxDecoration(
          gradient: LinearGradient(
            colors: const [
              Colors.red,
              Colors.blue,
            ],
          ),
        ),
      ),
    ),
  );
}
复制代码

Widget build(BuildContext context) {
  return Scaffold(
    appBar: AppBar(title: Text('gradient: RadialGradient')),
    body: Center(
      child: Container(
        height: 200,
        width: 200,
        decoration: BoxDecoration(
          gradient: RadialGradient(
            colors: const [Colors.yellow, Colors.blue],
            stops: const [0.4, 1.0],
          ),
        ),
      ),
    ),
  );
}
复制代码

Widget build(BuildContext context) {
  return Scaffold(
    appBar: AppBar(title: Text('gradient: SweepGradient')),
    body: Center(
      child: Container(
        height: 200,
        width: 200,
        decoration: BoxDecoration(
          gradient: SweepGradient(
            colors: const [
              Colors.blue,
              Colors.green,
              Colors.yellow,
              Colors.red,
              Colors.blue,
            ],
            stops: const [0.0, 0.25, 0.5, 0.75, 1.0],
          ),
        ),
      ),
    ),
  );
}
复制代码

backgroundBlendMode

backgroundBlendModeBoxDecoration 中最复杂的属性之一。

它负责将 BoxDecoration 中颜色/渐变,以及 BoxDecoration 上的任何内容混合一起。

使用 backgroundBlendMode, 你可以使用 BlendMode 枚举中指定的一长串算法。

首先,让我们将 BoxDecoration 设置为 foregroundDecoration,它被绘制在 Container 子部件之上(而 decoration 会绘制在子部件之后)。

Widget build(BuildContext context) {
  return Scaffold(
    appBar: AppBar(title: Text('backgroundBlendMode')),
    body: Center(
      child: Container(
        height: 200,
        width: 200,
        foregroundDecoration: BoxDecoration(
          backgroundBlendMode: BlendMode.exclusion,
          gradient: LinearGradient(
            colors: const [
              Colors.red,
              Colors.blue,
            ],
          ),
        ),
        child: Image.network(
          'https://flutter.io/images/catalog-widget-placeholder.png', // 图片 404
        ),
      ),
    ),
  );
}
复制代码

backgroundBlendMode 不仅仅影响它所在的 Container

backgroundBlendMode 改变其所在 Container 及其一下部件树的内容的颜色。

下面的代码又一个父部件 Container 来绘制一个 image,然后有一个子部件 Container 来使用 backgroundBlendMode,但是你还是获取到和之前的效果。

Widget build(BuildContext context) {
  return Scaffold(
    appBar: AppBar(title: Text('backgroundBlendMode')),
    body: Center(
      child: Container(
        decoration: BoxDecoration(
          image: DecorationImage(
            image: NetworkImage(
              'https://flutter.io/images/catalog-widget-placeholder.png', // 404
            ),
          ),
        ),
        child: Container(
          height: 200,
          width: 200,
          foregroundDecoration: BoxDecoration(
            backgroundBlendMode: BlendMode.exclusion,
            gradient: LinearGradient(
              colors: const [
                Colors.red,
                Colors.blue,
              ],
            ),
          ),
        ),
      ),
    ),
  );
}
复制代码

Material

有切角的边框。

Widget build(BuildContext context) {
  return Scaffold(
    appBar: AppBar(title: Text('shape: BeveledRectangleBorder')),
    body: Center(
      child: Material(
        shape: const BeveledRectangleBorder(
          borderRadius: BorderRadius.all(Radius.circular(20)),
          side: BorderSide(color: Colors.black, width: 4),
        ),
        color: Colors.yellow,
        child: Container(
          height: 200,
          width: 200,
        ),
      ),
    ),
  );
}
复制代码

Slivers

SliverFillRemaining

即便没有足够的空间,当你想要将内容居中,这个部件也是不可替代的。

Widget build(BuildContext context) {
  return Scaffold(
    appBar: AppBar(title: Text('SliverFillRemaining')),
    body: CustomScrollView(
      slivers: [
        SliverFillRemaining(
          hasScrollBody: false,
          child: Column(
            mainAxisAlignment: MainAxisAlignment.center,
            children: const [
              FlutterLogo(size: 200),
              Text(
                'This is some longest text that should be centered'
                'together with the logo',
                textAlign: TextAlign.center,
              ),
            ],
          ),
        ),
      ],
    ),
  );
}
复制代码

如果居中的内容没有足够的空间,SliverFillRemaining 将变为可滚动。

如果没使用 SliverFillRemaining,内容将会像下面这样溢出:

Filling the remaining space

除了对内容居中有用之外,SliverFillRemaining 还会填充剩余视口的可用空间。为此,此部件必须放置在 CustomScrollView 中,并且必须是最后一个 sliver

如果没有足够的空间,部件将变为可滚动。

Widget build(BuildContext context) {
  return Scaffold(
    appBar: AppBar(title: Text('SliverFillRemaining')),
    body: CustomScrollView(
      slivers: [
        SliverList(
          delegate: SliverChildListDelegate(const [
            ListTile(title: Text('First item')),
            ListTile(title: Text('Second item')),
            ListTile(title: Text('Third item')),
            ListTile(title: Text('Fourth item')),
          ]),
        ),
        SliverFillRemaining(
          hasScrollBody: false,
          child: Container(
            color: Colors.yellowAccent,
            child: Column(
              mainAxisAlignment: MainAxisAlignment.center,
              children: const [
                FlutterLogo(size: 200),
                Text(
                  'This is some longest text that should be centered'
                  'together with the logo',
                  textAlign: TextAlign.center,
                ),
              ],
            ),
          ),
        ),
      ],
    ),
  );
}
复制代码

SizedBox

SizedBox 是最简单但是最常用的小部件之一。

SizedBox as ConstrainedBox

SizedBox 工作方式跟 ConstrainedBox 有些类似。

SizedBox.expand(
  child: Card(
    child: Text('Hello World!'),
    color: Colors.yellowAccent,
  ),
),
复制代码

SizedBox as padding

当需要添加内边距和外边距的时候,你可以选择 PaddingContainer 小部件。但是,它们可以比添加 Sizedbox 更冗长且可读性更低。

Column(
  children: <Widget>[
    Icon(Icons.star, size: 50),
    const SizedBox(height: 100),
    Icon(Icons.star, size: 50),
    Icon(Icons.star, size: 50),
  ],
),
复制代码

SizedBox as an Invisible Object

很多时候,你想通过设置一个 bool 值来隐藏/展示小部件。

ShowHide

Widget build(BuildContext context) {
  bool isVisible = true; // true or false
  return Scaffold(
    appBar: AppBar(
      title: Text('isVisible = $isVisible'),
    ),
    body: isVisible 
      ? Icon(Icons.star, size: 150) 
      : const SizedBox(),
  );
}
复制代码

因为 SizedBox 有一个 const 构造函数,所以使用 const SizeBox() 真的很便宜。一种更便宜的解决方案是使用 Opacity 小部件,将其 opacity 值更改为 0.0。这种解决方案的缺点是给定的小部件只是不可见,但是还是占用空间。

SafeArea

在不同的平台,有些特定的区域,比如安卓的状态栏或者 iPhone X 的刘海区块,我们不应该在其下面绘制内容。

解决方案是使用 SafeArea 小部件。(下面截图是没使用/使用 SafeArea

withoutwith

Widget build(BuildContext context) {
  return Material(
    color: Colors.blue,
    child: SafeArea(
      child: SizedBox.expand(
        child: Card(color: Colors.yellowAccent),
      ),
    ),
  );
}
复制代码

本文翻译自 Flutter Layout Cheat Sheet。代码已经验证,需要留意 RaisedButton 已经被 ElevatedButton 替代,在现实使用中需要留意。本文重点是其布局思路和技巧。


作者:Jimmy
链接:https://juejin.cn/post/7108530957976043528
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值