文章首发博客网站,由于格式兼容问题,可前往阅读原文

如果你是一名web开发者应该对于元素的布局不陌生,直接给目标元素定义尺寸就可以了,如css的width/height 、android的layout_width等等,但在flutter中同样的尺寸定义可能并不会呈现出自己想要的效果

扫码关注公众号,查看更多优质文章 🔽 🔽 🔽

来看下面的2段代码:

  1. 片段1在最外层的Container中塞进了一个FlutterLogo,并设置了它的长宽都为100
  2. 片段2在第1段代码的基础上包裹了一层Center
// part1
Widget build(BuildContext context) {
  return Container(
    color: Colors.red,
    child: const FlutterLogo(
      size: 100, // 宽高为100 的flutter logo
    ),
  );
}

// part2
Widget build(BuildContext context) {
  return Center(
    child: Container(
      color: Colors.red,
      child: const FlutterLogo(
        size: 100, // 宽高为100 的flutter logo
      ),
    ),
  );
}

对于以上的布局相信大家大致都有基本的效果:页面上有2个长宽都为100且背景为红色的FluterLogo,其中一个居中显示。按道理也是这样的效果,可结果却出乎意料😂 :

很明显片段1中的logo尺寸不是100,片段2中的尺寸是正确的。你是不是很惊讶,Center不就是仅仅将元素居中排列吗,为什么二者显示差异这么大?

带着疑问我们将以上的代码稍作调整:改变背景区域的大小

Widget build(BuildContext context) {
  return Center(
    child: Container(
      width: 200,    // 改变背景区域的大小
      height: 300,// 改变背景区域的大小
      color: Colors.red,
      child: const FlutterLogo(
        size: 100, // 宽高为100 的flutter logo
      ),
    ),
  );
}

不知道你猜的对不对,反正我还是想错了,改变背景尺寸后flutter logo的尺寸也会发生变化

我们再稍微调整一下:

Widget build(BuildContext context) {
    return Center(
      child: Container(
        color: Colors.red,
        width: 200,
        height: 300,
        child: const UnconstrainedBox(
          child: FlutterLogo(
            size: 100, // 宽高为100 的flutter logo
          ),
        ),
      ),
    );
  }

上面调整后会发现背红色区域的大小没变,而Flutter Logo的大小却又变了

作为Flutter初学者你是否有同样的困惑,明明我的widget设置了宽高,却不生效;明明生效了,又不生效了!

要理解以上现象首先要搞清楚flutter背后的布局系统

布局约束

Flutter官网有一句简单的总结:<u>Constraints go down. Sizes go up. Parent sets position(向下传递约束,向上传递尺寸,父级决定子级的位置)</u>

布局系统依赖于一种独特的约束模型来确定组件的大小和位置。每个组件在布局阶段都会从父组件接收一组BoxConstraints,这些约束指定了子级的最小和最大宽度、高度范围,并规定子组件如何调整自己的尺寸,以适应父组件分配的空间。每个widget都无法设置任意大小的尺寸,也不能决定它在屏幕中的位置,必须通过父级进行约束

当子级将自己的实际大小告诉父级后,然后父级再来确定它的的位置,然后再传递给它的父级,就这样层层向上。这个过程会重复进行,直到所有组件都完成布局

约束类型

在 Flutter 中,布局约束是通过 BoxConstraints 来传递的。BoxConstraints 对象包含了对子组件的大小限制,它规定了子组件可以使用的最大和最小尺寸。它有两个关键属性:

  • minWidth:最小宽度
  • maxWidth:最大宽度
  • minHeight:最小高度
  • maxHeight:最大高度

父组件将这些约束传递给子组件,子组件根据这些约束来计算自己的尺寸

查看约束信息

实时查看组件的约束信息可以快速帮助我们清楚看到组件之间约束传递的信息,你可以使用LayoutBuilder打印约束信息;父组件传递给子组件一个 BoxConstraints 对象,其中包含了最大和最小的宽度、高度等限制。

Widget build(BuildContext context) {
  return Center(
    child: LayoutBuilder(builder: (context, constraints) {
      // 打印约束信息
      // flutter: BoxConstraints(0.0<=w<=393.0, 0.0<=h<=852.0)
      print(constraints);

      return Container(
        color: Colors.red,
        child: const UnconstrainedBox(
          child: FlutterLogo(
            size: 100,
          ),
        ),
      );
    }),
  );
}

当能拿到约束信息时对于响应式的布局是不是就可以无线发挥了呢

接下来我们进入约束类型部分

紧约束(tight constraints)

<u>紧约束(也叫严格约束)给子级一种获得确切大小的选择。换句话来说就是,它的最大/最小宽度是一致的,高度也一样;子组件的宽度和高度必须在父组件的约束范围内</u>

对应的BoxConstraints信息:

BoxConstraints(w=393.0, h=852.0)

我们来看一个示例:

Container(
  color: Colors.red,
  width: 200,
  height: 200,
  child: const FlutterLogo(
    size: 100,
  ),
);

Container接收到的布局约束为:紧约束,宽高为屏幕的宽高;由于是紧约束,这里设置的width/height属性不会生效

FlutterLogo接收到Container传来的约束,其也是屏幕的约束,紧约束;虽然自身设置了100的宽度,但显然不会生效

因此以上呈现的UI效果为撑满屏幕的红色背景和屏幕宽度的FlutterLogo

宽松约束(loose constraints)

<u>宽松约束的最小宽度/高度为 0</u>(Some boxes loosen the incoming constraints, meaning the maximum is maintained but the minimum is removed, so the widget can have a minimum width and height both equal to zero)

对应的BoxConstraints信息:

BoxConstraints(0.0<=w<=393.0, 0.0<=h<=852.0)

我们来看一个示例:

Center(child: Container(
  color: Colors.red,
  width: 200,
  height: 300,
  child: const FlutterLogo(
    size: 100,
  ),
));

Center接收到屏幕传来的紧约束后,会将紧约束变成宽松约束

Container接收到Center传递来的松约束,其设置了width/height后生效;由于设置了宽高后,其约束变为了紧约束

FlutterLogo接收到的Container传递来的紧约束后,自身的100就不再生效

无边界约束(unbounded constraints)

在某些情况下,widget的约束是无界的或无限的。这意味着最大宽度或最大高度设置为双无穷大

Flutter中ListView,ScollView等组件不会限制子级的宽高,意味着他的子级可以无限大,来看下面的示例:

ListView(
  children: [
    LayoutBuilder(builder: (context, constraints) {
      print(constraints);
      return Text("hahah...");
    })
  ],
);

这里constraints的值为BoxConstraints(w=393.0, 0.0<=h<=Infinity),由于ListView默认垂直可以滚动,所以他的高度约束就是无穷大

:::warning 注意
在无边界约束中需要注意,父级必须有明确的宽高界限(不管是显式的还是隐式的),由于自己的约束是无限的如果父级没有明确的约束就会发生异常
:::

更改约束

Flutter中提供了constraints属性来设置子级的约束BoxConstraints,点击去它的构造函数,可以看到它的常用属性方法:

const BoxConstraints({
    this.minWidth = 0.0,
    this.maxWidth = double.infinity,
    this.minHeight = 0.0,
    this.maxHeight = double.infinity,
  });

  /// Creates box constraints that is respected only by the given size.
  BoxConstraints.tight(Size size)
    : minWidth = size.width,
      maxWidth = size.width,
      minHeight = size.height,
      maxHeight = size.height;
// ...

其中:

  • 紧约束:tight、tightFor
  • 宽松约束:loose

详细信息建议自己查看源码

我们将以上的代码(松约束)修改他的布局约束:

Center(child: Container(
  color: Colors.red,
  width: 200,
  height: 300,
  constraints: BoxConstraints.tight(Size(50, 50)),
  child: const FlutterLogo(
    size: 100,
  ),
));

会发现FlutterLogo的大小变成了50,并且他的布局约束也变成了紧约束(BoxConstraints(w=50,h=50))

紧接着再看看以下示例,如果在紧约束下修改约束会是什么样子的❓

ontainer(
  color: Colors.red,
  width: 200,
  height: 300,
  constraints: BoxConstraints.tight(Size(50, 50)),
  child: const FlutterLogo(
    size: 100,
  ),
);

当我们运行发现修改了布局约束后竟然没有任何效果,FlutterLogo的大小还是屏幕给过来的紧约束大小,也就是宽度为屏幕的宽度

啊!为什么会这样,不是BoxConstraints可以修改布局约束的吗,到了这里我们就要了解下他的源码渲染逻辑了,我们来看下个章节

布局核心

前面我们知道了Flutter是向下传递约束向上传递大小,那么具体到代码上是怎么回事呢?

<u>其实我们屏幕中看到的内容都是由RenderObjectWidget实现的,RenderObjectWidget的createRenderObject返回的RenderObject对象中的layoutpaint负责的具体的绘制信息</u>;layout来传递给子级布局约束,并且获取到子级的真实大小,而paint来负责会知道屏幕的具体位置

前面我们通过在紧约束下修改布局约束后没有任何效果,这里我们从源码的角度来看看怎么回事,flutter中SizeBox提供的紧约束,我们直接看这个继承逻辑:

SizedBox < SingleChildRenderObjectWidget < RenderObjectWidget

其中SizeBox中实现了createRenderObject并返回了RenderConstraintedBox

RenderConstrainedBox createRenderObject(BuildContext context) {
  return RenderConstrainedBox(
    additionalConstraints: _additionalConstraints,
  );
}

