MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

Flutte弹性布局Flex与Expanded详解

2021-04-096.2k 阅读

Flutter弹性布局基础概念

在 Flutter 开发中,布局是构建用户界面的重要环节。弹性布局(Flex Layout)是一种灵活且强大的布局方式,它能帮助开发者在不同屏幕尺寸和方向上有效地排列和分配子部件的空间。Flex 布局基于 Flexbox 模型,这在前端开发领域是一种广泛使用的布局理念。

Flex 布局的核心思想是通过主轴(main axis)和交叉轴(cross axis)来管理子部件的排列和空间分配。主轴是子部件排列的主要方向,而交叉轴则垂直于主轴。这种布局方式使得开发者可以轻松地实现响应式设计,确保应用在各种设备上都能呈现出良好的视觉效果。

Flex 部件介绍

Flex 构造函数

Flex 部件是 Flutter 中实现弹性布局的关键部件之一。它的构造函数如下:

Flex({
  Key key,
  @required this.direction,
  List<Widget> children = const <Widget>[],
  this.mainAxisAlignment = MainAxisAlignment.start,
  this.mainAxisSize = MainAxisSize.max,
  this.crossAxisAlignment = CrossAxisAlignment.center,
  this.textDirection,
  this.verticalDirection = VerticalDirection.down,
  this.textBaseline,
})
  • direction:指定主轴的方向,取值为 Axis.horizontalAxis.vertical。这决定了子部件是水平排列还是垂直排列。
  • children:包含要在 Flex 布局中排列的子部件列表。
  • mainAxisAlignment:控制子部件在主轴上的对齐方式。常见取值有 MainAxisAlignment.start(起始位置对齐)、MainAxisAlignment.end(结束位置对齐)、MainAxisAlignment.center(居中对齐)、MainAxisAlignment.spaceAround(子部件之间及两端均匀分布空间)、MainAxisAlignment.spaceBetween(子部件之间均匀分布空间,两端不留空间)。
  • mainAxisSize:定义 Flex 在主轴方向上占用的空间大小,取值为 MainAxisSize.max(尽可能占用最大空间)或 MainAxisSize.min(仅占用子部件所需的最小空间)。
  • crossAxisAlignment:控制子部件在交叉轴上的对齐方式。常见取值有 CrossAxisAlignment.startCrossAxisAlignment.endCrossAxisAlignment.centerCrossAxisAlignment.stretch(拉伸以填满交叉轴空间)。
  • textDirection:指定文本方向,对于从左到右的语言(如英语)通常为 TextDirection.ltr,从右到左的语言(如阿拉伯语)为 TextDirection.rtl。它会影响主轴方向上子部件的排列顺序。
  • verticalDirection:当 directionAxis.vertical 时,该属性指定子部件的垂直排列方向,取值为 VerticalDirection.down(从上到下)或 VerticalDirection.up(从下到上)。
  • textBaseline:用于文本对齐的基线,一般在处理文本相关布局时使用。

水平 Flex 布局示例

下面是一个简单的水平 Flex 布局示例:

import 'package:flutter/material.dart';

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        appBar: AppBar(
          title: Text('Horizontal Flex Layout'),
        ),
        body: Flex(
          direction: Axis.horizontal,
          children: <Widget>[
            Container(
              width: 100,
              height: 100,
              color: Colors.red,
            ),
            Container(
              width: 100,
              height: 100,
              color: Colors.green,
            ),
            Container(
              width: 100,
              height: 100,
              color: Colors.blue,
            ),
          ],
        ),
      ),
    );
  }
}

在这个示例中,Flexdirection 设置为 Axis.horizontal,子部件 Container 水平排列。每个 Container 都有固定的宽度和高度,它们按照顺序从左到右排列在主轴上。

垂直 Flex 布局示例

import 'package:flutter/material.dart';

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        appBar: AppBar(
          title: Text('Vertical Flex Layout'),
        ),
        body: Flex(
          direction: Axis.vertical,
          children: <Widget>[
            Container(
              width: 100,
              height: 100,
              color: Colors.red,
            ),
            Container(
              width: 100,
              height: 100,
              color: Colors.green,
            ),
            Container(
              width: 100,
              height: 100,
              color: Colors.blue,
            ),
          ],
        ),
      ),
    );
  }
}

