Flutter | 布局流程(下)

简介: Flutter | 布局流程(下)

Layout流程


如果组件有子组件,则需要在 performLayout 中调用子组件的 layout 先对子组件进行布局,如下:


void layout(Constraints constraints, { bool parentUsesSize = false }) {
  RenderObject? relayoutBoundary;
  //先确定当前布局边界  
  if (!parentUsesSize || sizedByParent || constraints.isTight || parent is! RenderObject) {
    relayoutBoundary = this;
  } else {
    relayoutBoundary = (parent! as RenderObject)._relayoutBoundary;
  }
  // _neessLayout  标记当前组件是否被标记为需要布局
  // _constraints 是上次布局时父组件传递给当前组件的约束
  // _relayoutBoundary 为上次布局时当前组件的布局边界 
  // 所以,当当前组件没有被标记为需要布局,且父组件传递的约束没有发生变化
  // 和布局边界也没有发生变化时则不需要重新布局,直接返回即可  
  if (!_needsLayout && constraints == _constraints && relayoutBoundary == _relayoutBoundary) {
  ....///
    return;
  }
  // 如果需要布局,缓存约束和布局边界  
  _constraints = constraints;
  _relayoutBoundary = relayoutBoundary;
  assert(!_debugMutationsLocked);
  assert(!_doingThisLayoutWithCallback);
  assert(() {
    _debugMutationsLocked = true;
    if (debugPrintLayouts)
      debugPrint('Laying out (${sizedByParent ? "with separate resize" : "with resize allowed"}) $this');
    return true;
  }());
  // 后面解释
  if (sizedByParent) {
     performResize();
  }
  // 执行布局
  performLayout();
   //布局结束后将 _needsLayotu 置位 false
  _needsLayout = false;
  // 将当前组件标记为重绘,因为布局发生变化后,需要重新绘制
  markNeedsPaint();
}


简单的讲一下布局的过程:


确定当前组件的布局边界

判断是否需要重新布局,如果没有必要会直接返回,反之才需要重新布局。不需要布局时需要满足三个条件

单签组件没有被标记为需要重新布局。

父组件传递的约束没有发生变化。

当前组件布局边界也没有发生变化时。

调用 performLayout 进行布局,因为 performLayout 中又会调用子组件的 layout 方法,所以这是一个递归的过程,递归结束后整个组件的布局也就完成了。

请求重绘


sizedByParent


在 layout 方法中,有以下逻辑:


if (sizedByParent) {
     performResize();
  }


上面我们说过,sizeByParent 为 true 是表示:当前组件的大小值取决于父组件传递的约束,而不会依赖后组件的大小。前面我们说过,performLayout 中确定当前组件大小时通常会依赖子组件的大小,如果 sizedByParent 为 true,则当前组件大小就不会依赖于子组件的大小。


为了清晰逻辑,Flutter 框架中约定,当 sizedByParent 为 true 时,确定当前组件大小的逻辑应该抽离到 performResize() 中,这种情况下 performLayout 主要任务便只有两个:对子组件进行布局和确定子组件在当前组件中的偏移。


下面通过一个 AccurateSizedBox 示例来演示一下 sizebyParent 为 true 时我们应该如何布局:


AccurateSizeBox


Flutter 中的 SizeBox 会将其父组件的约束传递给其子组件,这也就意味着,如果父组件限制了最新的宽度为 100,即使我们通过 SizeBox 指定宽度为 50 也是没有用的。


因为 SizeBox 中的实现会让 SizedBox 的子组件先满足 SizeBox 父组件的约束。例如:


AppBar(
    title: Text(title),
    actions: <Widget>[
      SizedBox( // 使用SizedBox定制loading 宽高
        width: 20, 
        height: 20,
        child: CircularProgressIndicator(
          strokeWidth: 3,
          valueColor: AlwaysStoppedAnimation(Colors.white70),
        ),
      )
    ],
 )


实际结果还是 progress 的高度为 appbar 的高度。


通过查看 SizedBox 源码,如下所示:


@override
void performLayout() {
  final BoxConstraints constraints = this.constraints;
  if (child != null) {
    child!.layout(_additionalConstraints.enforce(constraints), parentUsesSize: true);
    size = child!.size;
  } else {
    size = _additionalConstraints.enforce(constraints).constrain(Size.zero);
  }
}
//返回尊重给定约束同时尽可能接近原始约束的新框约束
BoxConstraints enforce(BoxConstraints constraints) {
    return BoxConstraints(
        // clamp :根据数字返回一个介于低和高之间的值
        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),
    );
}


可以发现,之所以不生效,是应为父组件限制了最小高度,SizeBox 中的子组件会先满足父组件的约束。当然,我们也可以通过使用 UnconstrainedBox + SizedBox 来实现我们想要的效果,但是这里我们希望使用一个布局搞定,为此我们自定义一个 AccurateSizeBox 组件。


它和 SizedBox 主要的区别就是 AccurateSizedBox 自身会遵守其父组件传递的约束,而不是让子组件去满足 AccureateSizeBox 父组件的约束,具体:


AccurateSizedBox 自身大小只取决于父组件的约束和自身的宽高。

AccurateSizedBox 确定自身大小后,限制其子组件的大小。

class AccurateSizedBox extends SingleChildRenderObjectWidget {
  const AccurateSizedBox(
      {Key key, this.width = 0, this.height = 0, @required Widget child})
      : super(key: key, child: child);
  final double width;
  final double height;
  @override
  RenderObject createRenderObject(BuildContext context) {
    return RenderAccurateSizeBox(width, height);
  }
  @override
  void updateRenderObject(
      BuildContext context, covariant RenderAccurateSizeBox renderObject) {
    renderObject
      ..width = width
      ..height = height;
  }
}
class RenderAccurateSizeBox extends RenderProxyBoxWithHitTestBehavior {
  RenderAccurateSizeBox(this.width, this.height);
  double width;
  double height;
  //当前组件的大小只取决于父组件传递的约束
  @override
  bool get sizedByParent => true;
  // performResize 中会调用
  @override
  Size computeDryLayout(BoxConstraints constraints) {
    //设置当前元素的宽高,遵守父组件的约束
    return constraints.constrain(Size(width, height));
  }  
  @override
  void performLayout() {
    child.layout(
        BoxConstraints.tight(
            Size(min(size.width, width), min(size.height, height))),
        //父容器是固定大小,子元素大小改变时不影响父元素
        //parentUserSize 为 false时,子组件的布局边界会是他自身,子组件布局发生变化后不会影响当前组件
        parentUsesSize: false);
  }
}


上面代码有三点需要注意:


我们的 RenderAccurateSizedBox 不在继承自 RenderBox,而是继承 RenderProxyBoxWithHitTestBehavior ,RenderProxyBoxWithHitTestBehavior 是间接继承自 RenderBox 的,它里面包含了默认的命中测试和绘制相关逻辑,继承它以后则不需要我们手动实现了。


我们将确定当前组件大小的逻辑挪到了 computeDryLayout 方法中,因为 RenderBox 的 performResize 方法会调用 computeDryLayout,并将返回结果作为当前组件大小。


按照 Flutter 框架约定,我们应该重写 computeDryLayout 方法,而不是 performResize 方法。就行我们在布局时应该重写 performLayout 方法而不是 layout 方法;不过,这只是一个约定,并非强制,但我们应该尽可能遵守这个约定,除非你清楚的知道自己在干什么并且能确保之后维护你代码的人也清楚。


RenderAccurateSizedBox 在调用子组件 layout 时,将 parentUserSize 置为 false,这样的话子组件就会变成一个布局边界。


测试如下:


class AccurateSizedBoxRoute extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    final child = GestureDetector(
      onTap: () => print("tap"),
      child: Container(width: 300, height: 30, color: Colors.red),
    );
    return Row(
      children: [
        ConstrainedBox(
          //限制高度为 100x100
          constraints: BoxConstraints.tight(Size(100, 100)),
          child: SizedBox(
            width: 50,
            height: 50,
            child: child,
          ),
        ),
        Padding(
          padding: const EdgeInsets.only(left: 8),
          child: ConstrainedBox(
            constraints: BoxConstraints.tight(Size(100, 100)),
            child: AccurateSizedBox(width: 50, height: 50, child: child),
          ),
        )
      ],
    );
  }
}


