Flutter基建 - 布局组件全面解析

本篇基于Flutter 3.13.9,Dart 3.1.5版本

Flutter 3.13.9 • channel stable • github.com/flutter/flu...

Framework • revision d211f42860 (2 weeks ago) • 2023-10-25 13:42:25 -0700

Engine • revision 0545f8705d

Tools • Dart 3.1.5 • DevTools 2.25.0

本篇为Flutter基建的第五篇文章了,主要介绍Flutter中布局相关的组件,此篇文章涵盖了日常开发中经常使用的一些布局组件,希望大家可以通过本篇文章了解和熟悉Flutter中布局组件的相关知识。

Flutter基建 - Dart基础类型

Flutter基建 - Dart方法和类

Flutter基建 - 文本组件

Flutter基建 - 按钮全解析

Flutter基建 - 布局组件全面解析

线性组件

Flutter中线性布局提供了横向和纵向两种,Row为横向行布局,Column为纵向列布局,这两种布局类似于Android原生的LinearLayout,下面我们看看Row和Column的具体使用。

Row行组件

less 复制代码
Row(
  mainAxisAlignment: MainAxisAlignment.start,
  mainAxisSize: MainAxisSize.max,
  crossAxisAlignment: CrossAxisAlignment.start,
  children: [
    Container(
      width: 100,
      height: 100,
      color: Colors.red,
    ),
    Container(
      width: 100,
      height: 100,
      color: Colors.blue,
    ),
    const Text('Text'),
  ],
),

上述代码就实现了一个最基础的Row组件,内部包含了两个100像素容器和一个文本,这里我们需要关注重点参数为:

  • mainAxisAlignment:主轴的对齐方式,Row组件中主轴为x轴,可以设置start、end和center等方式;
  • mainAxisSize:主轴大小,也就是Row的宽度,可设置为max和min;
  • crossAxisAlignment:纵轴的对齐方式,Row组件中纵轴为y轴,也可以设置start、end和center等方式;
  • children:这个参数就不用多加介绍了,子组件。

了解完以上参数之后我们看下上述主轴为start对齐、主轴大小为max和纵轴为start对齐的具体效果:

下面是Flutter Inspector模式下的界面,可以清楚的看到Row的宽度为屏幕宽度,然后整体布局是靠左对齐,垂直方向为靠上对齐,接着我们将mainAxisAlignment和crossAxisAlignment都改为center再看下效果:

整体布局水平和垂直方向都变成了居中对齐了,这就是main和cross的alignment使用的效果。

最后我们再来看下mainAxisSize中max和min两种区别,上面我们定义的是max方式,然后我们将max改为min,mainAxisAlignment依旧为center,看看效果:

通过效果图可以看出Row的整体宽度不再是屏幕的宽度了,而是子组件的叠加宽度,此时MainAxisAlignment.center也不再具备效果,Column的宽度和子组件的叠加宽度一致也自然满足了居中的效果了。

Column列组件

了解了Row组件使用方式之后,Column上手就有如鱼得水,它其实就是垂直方向上的Row组件,下面我们也简单看下Column的使用方式。

less 复制代码
Column(
  mainAxisAlignment: MainAxisAlignment.center,
  mainAxisSize: MainAxisSize.max,
  crossAxisAlignment: CrossAxisAlignment.center,
  children: [
    Container(
      width: 100,
      height: 100,
      color: Colors.red,
      child: const Text('Red Container'),
    ),
    Container(
      width: 100,
      height: 100,
      color: Colors.blue,
      child: const Text('Blue Container'),
    ),
    const Text('Text'),
  ],
),

通过Inspector效果图可以看出,Column组件的高度是除状态栏和标题栏之后的整个高度,而宽度则变成了子组件的最大宽度,用法和Row基本是一致的,只需要注意主轴和纵轴的区分就可以了,这里就不再过多介绍了哈。

叠加组件

Stack组件

Stack组件看名字有点难以理解,堆栈组件是啥个意思呢?其实就是按照指定的对齐方式依次往上叠加的一种组件,子组件会按照顺序依次叠加在层次上,最上面的组件会盖住下层的组件,下面我们先了解下最简单的Stack组件实现方式。

