Flutter 各种布局方式汇总

本文会列举一组 Flutter 布局代码示例。因为 Flutter 相对于 Android 原生的 layout 布局或者是 Compose 布局,还是不太一样。如果不了解到全部的布局方式,在一些应用场景,就不能选择到最佳的布局方式来实现当前的需求。

目录:

  • Row and Column
  • IntrinsicWidth and IntrinsicHeight
  • Stack
  • Expanded
  • ConstrainedBox
  • Align
  • Container
    decoration: BoxDecoration
    • image: DecorationImage
    • border: Border
    • borderRadius: BorderRadius
    • shape: BoxShape
    • boxShadow: List<BoxShadow>
    • gradient: RadialGradient
    • backgroundBlendMode: BlendMode
  • Material
    • shape: BeveledRectangleBorder
  • Slivers
    • SliverFillRemaining
  • SizedBox
  • SafeArea

Row and Column

MainAxisAlignment

void main() => runApp(const MaterialApp(home: TestDemo1()));

class TestDemo1 extends StatelessWidget {

  const TestDemo1({super.key});

  @override
  Widget build(context) => Scaffold(
    appBar: AppBar(
        title: const Text('TestDemo1')
    ),
    body: Container(
        color: Colors.pink,
        width: double.infinity,
        height: 300,
        child: const Row(
          mainAxisAlignment: MainAxisAlignment.start,    <-- 替换此处
          children: <Widget>[
            Icon(Icons.star, size: 50),
            Icon(Icons.star, size: 50),
            Icon(Icons.star, size: 50),
          ],
        )
    ),
  );
}

可替换这个 mainAxisAlignment 的设置:

mainAxisAlignment: MainAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.end,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
mainAxisAlignment: MainAxisAlignment.spaceAround,

如果需要对齐不同文本的基线,则应使用 CrossAxisAlignment.baseline

Row(
  crossAxisAlignment: CrossAxisAlignment.baseline,
  textBaseline: TextBaseline.alphabetic,
  children: <Widget>[
    Text(
      'Baseline',
      style: Theme.of(context).textTheme.display3,
    ),
    Text(
      'Baseline',
      style: Theme.of(context).textTheme.body1,
    ),
  ],
),

CrossAxisAlignment

Row /*or Column*/( 
  crossAxisAlignment: CrossAxisAlignment.start,  <-- 替换此处
  children: <Widget>[
    Icon(Icons.star, size: 50),
    Icon(Icons.star, size: 200),
    Icon(Icons.star, size: 50),
  ],
),

可替换这个 crossAxisAlignment 的设置:

crossAxisAlignment: CrossAxisAlignment.end,
crossAxisAlignment: CrossAxisAlignment.stretch,
mainAxisSize: MainAxisSize.max,
mainAxisSize: MainAxisSize.min,

IntrinsicWidth and IntrinsicHeight

希望行或列中的所有小部件与最高/最宽的小部件一样高/宽?

如果你有这种布局:


Widget build(BuildContext context) {
  return Scaffold(
    appBar: AppBar(title: Text('IntrinsicWidth')),
    body: Center(
      child: Column(
        children: <Widget>[
          ElevatedButton(
            onPressed: () {},
            child: Text('Short'),
          ),
          ElevatedButton(
            onPressed: () {},
            child: Text('A bit Longer'),
          ),
          ElevatedButton(
            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>[
              ElevatedButton(
                onPressed: () {},
                child: Text('Short'),
              ),
              ElevatedButton(
                onPressed: () {},
                child: Text('A bit Longer'),
              ),
              ElevatedButton(
                onPressed: () {},
                child: Text('The Longest text button'),
              ),
            ],
          ),
        ),
      ),
    );
  }

如果您遇到类似的问题,但希望所有小部件与最高的小部件一样高,只需使用 IntrinsicHeightRow 小部件的组合即可。


Stack

非常适合将小部件相互叠加


@override
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,
      ),
    ],
  );
}

使用你自己的 Widget,你需要将它们放置在 Positioned Widget

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: 50),
            ),
            Positioned(
              top: constraints.maxHeight - iconSize,
              left: constraints.maxWidth - iconSize,
              child: Icon(Icons.call, size: 50),
            ),
          ],
        ),
    ),
  );
}

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

有时你很难将我们的小部件设置为适当的大小 - 例如,当你不想时,它会不断拉伸:


例如,当你有一个带有 CrossAxisAlignment.stretchColumn 并且你只希望按钮不被拉伸时,就会发生上述情况:

Widget build(BuildContext context) {
  return Scaffold(
    appBar: AppBar(title: Text('Align: without Align')),
    body: Column(
      crossAxisAlignment: CrossAxisAlignment.stretch,
      children: <Widget>[
        Align(
          child: RaisedButton(
            onPressed: () {},
            child: const Text('Button'),
          ),
        ),
      ],
    ),
  );
}

总是当你的小部件不听从你尝试设置的约束时,首先尝试用 Align 包装它。


Container

最常用的小部件之一 - 并且有充分的理由:

Container as a layout tool

当您不指定 Container 的高度和宽度时,它将与其子容器的大小相匹配

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

如果要拉伸 Container 以匹配其父级,请使用 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

您可以使用 color 属性来影响 Container 的背景、装饰和前景装饰。 (有了这两个属性,你可以完全改变 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"),
    ),
  );
}

decoration and foregroundDecoration
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

装饰通常用在容器小部件上以更改容器的外观。

image: DecorationImage

将图像作为背景:

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://img.alicdn.com/imgextra/i3/O1CN01cc4qI41HXKUftY8IO_!!6000000000767-2-tps-200-80.png',
          ),
        ),
      ),
    ),
  ),
);

border: Border

指定容器的边框应该是什么样子。


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

使边框角变圆。
如果装饰的形状是 BoxShape.circleborderRadius 不起作用

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

盒子装饰可以是矩形/正方形或椭圆/圆形。

对于任何其他形状,您可以使用 ShapeDecoration 而不是 BoxDecoration

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<BoxShadow>

向容器添加阴影。

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


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

LinearGradient

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,
          ],
        ),
      ),
    ),
  ),
);

RadialGradient
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],
        ),
      ),
    ),
  ),
);

SweepGradient
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 中最复杂的属性。
它负责将 BoxDecorationBoxDecoration 之上的任何内容的颜色/渐变混合在一起。

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

首先,我们将 BoxDecoration 设置为 foregroundDecoration,它绘制在 Container 的子项之上(而装饰则绘制在子项后面)。

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://img.alicdn.com/imgextra/i3/O1CN01cc4qI41HXKUftY8IO_!!6000000000767-2-tps-200-80.png',
      ),
    ),
  ),
);

backgroundBlendMode 不仅仅影响它所在的容器。

backgroundBlendMode 更改容器中小部件树上任何内容的颜色。
以下代码有一个绘制图像的父容器和使用 backgroundBlendMode 的子容器。 尽管如此,您仍然会得到与以前相同的效果。

Scaffold(
  appBar: AppBar(title: Text('backgroundBlendMode')),
  body: Center(
    child: Container(
      decoration: BoxDecoration(
        image: DecorationImage(
          image: NetworkImage(
            'https://img.alicdn.com/imgextra/i3/O1CN01cc4qI41HXKUftY8IO_!!6000000000767-2-tps-200-80.png',
          ),
        ),
      ),
      child: Container(
        height: 200,
        width: 200,
        foregroundDecoration: BoxDecoration(
          backgroundBlendMode: BlendMode.exclusion,
          gradient: LinearGradient(
            colors: const [
              Colors.red,
              Colors.blue,
            ],
          ),
        ),
      ),
    ),
  ),
);

Material

带有切角的边框


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

当你想要将内容居中时,即使没有足够的空间,此小部件也是不可替代的。


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 ,内容会溢出,如下所示:

填充剩余空间

除了有助于将内容居中之外,SliverFillRemaining 还将填充剩余视口的可用空间。 为此,这个小部件必须放置在 CustomScrollView 中,并且需要是最后一个条子


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

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 as ConstrainedBox

SizedBox 的工作方式与 ConstrainedBox 类似

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

SizedBox as padding

当需要添加填充或边距时,您可以选择填充或容器小部件。 但它们可能比添加 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

很多时候你想根据布尔值隐藏/显示小部件

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

因为 SizedBox 有一个 const 构造函数,所以使用 const SizedBox() 非常简单。

一种更简单的解决方案是使用 Opacity 小部件并将不透明度值更改为 0.0 。 该解决方案的缺点是给定的小部件只是不可见,但仍然会占用空间。

SafeArea

在不同的平台上,有一些特殊区域,例如 Android 上的状态栏或 iPhone X 上的刘海,我们可能会避免在其下方绘制。

这个问题的解决方案是 SafeArea 小部件(不带/带 SafeArea 的示例)


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

目前使用 Flutter 来布局还不是很熟练的朋友,希望看完本文后对你有一定的帮助。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,294评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,493评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,790评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,595评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,718评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,906评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,053评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,797评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,250评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,570评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,711评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,388评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,018评论 3 316
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,796评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,023评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,461评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,595评论 2 350

推荐阅读更多精彩内容