结果如上所示,当父组件宽高是 100 时,我们通过 SizedBox 指定 Container 大小是 50x50 是不能成功的。而通过 AccurateSizedBox 时成功了。


需要注意的是,如果一个组件的 sizeByParent 为 true,那它在布局子组件的时候也是能将 parentUserSize 的,sizeByParent 为 true 表示自己是布局边界。


而将 parentUsesSize 置为 true 或者 false 决定的是子组件是否是布局边界,两者并不相矛盾,这一点不能混淆。


另外,在 Flutter 自带的 OverflowBox 组件中,他的 sizeByParent 为 true,在调用子组件 layout 时,parentUsesSize 也是 true,详情可查看 OverflowBox 的源码


Constraints


Constraints(约束)主要描述了最小和最大宽高的限制,理解组件在布局过程中如何根据约束确定自身或子节点的大小对我们理解组件的布局行为有很大的帮助。


我们通过一个 200*200 的 Container 的例子来说明,为了排除干扰,我们让根节点(RenderView) 作为 Container 的父组件,代码如下:


Container(width: 200, height: 200, color: Colors.red)


运行之后,就会发现整个屏幕都为红色,为什么呢,我们看看 RenderView 的实现:


@override
void performLayout() {
  //configurateion.sieze 为当前设备的屏幕
  _size = configuration.size;
  assert(_size.isFinite);
  if (child != null)
    child!.layout(BoxConstraints.tight(_size));//强制子组件和屏幕一样大
}


这里需要介绍一下两种常用的约束:


宽松约束:不限制最小宽高(为 0),只限制最大宽高,可以通过 BoxConstraints.loose(Size size) 来快速创建。

严格约束:限制为固定大小,即最小宽度等于最大宽度,最小高度等于最大高度,可以通过 BoxConstraints.thght(Size) 来快速创建。

可以发现,RenderView 中给子组件传递的是一个严格的约束,即强制子组件等于屏幕大小,所以 Container 便撑满了屏幕。


那么我们如何才能让指定的大小生效呢,答案就是 “引入一个中间组件,让中间组件遵守父组件的约束,然后对子组件传递新的约束”。对于这个例子来说,最简单的办法就是使用一个 Align 组件来包裹 Container:


@override
Widget build(BuildContext context) {
  var container = Container(width: 200, height: 200, color: Colors.red);
  return Align(
    child: container,
    alignment: Alignment.topLeft,
  );
}


Align 会遵守 RenderView 的约束,让自身撑满屏幕,然后会给子组件一个宽松的约束(最小宽度为 0,最大宽度为 200),这样 Container 就可以变成 200*200 了。


当然我们也可以使用其他组件来代替 Align,例如 UnconstrainedBox,但原理是相同的。具体可查看源码进行验证。


例如 Align 的布局过程如下:


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


总结


到这里我们已经对 flutter 布局流程比较熟悉了,现在我们看一张官网的图:


image.png


在进行布局的时候,Flutter 会以 DFS(深度优先遍历) 的方式遍历渲染树,并限制自上而下的方式从父节点传递给子节点。子节点如果需要确定自身的大小,则必须遵守父节点传递的限制。子节点的响应方式是在父节点建立的约束内将大小以自上而下的方式传递给父节点。


是不是理解的更透彻了一些