此示例中,Flexdirection 设置为 Axis.vertical,子部件 Container 垂直排列,从顶部开始依次向下排列在主轴上。

Flex 布局属性深入分析

mainAxisAlignment 属性

  1. MainAxisAlignment.start:子部件从主轴的起始位置开始排列。例如,在水平主轴方向上,子部件从左到右排列;在垂直主轴方向上,子部件从上到下排列。
Flex(
  direction: Axis.horizontal,
  mainAxisAlignment: MainAxisAlignment.start,
  children: <Widget>[
    Container(
      width: 100,
      height: 100,
      color: Colors.red,
    ),
    Container(
      width: 100,
      height: 100,
      color: Colors.green,
    ),
    Container(
      width: 100,
      height: 100,
      color: Colors.blue,
    ),
  ],
)
  1. MainAxisAlignment.end:子部件从主轴的结束位置开始排列。在水平主轴方向上,子部件从右到左排列;在垂直主轴方向上,子部件从下到上排列。
Flex(
  direction: Axis.horizontal,
  mainAxisAlignment: MainAxisAlignment.end,
  children: <Widget>[
    Container(
      width: 100,
      height: 100,
      color: Colors.red,
    ),
    Container(
      width: 100,
      height: 100,
      color: Colors.green,
    ),
    Container(
      width: 100,
      height: 100,
      color: Colors.blue,
    ),
  ],
)
  1. MainAxisAlignment.center:子部件在主轴上居中排列。如果是水平主轴,子部件在水平方向上居中;如果是垂直主轴,子部件在垂直方向上居中。
Flex(
  direction: Axis.horizontal,
  mainAxisAlignment: MainAxisAlignment.center,
  children: <Widget>[
    Container(
      width: 100,
      height: 100,
      color: Colors.red,
    ),
    Container(
      width: 100,
      height: 100,
      color: Colors.green,
    ),
    Container(
      width: 100,
      height: 100,
      color: Colors.blue,
    ),
  ],
)
  1. MainAxisAlignment.spaceAround:子部件之间以及两端均匀分布空间。每个子部件两侧的空白空间相等。
Flex(
  direction: Axis.horizontal,
  mainAxisAlignment: MainAxisAlignment.spaceAround,
  children: <Widget>[
    Container(
      width: 100,
      height: 100,
      color: Colors.red,
    ),
    Container(
      width: 100,
      height: 100,
      color: Colors.green,
    ),
    Container(
      width: 100,
      height: 100,
      color: Colors.blue,
    ),
  ],
)
  1. MainAxisAlignment.spaceBetween:子部件之间均匀分布空间,但两端不留空间。
Flex(
  direction: Axis.horizontal,
  mainAxisAlignment: MainAxisAlignment.spaceBetween,
  children: <Widget>[
    Container(
      width: 100,
      height: 100,
      color: Colors.red,
    ),
    Container(
      width: 100,
      height: 100,
      color: Colors.green,
    ),
    Container(
      width: 100,
      height: 100,
      color: Colors.blue,
    ),
  ],
)

mainAxisSize 属性

  1. MainAxisSize.max:Flex 在主轴方向上会尽可能占用最大空间。例如,当 directionAxis.horizontalmainAxisSizeMainAxisSize.max 时,Flex 会在水平方向上填满可用空间,子部件会根据其他属性(如 mainAxisAlignment)进行排列。
Flex(
  direction: Axis.horizontal,
  mainAxisSize: MainAxisSize.max,
  mainAxisAlignment: MainAxisAlignment.spaceEvenly,
  children: <Widget>[
    Container(
      width: 100,
      height: 100,
      color: Colors.red,
    ),
    Container(
      width: 100,
      height: 100,
      color: Colors.green,
    ),
    Container(
      width: 100,
      height: 100,
      color: Colors.blue,
    ),
  ],
)
  1. MainAxisSize.min:Flex 在主轴方向上仅占用子部件所需的最小空间。这意味着 Flex 的大小将根据子部件的总大小来确定,不会额外扩展。
Flex(
  direction: Axis.horizontal,
  mainAxisSize: MainAxisSize.min,
  children: <Widget>[
    Container(
      width: 100,
      height: 100,
      color: Colors.red,
    ),
    Container(
      width: 100,
      height: 100,
      color: Colors.green,
    ),
    Container(
      width: 100,
      height: 100,
      color: Colors.blue,
    ),
  ],
)