RenderConstrainedBox中实现了performLayout方法:

void performLayout() {
  final BoxConstraints constraints = this.constraints;
  if (child != null) {
    // 这里直接拿父级的布局约束传递给子级
    // The box constraints most recently received from the parent.
    // BoxConstraints get constraints => super.constraints as BoxConstraints;
    child!.layout(_additionalConstraints.enforce(constraints), parentUsesSize: true);
    size = child!.size;
  } else {
    size = _additionalConstraints.enforce(constraints).constrain(Size.zero);
  }
}

也就是说<u>在严格约束中会直接忽略掉外部设置的布局约束,直接传递父级的约束</u>

趁热打铁再来看看宽松约束的布局过程,这里我们来看Center的继承过程:

Center < Align < SingleChildRenderObjectWidget < RenderObjectWidget

其中Align中实现了createRenderObject并返回了RenderPositionedBox

RenderPositionedBox createRenderObject(BuildContext context) {
  return RenderPositionedBox(
    alignment: alignment,
    widthFactor: widthFactor,
    heightFactor: heightFactor,
    textDirection: Directionality.maybeOf(context),
  );
}

再来看下RenderPositionedBox中的performLayout方法:

void performLayout() {
  final BoxConstraints constraints = this.constraints;
  final bool shrinkWrapWidth = _widthFactor != null || constraints.maxWidth == double.infinity;
  final bool shrinkWrapHeight = _heightFactor != null || constraints.maxHeight == double.infinity;

  if (child != null) {
    // 这里直接将约束变成宽松约束,并传递给了子级
    child!.layout(constraints.loosen(), parentUsesSize: true);
    size = constraints.constrain(Size(
      shrinkWrapWidth ? child!.size.width * (_widthFactor ?? 1.0) : double.infinity,
      shrinkWrapHeight ? child!.size.height * (_heightFactor ?? 1.0) : double.infinity,
    ));
    alignChild();
  } else {
    size = constraints.constrain(Size(
      shrinkWrapWidth ? 0.0 : double.infinity,
      shrinkWrapHeight ? 0.0 : double.infinity,
    ));
  }
}

通过源码的角度我们了解到紧约束和宽松约束的布局流程以及细节差异,紧约束布局会直接忽略掉自身的约束直接将父级约束传递下去

特殊Widget

在Flutter中有很多特殊的组件需要注意,通常在学习过程中我们应该把它记录下来,以便开发过程中产生一些不解,这里只提供部分widget介绍

Flex/Row、Column

flex是弹性布局类似于css中的flex,而row和column也是flex只是固定了主轴方向;这类组件在布局时不会限制子组件的约束,也就是子组件可以是无限大

Flex(
  direction: Axis.vertical,
  children:[ LayoutBuilder(builder: (context, constraints) {
    print(constraints); // BoxConstraints(0.0<=w<=393.0, 0.0<=h<=Infinity)
    return Container(color: Colors.red,);
  })],
)

Stack/Position

stack、position为定位组件,其内部的布局流程也会稍有不同,以及它的相关数据会影响到布局结果,这里多做介绍,感兴趣的可以上代码试试

开发调试

在开发中,布局约束的问题常常是导致 UI 问题的原因。Flutter 提供了一些工具来帮助开发者调试布局问题\

debugPaintSizeEnabled

debugPaintSizeEnabled 是 Flutter 提供的一个开发调试工具,它会在 UI 上绘制边框,帮助开发者查看组件的大小和边界

debugPaintSizeEnabled = true;

LayoutBuilder

LayoutBuilder 是一个可以访问父组件约束并基于这些约束构建子组件的组件。它允许开发者在构建时动态地获取父组件的布局信息,从而更好地控制子组件的布局

查看以上内容部分

vscode devtools

vscode在调试模式下直接快捷键cmd+shift+p然后输入:flutter open devtool

然后选择对应的调试类型

andriod stutio

Chrome DevTool

在终端使用命令的方式运行程序flutter run -d your_device

# 省略部分内容
A Dart VM Service on iPhone 15 Pro is available at: http://127.0.0.1:51441/KLWR3AMJ9pc=/
The Flutter DevTools debugger and profiler on iPhone 15 Pro is available at: http://127.0.0.1:9101?uri=http://127.0.0.1:51441/KLWR3AMJ9pc=/

会看到The Flutter DevTools debugger and profiler这段信息,后面的链接则是调试链接,直接复制到chrome中打开即可

总结

在Flutter开发中,我们需要合理理解和使用不同的布局约束,掌握紧约束、宽松约束和无边界约束的使用场景,以便构建出符合需求的 UI。同时,通过调试工具和布局小部件,开发者可以更高效地解决布局问题


ihengshuai
70 声望7 粉丝

人生是一场修行


« 上一篇
IP协议
下一篇 »
ARP协议