相关文章
|
6月前
|
编解码 前端开发 开发者
【Flutter前端技术开发专栏】Flutter中的响应式设计与自适应布局
【4月更文挑战第30天】Flutter框架助力移动应用实现响应式设计与自适应布局,通过层次化布局系统和`Widget`树管理,结合`BoxConstraints`定义尺寸范围,实现自适应。利用`MediaQuery`获取设备信息,调整布局以适应不同屏幕。`FractionallySizedBox`按比例设定尺寸,`LayoutBuilder`动态计算布局。借助这些工具,开发者能创建跨屏幕尺寸、方向兼容的应用,提升用户体验。
161 0
【Flutter前端技术开发专栏】Flutter中的响应式设计与自适应布局
|
15天前
|
存储 调度 数据安全/隐私保护
鸿蒙Flutter实战:13-鸿蒙应用打包上架流程
鸿蒙应用打包上架流程包括创建应用、打包签名和上传应用。首先,在AppGallery Connect中创建项目、APP ID和元服务。接着,使用Deveco进行手动签名,生成.p12和.csr文件,并在AppGallery Connect中上传CSR文件获取证书。最后,配置签名并打包生成.app文件,上传至应用市场。常见问题包括检查签名配置文件是否正确。参考资料:[应用/服务签名](https://developer.huawei.com/consumer/cn/doc/harmonyos-guides-V5/ide-signing-V5)。
44 3
鸿蒙Flutter实战:13-鸿蒙应用打包上架流程
|
21天前
|
开发者 容器
Flutter&鸿蒙next 布局架构原理详解
本文详细介绍了 Flutter 中的主要布局方式,包括 Row、Column、Stack、Container、ListView 和 GridView 等布局组件的架构原理及使用场景。通过了解这些布局 Widget 的基本概念、关键属性和布局原理,开发者可以更高效地构建复杂的用户界面。此外,文章还提供了布局优化技巧,帮助提升应用性能。
80 4
|
21天前
|
容器
深入理解 Flutter 鸿蒙版的 Stack 布局:适配屏幕与层叠样式布局
Flutter 的 Stack 布局组件允许你将多个子组件层叠在一起,实现复杂的界面效果。本文介绍了 Stack 的基本用法、核心概念(如子组件层叠、Positioned 组件和对齐属性),以及如何使用 MediaQuery 和 LayoutBuilder 实现响应式设计。通过示例展示了照片展示与文字描述、动态调整层叠布局等高级用法,帮助你构建更加精美和实用的 Flutter 应用。
108 2
|
1月前
|
容器
Flutter&鸿蒙next 布局架构原理详解
Flutter&鸿蒙next 布局架构原理详解
|
1月前
|
Android开发 开发者 容器
flutter:&UI布局 (六)
本文档介绍了Flutter中的UI布局方式,包括线性布局(如Column和Row)、非线性布局(如Stack、Flex、Positioned)以及Wrap布局等。通过具体示例代码展示了如何使用这些布局组件来构建灵活多变的用户界面,例如使用Column垂直排列文本、使用Stack叠加组件、以及利用Wrap实现自动换行的按钮布局等。
|
6月前
|
开发框架 前端开发 数据安全/隐私保护
【Flutter 前端技术开发专栏】Flutter 中的布局与样式设计
【4月更文挑战第30天】本文探讨了Flutter的布局和样式设计,关键点包括:1) 布局基础如Column、Row和Stack用于创建复杂结构;2) Container、Center和Expanded等常用组件的作用;3) Theme和Decoration实现全局样式和组件装饰;4) 实战应用如登录界面和列表页面的构建;5) 响应式布局利用MediaQuery和弹性组件适应不同屏幕;6) 性能优化,避免过度复杂设计。了解并掌握这些,有助于开发者创建高效美观的Flutter应用。
188 0
【Flutter 前端技术开发专栏】Flutter 中的布局与样式设计
|
4月前
|
容器
flutter 布局管理【详解】
flutter 布局管理【详解】
40 3
|
4月前
Flutter-自定义折叠流布局实现
Flutter-自定义折叠流布局实现
84 0
|
6月前
|
开发框架 前端开发 搜索推荐
【Flutter前端技术开发专栏】Flutter中的自定义Widget与渲染流程
【4月更文挑战第30天】探索Flutter的自定义Widget与渲染流程。自定义Widget是实现复杂UI设计的关键,优点在于个性化设计、功能扩展和代码复用,但也面临性能优化和复杂性管理的挑战。创建步骤包括设计结构、定义Widget类、实现构建逻辑和处理交互。Flutter渲染流程涉及渲染对象树、布局、绘制和合成阶段。实践案例展示如何创建带渐变背景和阴影的自定义按钮。了解这些知识能提升应用体验并应对开发挑战。查阅官方文档以深入学习。
77 0
【Flutter前端技术开发专栏】Flutter中的自定义Widget与渲染流程