crossAxisAlignment 属性

  1. CrossAxisAlignment.start:子部件在交叉轴的起始位置对齐。例如,当主轴为水平方向时,子部件在垂直方向的顶部对齐。
Flex(
  direction: Axis.horizontal,
  crossAxisAlignment: CrossAxisAlignment.start,
  children: <Widget>[
    Container(
      width: 100,
      height: 50,
      color: Colors.red,
    ),
    Container(
      width: 100,
      height: 100,
      color: Colors.green,
    ),
    Container(
      width: 100,
      height: 150,
      color: Colors.blue,
    ),
  ],
)
  1. CrossAxisAlignment.end:子部件在交叉轴的结束位置对齐。当主轴为水平方向时,子部件在垂直方向的底部对齐。
Flex(
  direction: Axis.horizontal,
  crossAxisAlignment: CrossAxisAlignment.end,
  children: <Widget>[
    Container(
      width: 100,
      height: 50,
      color: Colors.red,
    ),
    Container(
      width: 100,
      height: 100,
      color: Colors.green,
    ),
    Container(
      width: 100,
      height: 150,
      color: Colors.blue,
    ),
  ],
)
  1. CrossAxisAlignment.center:子部件在交叉轴上居中对齐。当主轴为水平方向时,子部件在垂直方向上居中。
Flex(
  direction: Axis.horizontal,
  crossAxisAlignment: CrossAxisAlignment.center,
  children: <Widget>[
    Container(
      width: 100,
      height: 50,
      color: Colors.red,
    ),
    Container(
      width: 100,
      height: 100,
      color: Colors.green,
    ),
    Container(
      width: 100,
      height: 150,
      color: Colors.blue,
    ),
  ],
)
  1. CrossAxisAlignment.stretch:子部件在交叉轴上拉伸以填满可用空间。例如,当主轴为水平方向时,子部件的高度会拉伸以填满 Flex 的垂直空间。
Flex(
  direction: Axis.horizontal,
  crossAxisAlignment: CrossAxisAlignment.stretch,
  children: <Widget>[
    Container(
      width: 100,
      color: Colors.red,
    ),
    Container(
      width: 100,
      color: Colors.green,
    ),
    Container(
      width: 100,
      color: Colors.blue,
    ),
  ],
)

Expanded 部件详解

Expanded 作用

Expanded 部件是 Flex 布局中用于灵活分配剩余空间的重要部件。它可以使子部件在主轴方向上按比例分配剩余空间。Expanded 部件必须是 Flex 部件的直接子部件。

Expanded 构造函数

Expanded({
  Key key,
  int flex = 1,
  @required Widget child,
})
  • flex:用于指定子部件在分配剩余空间时的比例因子。默认值为 1。如果有多个 Expanded 子部件,它们会按照 flex 的比例来分配剩余空间。
  • child:需要在 Expanded 中显示的子部件。

简单 Expanded 示例

import 'package:flutter/material.dart';

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        appBar: AppBar(
          title: Text('Expanded Example'),
        ),
        body: Flex(
          direction: Axis.horizontal,
          children: <Widget>[
            Expanded(
              flex: 1,
              child: Container(
                color: Colors.red,
              ),
            ),
            Expanded(
              flex: 2,
              child: Container(
                color: Colors.green,
              ),
            ),
          ],
        ),
      ),
    );
  }
}

在这个示例中,水平 Flex 布局中有两个 Expanded 子部件。第一个 Expandedflex1,第二个 Expandedflex2。剩余空间会按照 1:2 的比例分配给这两个子部件,所以绿色容器的宽度将是红色容器宽度的两倍。

Expanded 与非 Expanded 子部件混合使用

import 'package:flutter/material.dart';

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        appBar: AppBar(
          title: Text('Mixed Expanded and Non - Expanded'),
        ),
        body: Flex(
          direction: Axis.horizontal,
          children: <Widget>[
            Container(
              width: 100,
              height: 100,
              color: Colors.red,
            ),
            Expanded(
              flex: 1,
              child: Container(
                color: Colors.green,
              ),
            ),
            Container(
              width: 150,
              height: 100,
              color: Colors.blue,
            ),
          ],
        ),
      ),
    );
  }
}