less 复制代码
Stack(
  children: [
    Container(
      width: 100,
      height: 100,
      color: Colors.blue,
    ),
    Container(
      width: 50,
      height: 50,
      color: Colors.red,
    ),
    const Text('Text'),
  ],
),

上面代码中Stack组件只设置了children一个参数,内部包含了100像素的蓝色Container、50像素的红色Container和一个文本组件,最终的效果如上图所示,它们按照顺序依次叠加到Stack中,红色的Container遮住了蓝色的Container一部分,接下来我们在增加额外参数试试效果。

less 复制代码
Stack(
  alignment: AlignmentDirectional.center,
  children: [
    Container(
      width: 100,
      height: 100,
      color: Colors.blue,
    ),
    Container(
      width: 50,
      height: 50,
      color: Colors.red,
    ),
    const Text('Text'),
  ],
),

我们给Stack指定了alignment参数为AlignmentDirectional.center属性,它的默认值为AlignmentDirectional.topStart,这时候Stack子组件应该是按照居中的方式来依次叠加,效果如下:

再来看看Stack中fit参数,fit参数默认值为StackFit.loose,意思是Stack不会干扰子组件的大小,子组件该多大就是多大,也就是上面我们看到的效果,两个Container都是采用的自身定义的长宽;fit还有一张就是StackFit.expand,这个值就比较有意思,它会让未定位的(这里在Positioned组件中会介绍)子组件忽略掉自身的大小,从而变成Stack的大小,我们来看看具体效果:

less 复制代码
SizedBox(
  width: 200,
  height: 200,
  child: Stack(
    fit: StackFit.expand,
    children: [
      Container(
        width: 100,
        height: 100,
        color: Colors.blue,
      ),
      Container(
        width: 50,
        height: 50,
        color: Colors.red,
      ),
      const Text('Text'),
    ],
  ),
),

这里我们使用SizeBox将Stack的长宽约束为200,然后子组件中Container长宽不变,然而最终的效果却是Container的长宽都变成了200,自身设置的长宽参数失效了,这就导致有可能最上面的子组件会将下方所有的组件都给遮盖住,所以大家在日常开发使用中还是要按需采用哈。

Positioned组件

Positioned组件可以根据左上右下四个位置来实现组件的绝对定位,配合Stack可以完美的将子组件定位在Stack中,Positioned组件的构造方法非常简单:

kotlin 复制代码
const Positioned({
    super.key,
    this.left,
    this.top,
    this.right,
    this.bottom,
    this.width,
    this.height,
    required super.child,
  })

可以设置left、top、right和bottom来实现四个位置的定义效果;width和height用于设置组件的长度和宽度属性。

less 复制代码
Stack(
  children: [
    Positioned(
      left: 20,
      child: Container(
        width: 100,
        height: 100,
        color: Colors.blue,
      ),
    ),
    Positioned(
      left: 50,
      top: 50,
      child: Container(
        width: 50,
        height: 50,
        color: Colors.red,
      ),
    ),
  ],
),

通过Positioned的left和top我们就可以将自身的位置精准的定位到指定的坐标。

在上面介绍Stack组件的时候我们提到了StackFit.expand可以将未定位的子组件扩展到自身的大小,而经过定位的子组件也会有同样的效果么,我们实践下试试效果:

less 复制代码
SizedBox(
  width: 200,
  height: 200,
  child: Stack(
    fit: StackFit.expand,
    children: [
      Container(
        width: 150,
        height: 150,
        color: Colors.black,
      ),
      Positioned(
        left: 20,
        child: Container(
          width: 100,
          height: 100,
          color: Colors.blue,
        ),
      ),
      Positioned(
        left: 50,
        top: 50,
        child: Container(
          width: 50,
          height: 50,
          color: Colors.red,
        ),
      ),
    ],
  ),
),

上面代码中我们将Stack组件的fit属性设置为StackFit.expand,子组件中大小为150的Container为使用Positioned进行定位,后面两个都包裹了Positioned组件进行定位,在这种情况下,expand属性只会将大小150的Container扩展到自身200像素的大小,而使用Positioned包裹的Container则不会改变大小,这就是expand在Positioned下特殊的效果,具体界面如下:

流式组件

Wrap组件

