Flutter 布局真经

引言

对于刚刚接触flutter的新手甚至是使用过一段时间的老手来说,布局就像是一个熟悉的陌生人,我们无时不刻不与它打交道,但是它总会出现莫名其妙的问题。 在fluter中要实现一个布局效果很简单,各种布局widget相互组合就能很轻易实现,可组合的方式往往不一而足,而这些布局方案虽然都能表现出一样的布局效果,但在效率、可读性、甚至对外部布局的影响都不一样,那如何从这些组合中挑选出最合适的布局方案呢?这就要从布局原理说起了……

布局原理概览

个人想法

对于布局来说,必要条件只有两个:大小和位置。 确定了大小和位置,布局也就确定了,而在flutter中,由于其树形结构的特性,各个节点的联系往往密不可分而又相互隔离。 密不可分是因为整个布局树的确定是相邻节点互相确认、传递约束才完成的;相互隔离是因为每个节点其实只关心自身的大小以及其子节点的位置。这就导致糟糕的布局设计下,很可能会有出现牵一发而动全身的窘迫情况,这对程序设计是很不利的!

官方指导

widget布局规则有三:

  1. 上层 widget 向下层 widget 传递约束条件
  2. 下层 widget 向上层 widget 传递大小信息
  3. 上层 widget 决定下层 widget 的位置

更多细节:

  • Widget 会通过它的 父级 获得自身的约束。约束实际上就是 4 个浮点类型的集合:最大/最小宽度,以及最大/最小高度。

  • 然后,这个 widget 将会逐个遍历它的 children 列表。向子级传递 约束(子级之间的约束可能会有所不同),然后询问它的每一个子级需要用于布局的大小。

  • 然后,这个 widget 就会对它子级的 children 逐个进行布局。(水平方向是 x 轴,竖直是 y 轴)

  • 最后,widget 将会把它的大小信息向上传递至父 widget(包括其原始约束条件)。

官方对话(构建一个带有padding的column):

Widget: “嘿!我的父级。我的约束是多少?”

Parent: “你的宽度必须在 80300 像素之间,高度必须在 3085 之间。”

Widget: “嗯…我想要 5 个像素的内边距,这样我的子级能最多拥有 290 个像素宽度和 75 个像素高度。”

Widget: “嘿,我的第一个子级,你的宽度必须要在 0290,长度在 075 之间。”

First child: “OK,那我想要 290 像素的宽度,20 个像素的长度。”

Widget: “嗯…由于我想要将我的第二个子级放在第一个子级下面,所以我们仅剩 55 个像素的高度给第二个子级了。”

Widget: “嘿,我的第二个子级,你的宽度必须要在 0290,长度在 055 之间。”

Second child: “OK,那我想要 140 像素的宽度,30 个像素的长度。”

Widget: “很好。我的第一个子级将被放在 x: 5 & y: 5 的位置,而我的第二个子级将在 x: 80 & y: 25 的位置。”

Widget: “嘿,我的父级,我决定我的大小为 300 像素宽度,60 像素高度。”

什么是约束?

约束Constraints 在Flutter中是一种_布局协议_,Flutter中有两大布局协议BoxConstraintsSliverConstraints。对于非滑动的控件例如Padding,Flex等一般都使用_BoxConstraints盒约束_。

约束可以分为:宽松约束(loose)和 严格约束(tight) 就widget布局树而言,约束默认向下传递,可有些布局类复写了performLayout改变了约束行为,将向下传递的约束信息改变甚至重建,从而导致有些布局行为变得不可预测,因此就需要掌握一些常用布局的布局原理,才能对整体的布局行为加以分析。

案例分析

案例一:Container的约束失效

ConstrainedBox(
	constraints: BoxConstraints.tightFor(
    width: double.infinity,
    height: double.infinity),
    child: Container(width: 100, height: 100, color: red)
)

上边的案例,展示了一个预期宽100,高100的红色方块,可实际上只会得到一个满屏幕的红色。 虽然Container内部有宽高的约束,但是在这里是不起作用的,究其原因,是因为Container内部的布局特性引起的,父部件传递一个tight紧布局,而到了Container由于内部使用的ConstrainedBox,其布局行为会做如下改变:

@override
// this.constraints 为父部件传递的约束
  void performLayout() {
    
    
    final BoxConstraints constraints = this.constraints; // this.constraints为父部件传递过来的约束
    if (child != null) {
    
    
      // 在这里改变了约束,可能会导致自身设置的约束失效
      child!.layout(_additionalConstraints.enforce(constraints),
          parentUsesSize: true);
      size = child!.size;
    } else {
    
    
      size = _additionalConstraints.enforce(constraints).constrain(Size.zero);
    }
  }

// clamp方法会自行在约束范围境内选择
  BoxConstraints enforce(BoxConstraints constraints) {
    
    
    return BoxConstraints(
      minWidth: minWidth.clamp(constraints.minWidth, constraints.maxWidth),
      maxWidth: maxWidth.clamp(constraints.minWidth, constraints.maxWidth),
      minHeight: minHeight.clamp(constraints.minHeight, constraints.maxHeight),
      maxHeight: maxHeight.clamp(constraints.minHeight, constraints.maxHeight),
    );
  }

按照先前的布局原理来说,Container告诉父布局自己需要一个100x100的空间,而父布局也有充分的空间提供,那预期的效果应该会很好的呈现,可在这里由于ConstrainedBox内部改变了布局行为,导致预期结果不生效,ConstrainedBox会通过enforce函数衡量自身的约束属性即 _additionalConstraints,和父布局传递的约束,在其中取临近值,在这里由于父部件约束为:

BoxConstraints(
      minWidth: double.infinity,
      maxWidth: double.infinity,
      minHeight: double.infinity,
      maxHeight: double.infinity,
    );

故所以_additionalConstraints的约束行为会被改成double.infinity~double.infinity之间的值即double.infinity。

案例二:奇怪的LimitedBox

现有这样两个场景:

ConstrainedBox(
  constraints: BoxConstraints.tightFor(
      width: double.infinity,
      height: double.infinity),
  child: UnconstrainedBox(
      child: LimitedBox(
        maxWidth: 100,
        child:
            Container(color: Colors.red, width: double.infinity, height: 100),
      ),
  )
)
ConstrainedBox(
  constraints: BoxConstraints.tightFor(
      width: double.infinity,
      height: double.infinity),
  child: Center(
      child: LimitedBox(
        maxWidth: 100,
        child:
            Container(color: Colors.red, width: double.infinity, height: 100),
      ),
  )
)

两者的区别不是很明显,只是前者的LimitedBox由UnconstrainedBox包裹,而后由Center包裹,但两个例子所展示的UI却大相径庭:

这样看来是Center致使LimitedBox的maxWidth约束失效了,为什么会这样呢?

// LimitedBox的布局过程
  BoxConstraints _limitConstraints(BoxConstraints constraints) {
    
    
    return BoxConstraints(
      minWidth: constraints.minWidth,
      // 判断是否有边界,如果有,则maxWidth会失效
      maxWidth: constraints.hasBoundedWidth ? constraints.maxWidth : constraints.constrainWidth(maxWidth),
      minHeight: constraints.minHeight,
      maxHeight: constraints.hasBoundedHeight
          ? constraints.maxHeight
          : constraints.constrainHeight(maxHeight),
    );
  }

  @override
  void performLayout() {
    
    
    if (child != null) {
    
    
      final BoxConstraints constraints = this.constraints;
      child!.layout(_limitConstraints(constraints), parentUsesSize: true);
      size = constraints.constrain(child!.size);
    } else {
    
    
      size = _limitConstraints(constraints).constrain(Size.zero);
    }
  }
  bool get hasBoundedWidth => maxWidth < double.infinity;

引起LimitedBox的maxWidth失效的原因已经很明显了, 关键就在于这行代码:

constraints.hasBoundedWidth ? constraints.maxWidth : constraints.constrainWidth(maxWidth)

分为两种情况:

  1. 父布局传递constraints的hasBoundedWidth为true,这种情况下,maxWidth是肯定失效的
  2. 父布局必须是宽松布局才行,constraints.constrainWidth(maxWidth)会取constraints中的maxWidth临近值,所以紧布局也可能会使maxWidth失效。

所以这里LimitedBox的maxWidth约束是否失效的关键,在于父部件传递来的布局是否有边界,而Center和UnconstrainedBox在传递给子部件约束信息的处理上是有区别的: Center调用的constraints.loosen()方法,将当前约束进行松绑,而当前约束的hasBoundedWidth是不确定的!(依赖父部件)传递的约束。 UnconstrainedBox则重新构建的了一个无限大小的约束:childConstraints = const BoxConstraints(); 这样子部件是肯定可以收到一个hasBoundedWidth = false 并且为宽松特性的约束,所以在这里Center是有可能致使LimitedBox的maxWidth约束失效的,而UnconstrainedBox则可以百分百保证LimitedBox的布局行为符合预期。

位置的确定

约束确定后,想要绘制上屏,还缺一个很重要的属性——位置

ParentData

子部件的大小是通过父类传递的约束来确定的,同样的,位置也是由父部件确定,以RenderBaseline为例,

  @override
  void performLayout() {
    
    
    if (child != null) {
    
    
      final BoxConstraints constraints = this.constraints;
      child!.layout(constraints.loosen(), parentUsesSize: true);
      final double childBaseline = child!.getDistanceToBaseline(baselineType)!;
      final double actualBaseline = baseline;
      final double top = actualBaseline - childBaseline;
      final BoxParentData childParentData = child!.parentData as BoxParentData;
      childParentData.offset = Offset(0.0, top);
      final Size childSize = child!.size;
      size = constraints.constrain(Size(childSize.width, top + childSize.height));
    } else {
    
    
      performResize();
    }

可以看到在子部件不为空的情况下,先对子部件传递约束,随之通过自身的特性,确定下一个Offset类型的位置点,然后传递给child的parentData,帮助child部件确定位置属性,当子部件进行绘制的时候,直接读取即可!

后续

Flutter布局的基本原理无外乎父部件对子部件位置和大小的确定,不同类型的widget有不同的‘脾气’,会在传递约束以及位置的时候做出不同的更改。 不过,在Flutter中,布局还牵扯到很多其他功能,例如relayoutBoundary对于重绘的处理、部件点击事件的处理,这些都是和布局原理密不可分的,在搞懂布局原理的情况下,在去看这些实现,会轻松很多。

大家如果还想了解更多Android 相关的更多知识点,可以点进我的 GitHub项目中:https://github.com/733gh/Android-T3 自行查看,里面记录了许多的Android 知识点。最后还请大家点点赞支持下!!!



大家如果还想了解更多Android 相关的更多知识点,可以点进我的 GitHub项目中:https://github.com/733gh/Android-T3 自行查看,里面记录了许多的Android 知识点。最后还请大家点点赞支持下!!!

猜你喜欢

转载自blog.csdn.net/u012165769/article/details/115328889