在这个例子中,有一个固定宽度的红色 Container,一个 Expanded 的绿色 Container 和另一个固定宽度的蓝色 ContainerExpanded 的绿色 Container 会在红色和蓝色 Container 占据固定宽度后,分配剩余的水平空间。

Expanded 原理深入剖析

空间分配算法

Flex 布局中有 Expanded 子部件时,空间分配过程如下:

  1. 首先,Flex 会计算所有非 Expanded 子部件在主轴方向上所需的空间总和。
  2. 然后,Flex 会确定自身在主轴方向上的可用空间(由 mainAxisSize 和父部件的约束决定)。
  3. 接着,从可用空间中减去非 Expanded 子部件所需的空间,得到剩余空间。
  4. 最后,按照 Expanded 子部件的 flex 比例,将剩余空间分配给各个 Expanded 子部件。

例如,假设有一个水平 Flex,总宽度为 400,其中有一个宽度为 100 的非 Expanded 子部件,以及两个 Expanded 子部件,flex 分别为 1 和 2。非 Expanded 子部件占用 100 宽度,剩余空间为 400 - 100 = 300。按照 1:2 的比例,第一个 Expanded 子部件将获得 100 宽度(300 * 1 / (1 + 2)),第二个 Expanded 子部件将获得 200 宽度(300 * 2 / (1 + 2))。

嵌套 Expanded 情况

在嵌套 Expanded 时,需要注意空间分配的层次。例如,在一个垂直 Flex 中有一个水平 Flex,水平 Flex 中有多个 Expanded 子部件。外层垂直 Flex 会先按照自身规则分配空间给内层水平 Flex,然后内层水平 Flex 再对其内部的 Expanded 子部件进行空间分配。

import 'package:flutter/material.dart';

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        appBar: AppBar(
          title: Text('Nested Expanded'),
        ),
        body: Flex(
          direction: Axis.vertical,
          children: <Widget>[
            Expanded(
              flex: 1,
              child: Flex(
                direction: Axis.horizontal,
                children: <Widget>[
                  Expanded(
                    flex: 1,
                    child: Container(
                      color: Colors.red,
                    ),
                  ),
                  Expanded(
                    flex: 2,
                    child: Container(
                      color: Colors.green,
                    ),
                  ),
                ],
              ),
            ),
            Expanded(
              flex: 2,
              child: Container(
                color: Colors.blue,
              ),
            ),
          ],
        ),
      ),
    );
  }
}

在这个示例中,外层垂直 Flex 有两个 Expanded 子部件,比例为 1:2。第一个 Expanded 子部件内部是一个水平 Flex,又有两个 Expanded 子部件,比例为 1:2。这就形成了一个复杂但有序的空间分配结构。

Flex 与 Expanded 在实际项目中的应用场景

导航栏布局

在应用的导航栏中,常常需要使用 FlexExpanded 来实现不同元素的合理排列。例如,一个常见的导航栏可能包含一个左侧的菜单按钮、中间的标题和右侧的操作按钮。

import 'package:flutter/material.dart';

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        appBar: AppBar(
          title: Flex(
            direction: Axis.horizontal,
            children: <Widget>[
              IconButton(
                icon: Icon(Icons.menu),
                onPressed: () {},
              ),
              Expanded(
                child: Center(
                  child: Text('App Title'),
                ),
              ),
              IconButton(
                icon: Icon(Icons.search),
                onPressed: () {},
              ),
            ],
          ),
        ),
        body: Container(),
      ),
    );
  }
}

在这个示例中,Flex 布局用于水平排列菜单按钮、标题和搜索按钮。Expanded 部件使得标题能够在剩余空间中居中显示,并且随着屏幕宽度变化自适应。

卡片式布局

卡片式布局在移动应用中非常常见,比如展示文章列表、产品列表等。可以使用 FlexExpanded 来构建卡片的内部结构。

import 'package:flutter/material.dart';

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        appBar: AppBar(
          title: Text('Card Layout'),
        ),
        body: ListView.builder(
          itemCount: 5,
          itemBuilder: (context, index) {
            return Card(
              child: Flex(
                direction: Axis.horizontal,
                children: <Widget>[
                  Container(
                    width: 80,
                    height: 80,
                    color: Colors.grey[300],
                    child: Icon(Icons.image),
                  ),
                  Expanded(
                    child: Padding(
                      padding: EdgeInsets.all(16.0),
                      child: Column(
                        crossAxisAlignment: CrossAxisAlignment.start,
                        children: <Widget>[
                          Text('Article Title'),
                          SizedBox(height: 8),
                          Text('Article summary...'),
                        ],
                      ),
                    ),
                  ),
                ],
              ),
            );
          },
        ),
      ),
    );
  }
}