Warp组件作为Flutter提供的流式布局,在用法上面也是提供了非常多的可配置性,可设置主轴方向,包括主轴和纵轴的对其方式、子布局在主轴和纵轴方向的边距属性,下面我们就看看Wrap是如何进行配置的吧~

less 复制代码
Widget buildWrap(BuildContext context) {
  List<Color> containerColor = [Colors.black, Colors.red, Colors.blue, Colors.green, Colors.amber];
  return buildScaffold(
    context,
    SizedBox(
      width: double.maxFinite,
      height: double.maxFinite,
      child: Wrap(
        direction: Axis.horizontal,
        spacing: 10,
        runSpacing: 10,
        alignment: WrapAlignment.center,
        runAlignment: WrapAlignment.center,
        verticalDirection: VerticalDirection.down,
        children: [
          buildContainer(containerColor[0]),
          buildContainer(containerColor[1]),
          buildContainer(containerColor[2]),
          buildContainer(containerColor[3]),
          buildContainer(containerColor[4]),
      	],
    	),
  	),
	);
}

Container buildContainer(Color containerColor) {
  return Container(
    width: 100,
    height: 100,
    color: containerColor,
  );
}

这里我们先通过SizeBox将Wrap长度和宽度都设置为充满屏幕,然后通过direction将主轴方向设置为水平方向,spacing参数为主轴方向子组件的边距,runSpacing参数为纵轴方向上子组件的边距,alignment参数为主轴方向子组件的对齐方式,runAlignment参数为纵轴方向子组件的对齐方式,verticalDiretion这个参数为垂直方向是如何排列,这里使用的默认值down,先来看看效果,最后我们会单独介绍verticalDirection参数。

通过上面效果图可以看出,一行只够三个Container的宽度,当排列不下的时候自动转到下一行,并且在主轴和纵轴方向上都是居中排列。

这时候5个Container都是按照顺序依次排列下来,此时我们将verticalDirection改为up再来看看是如何排列的。

可以看到5个Container的上下位置颠倒过来了,不需要我们改变Container的顺序,只需要将VerticalDiraction设置为up即可。

Flow组件

Flow组件也是可以达到Wrap一样的流式布局的效果,但是它的实现要复杂的多,如果不是定制化的流式布局不建议使用Flow来完成,Flow需要自定义FLowDelegate来完成子组件的排列组合。

下面我们来简单实现一个类似于Wrap的效果。

ini 复制代码
Widget buildFlow(BuildContext context) {
  List<Color> containerColor = [Colors.black, Colors.red, Colors.blue, Colors.green, Colors.amber];
  return buildScaffold(
    context,
    Flow(
      delegate: BuildFlowDelegate(),
      children: [
        buildContainer(containerColor[0]),
        buildContainer(containerColor[1]),
        buildContainer(containerColor[2]),
        buildContainer(containerColor[3]),
        buildContainer(containerColor[4]),
      ],
    ),
  );
}

class BuildFlowDelegate extends FlowDelegate {
  double width = 0;
  double height = 0;

  BuildFlowDelegate();

  @override
  void paintChildren(FlowPaintingContext context) {
    var x = 0.0;
    var y = 0.0;
    for (int i = 0; i < context.childCount; i++) {
      var childX = context.getChildSize(i)!.width + x;
      // 如果一行剩余的宽度还够绘制一个子组件,那么就在这行继续绘制;
      // 如果一行剩余宽度已经不够绘制一个子组件了,那么就换行开始绘制。
      if (childX < context.size.width) {
        context.paintChild(i, transform: Matrix4.translationValues(x, y, 0.0));
        x = childX;
      } else {
        x = 0;
        y += context.getChildSize(i)!.height;
        context.paintChild(i, transform: Matrix4.translationValues(x, y, 0.0));
        x += context.getChildSize(i)!.width;
      }
    }
  }

  @override
  bool shouldRepaint(covariant FlowDelegate oldDelegate) {
    return oldDelegate != this;
  }
}

BuildFlowDelegate类就是自定义的FlowDelegte,需要在它的paintChildren方法中完成子组件的排列规则,这里我们采用的是最简单一行剩余宽度是否满足绘制下一个子组件的规则来绘制,这里我们没有考虑给子组件设置边距的属性,只是在判断条件中考虑了组件的宽度,具体实现的效果如下所示:

Flow在实现普通流式布局的场景下过于复杂了,一般直接考虑使用Wrap来完成即可,如果Wrap组件不满足你的实线要求,这时候就可以采用Flow来作一些特殊的定制化效果。

约束组件

Flutter中约束组件主要针对的是布局的大小进行限制,它可以很好的帮助我们管理组件的大小,也是本篇文章最简单的一节内容了,下面我们逐一看下ConstrainedBox、UnConstrainedBox和SizeBox的具体使用方式。

ConstrainedBox组件

ConstrainedBox约束主要是通过constraints参数来决定,它可以限制最小宽高和最大宽高属性。

less 复制代码
Widget buildConstraints(BuildContext context) {
  return buildScaffold(
    context,
    ConstrainedBox(
      constraints: const BoxConstraints(minWidth: 50, minHeight: 50, maxWidth: 200, maxHeight: 200),
      child: Container(
        width: 10,
        height: 10,
        color: Colors.red,
      ),
    ),
  );
}

这里我们将ConstrainedBox的最小宽高设置为50,最大宽高设置为200,然后它的子组件Container宽高设置为10,此时Container最终显示宽高则被constraints的属性限制了,并没有采用自身10像素的大小,而是变成constraints的最小宽高50。

如果我们将Container的宽高设置为500,那么它也会被constraints最大宽高200给限制,并不能达到自身500的大小。

这时候我们将不被ConstrainedBox给限制该如何实现呢,就需要UnConstrainedBox来取消限制了。

UnConstrainedBox组件

less 复制代码
Widget buildConstraints(BuildContext context) {
  return buildScaffold(
    context,
    ConstrainedBox(
      constraints: const BoxConstraints(minWidth: 50, minHeight: 50, maxWidth: 200, maxHeight: 200),
      child: UnconstrainedBox(
        child: Container(
          width: 10,
          height: 10,
          color: Colors.red,
        ),
      ),
    ),
  );
}

这里即使ConstrainedBox设置了最小宽高50的限制,我们依然可以通过UnconstrainedBox来取消这一限制,Container会按照自身的大小进行绘制。

SizeBox组件

SizeBox和ConstrainedBox不同的是,ConstrainedBox限制的是一个范围,而SizeBox限制的是具体值,SizeBox需要传入具体的宽高值,子组件需要按照这个值进行绘制。

less 复制代码
Widget buildSizeBox(BuildContext context) {
  return buildScaffold(
    context,
    SizedBox(
      width: 100,
      height: 100,
      child: Container(
        width: 200,
        height: 200,
        color: Colors.red,
      ),
    ),
  );
}

这里即使Container设置了宽高为200的属性,但是上层的SizeBox只设置了宽高100的属性,所以Container只能按照SizeBox设置的大小进行绘制。

写在最后

本篇文章全面的介绍了Flutter中布局组件的相关知识,希望可以帮助大家了进一步了解和熟悉布局组件的相关知识,后续会循序渐进逐步接触Flutter更多的知识。

我是Taonce,如果觉得本文对你有所帮助,帮忙关注、赞或者收藏三连一下,谢谢😆😆~

相关推荐
王晓枫14 分钟前
flutter接入三方库运行报错:Error running pod install
前端·flutter
砖厂小工6 小时前
用 GLM + OpenClaw 打造你的 AI PR Review Agent — 让龙虾帮你审代码
android·github
张拭心7 小时前
春节后,有些公司明确要求 AI 经验了
android·前端·人工智能
张拭心7 小时前
Android 17 来了!新特性介绍与适配建议
android·前端
shankss8 小时前
Flutter 下拉刷新库 pull_to_refresh_plus 设计与实现分析
flutter
Kapaseker9 小时前
Compose 进阶—巧用 GraphicsLayer
android·kotlin
黄林晴10 小时前
Android17 为什么重写 MessageQueue
android
忆江南1 天前
iOS 深度解析
flutter·ios
明君879971 天前
Flutter 实现 AI 聊天页面 —— 记一次 Markdown 数学公式显示的踩坑之旅
前端·flutter
恋猫de小郭1 天前
移动端开发稳了?AI 目前还无法取代客户端开发,小红书的论文告诉你数据
前端·flutter·ai编程