在这个卡片布局中,左侧是一个固定大小的图标容器,右侧使用 Expanded 来填充剩余空间,展示文章标题和摘要。这样可以确保卡片在不同屏幕宽度下都能保持良好的布局。

响应式布局

在响应式设计中,FlexExpanded 起着关键作用。通过检测屏幕尺寸,可以动态调整 Flex 的方向和 Expanded 的比例。

import 'package:flutter/material.dart';

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    final isLandscape = MediaQuery.of(context).orientation == Orientation.landscape;
    return MaterialApp(
      home: Scaffold(
        appBar: AppBar(
          title: Text('Responsive Layout'),
        ),
        body: Flex(
          direction: isLandscape? Axis.horizontal : Axis.vertical,
          children: <Widget>[
            Expanded(
              flex: isLandscape? 2 : 1,
              child: Container(
                color: Colors.red,
              ),
            ),
            Expanded(
              flex: isLandscape? 1 : 2,
              child: Container(
                color: Colors.green,
              ),
            ),
          ],
        ),
      ),
    );
  }
}

在这个示例中,根据屏幕的横竖屏状态,Flex 的方向和 Expanded 的比例会动态变化,从而实现响应式布局。

常见问题及解决方法

Expanded 子部件不显示

  1. 问题描述:在使用 Expanded 时,发现子部件没有显示出来。
  2. 可能原因
    • 子部件本身没有设置合适的尺寸或颜色,导致看起来没有显示。例如,一个透明且没有大小的 Container 放在 Expanded 中就不会显示。
    • Flex 的父部件没有足够的空间,导致 Expanded 无法分配到空间。比如,一个 Flex 放在一个宽度为 0 的 Container 中,Expanded 子部件就没有空间显示。
  3. 解决方法
    • 确保子部件设置了合适的尺寸、颜色或其他可见属性。例如,给 Container 设置宽度、高度和颜色。
    • 检查 Flex 的父部件是否有足够的空间。可以通过设置父部件的尺寸或者让父部件自适应屏幕空间来解决。

Flex 布局中元素重叠

  1. 问题描述:在 Flex 布局中,子部件出现重叠现象。
  2. 可能原因
    • 子部件的尺寸设置不合理,导致超出了 Flex 布局分配的空间。例如,两个固定宽度的子部件总宽度超过了 Flex 的可用宽度。
    • mainAxisAlignmentcrossAxisAlignment 设置不当,使得子部件的位置发生冲突。比如,将 crossAxisAlignment 设置为 CrossAxisAlignment.stretch 时,如果子部件有固定高度,可能会导致重叠。
  3. 解决方法
    • 调整子部件的尺寸,确保它们在 Flex 布局的可用空间内。可以使用 Expanded 来灵活分配空间,或者调整固定尺寸子部件的大小。
    • 检查并正确设置 mainAxisAlignmentcrossAxisAlignment 属性,根据实际需求选择合适的对齐方式。

嵌套 Flex 布局性能问题

  1. 问题描述:在复杂的嵌套 Flex 布局中,应用可能会出现性能下降的情况。
  2. 可能原因
    • 过多的嵌套层次会增加布局计算的复杂度。每一层 Flex 都需要计算子部件的大小和位置,嵌套层数越多,计算量越大。
    • 频繁的布局重建也会影响性能。如果在 Flex 布局中的某个子部件频繁更新,可能会导致整个布局重新计算。
  3. 解决方法
    • 尽量减少嵌套层次。可以通过合理规划布局结构,使用其他布局方式(如 StackGridView 等)来替代一些不必要的 Flex 嵌套。
    • 使用 const 构造函数来创建不变的子部件,这样可以避免不必要的布局重建。同时,将频繁更新的部件与布局结构分离,减少对整体布局的影响。

通过深入理解 FlexExpanded 的原理、属性以及在实际项目中的应用场景,并解决常见问题,开发者能够在 Flutter 前端开发中更加高效地构建灵活、美观且性能良好的用户界面。无论是简单的页面布局还是复杂的响应式设计,FlexExpanded 都是强大的工具。