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(深度优先遍历) 的方式遍历渲染树,并限制自上而下的方式从父节点传递给子节点。子节点如果需要确定自身的大小,则必须遵守父节点传递的限制。子节点的响应方式是在父节点建立的约束内将大小以自上而下的方式传递给父节点。


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


相关文章
|
1月前
|
前端开发 Java 开发工具
【03】完整flutter的APP打包流程-以apk设置图标-包名-签名-APP名-打包流程为例—-开发完整的社交APP-前端客户端开发+数据联调|以优雅草商业项目为例做开发-flutter开发-全流程-商业应用级实战开发-优雅草央千澈 章节内容【03】
【03】完整flutter的APP打包流程-以apk设置图标-包名-签名-APP名-打包流程为例—-开发完整的社交APP-前端客户端开发+数据联调|以优雅草商业项目为例做开发-flutter开发-全流程-商业应用级实战开发-优雅草央千澈 章节内容【03】
76 18
【03】完整flutter的APP打包流程-以apk设置图标-包名-签名-APP名-打包流程为例—-开发完整的社交APP-前端客户端开发+数据联调|以优雅草商业项目为例做开发-flutter开发-全流程-商业应用级实战开发-优雅草央千澈 章节内容【03】
|
10天前
|
前端开发 安全 开发工具
【11】flutter进行了聊天页面的开发-增加了即时通讯聊天的整体页面和组件-切换-朋友-陌生人-vip开通详细页面-即时通讯sdk准备-直播sdk准备-即时通讯有无UI集成的区别介绍-开发完整的社交APP-前端客户端开发+数据联调|以优雅草商业项目为例做开发-flutter开发-全流程-商业应用级实战开发-优雅草Alex
【11】flutter进行了聊天页面的开发-增加了即时通讯聊天的整体页面和组件-切换-朋友-陌生人-vip开通详细页面-即时通讯sdk准备-直播sdk准备-即时通讯有无UI集成的区别介绍-开发完整的社交APP-前端客户端开发+数据联调|以优雅草商业项目为例做开发-flutter开发-全流程-商业应用级实战开发-优雅草Alex
137 90
【11】flutter进行了聊天页面的开发-增加了即时通讯聊天的整体页面和组件-切换-朋友-陌生人-vip开通详细页面-即时通讯sdk准备-直播sdk准备-即时通讯有无UI集成的区别介绍-开发完整的社交APP-前端客户端开发+数据联调|以优雅草商业项目为例做开发-flutter开发-全流程-商业应用级实战开发-优雅草Alex
|
25天前
|
Dart 前端开发
【05】flutter完成注册页面完善样式bug-增加自定义可复用组件widgets-严格规划文件和目录结构-规范入口文件-开发完整的社交APP-前端客户端开发+数据联调|以优雅草商业项目为例做开发-flutter开发-全流程-商业应用级实战开发-优雅草央千澈
【05】flutter完成注册页面完善样式bug-增加自定义可复用组件widgets-严格规划文件和目录结构-规范入口文件-开发完整的社交APP-前端客户端开发+数据联调|以优雅草商业项目为例做开发-flutter开发-全流程-商业应用级实战开发-优雅草央千澈
116 75
【05】flutter完成注册页面完善样式bug-增加自定义可复用组件widgets-严格规划文件和目录结构-规范入口文件-开发完整的社交APP-前端客户端开发+数据联调|以优雅草商业项目为例做开发-flutter开发-全流程-商业应用级实战开发-优雅草央千澈
|
3月前
|
缓存 监控 前端开发
优化 Flutter 应用启动速度的策略,涵盖理解启动过程、资源加载优化、减少初始化工作、界面布局优化、异步初始化、预加载关键数据、性能监控与分析等方面
本文探讨了优化 Flutter 应用启动速度的策略,涵盖理解启动过程、资源加载优化、减少初始化工作、界面布局优化、异步初始化、预加载关键数据、性能监控与分析等方面,并通过案例分析展示了具体措施和效果,强调了持续优化的重要性及未来优化方向。
114 10
|
15天前
|
前端开发 Java Shell
【08】flutter完成屏幕适配-重建Android,增加GetX路由,屏幕适配,基础导航栏-多版本SDK以及gradle造成的关于fvm的使用(flutter version manage)-卓伊凡换人优雅草Alex-开发完整的社交APP-前端客户端开发+数据联调|以优雅草商业项目为例做开发-flutter开发-全流程-商业应用级实战开发-优雅草Alex
【08】flutter完成屏幕适配-重建Android,增加GetX路由,屏幕适配,基础导航栏-多版本SDK以及gradle造成的关于fvm的使用(flutter version manage)-卓伊凡换人优雅草Alex-开发完整的社交APP-前端客户端开发+数据联调|以优雅草商业项目为例做开发-flutter开发-全流程-商业应用级实战开发-优雅草Alex
115 20
【08】flutter完成屏幕适配-重建Android,增加GetX路由,屏幕适配,基础导航栏-多版本SDK以及gradle造成的关于fvm的使用(flutter version manage)-卓伊凡换人优雅草Alex-开发完整的社交APP-前端客户端开发+数据联调|以优雅草商业项目为例做开发-flutter开发-全流程-商业应用级实战开发-优雅草Alex
|
23天前
|
Dart 前端开发 容器
【07】flutter完成主页-完成底部菜单栏并且做自定义组件-完整短视频仿抖音上下滑动页面-开发完整的社交APP-前端客户端开发+数据联调|以优雅草商业项目为例做开发-flutter开发-全流程-商业应用级实战开发-优雅草央千澈
【07】flutter完成主页-完成底部菜单栏并且做自定义组件-完整短视频仿抖音上下滑动页面-开发完整的社交APP-前端客户端开发+数据联调|以优雅草商业项目为例做开发-flutter开发-全流程-商业应用级实战开发-优雅草央千澈
73 18
【07】flutter完成主页-完成底部菜单栏并且做自定义组件-完整短视频仿抖音上下滑动页面-开发完整的社交APP-前端客户端开发+数据联调|以优雅草商业项目为例做开发-flutter开发-全流程-商业应用级实战开发-优雅草央千澈
|
12天前
|
Dart 前端开发 Android开发
【09】flutter首页进行了完善-采用android studio 进行真机调试开发-增加了直播间列表和短视频人物列表-增加了用户中心-卓伊凡换人优雅草Alex-开发完整的社交APP-前端客户端开发+数据联调|以优雅草商业项目为例做开发-flutter开发-全流程-商业应用级实战开发-优雅草Alex
【09】flutter首页进行了完善-采用android studio 进行真机调试开发-增加了直播间列表和短视频人物列表-增加了用户中心-卓伊凡换人优雅草Alex-开发完整的社交APP-前端客户端开发+数据联调|以优雅草商业项目为例做开发-flutter开发-全流程-商业应用级实战开发-优雅草Alex
36 4
【09】flutter首页进行了完善-采用android studio 进行真机调试开发-增加了直播间列表和短视频人物列表-增加了用户中心-卓伊凡换人优雅草Alex-开发完整的社交APP-前端客户端开发+数据联调|以优雅草商业项目为例做开发-flutter开发-全流程-商业应用级实战开发-优雅草Alex
|
28天前
|
缓存 前端开发 Android开发
【04】flutter补打包流程的签名过程-APP安卓调试配置-结构化项目目录-完善注册相关页面-开发完整的社交APP-前端客户端开发+数据联调|以优雅草商业项目为例做开发-flutter开发-全流程
【04】flutter补打包流程的签名过程-APP安卓调试配置-结构化项目目录-完善注册相关页面-开发完整的社交APP-前端客户端开发+数据联调|以优雅草商业项目为例做开发-flutter开发-全流程
79 12
【04】flutter补打包流程的签名过程-APP安卓调试配置-结构化项目目录-完善注册相关页面-开发完整的社交APP-前端客户端开发+数据联调|以优雅草商业项目为例做开发-flutter开发-全流程
|
1月前
|
Dart 前端开发 Android开发
【02】写一个注册页面以及配置打包选项打包安卓apk测试—开发完整的社交APP-前端客户端开发+数据联调|以优雅草商业项目为例做开发-flutter开发-全流程-商业应用级实战开发-优雅草央千澈
【02】写一个注册页面以及配置打包选项打包安卓apk测试—开发完整的社交APP-前端客户端开发+数据联调|以优雅草商业项目为例做开发-flutter开发-全流程-商业应用级实战开发-优雅草央千澈
36 1
【02】写一个注册页面以及配置打包选项打包安卓apk测试—开发完整的社交APP-前端客户端开发+数据联调|以优雅草商业项目为例做开发-flutter开发-全流程-商业应用级实战开发-优雅草央千澈
|
1月前
|
Dart 前端开发 架构师
【01】vs-code如何配置flutter环境-开发完整的社交APP-前端客户端开发+数据联调|以优雅草商业项目为例做开发-flutter开发-全流程-商业应用级实战开发-优雅草央千澈-供大大的学习提升
【01】vs-code如何配置flutter环境-开发完整的社交APP-前端客户端开发+数据联调|以优雅草商业项目为例做开发-flutter开发-全流程-商业应用级实战开发-优雅草央千澈-供大大的学习提升
112 26

热门文章

最新文章

  • 1
    【11】flutter进行了聊天页面的开发-增加了即时通讯聊天的整体页面和组件-切换-朋友-陌生人-vip开通详细页面-即时通讯sdk准备-直播sdk准备-即时通讯有无UI集成的区别介绍-开发完整的社交APP-前端客户端开发+数据联调|以优雅草商业项目为例做开发-flutter开发-全流程-商业应用级实战开发-优雅草Alex
  • 2
    【05】flutter完成注册页面完善样式bug-增加自定义可复用组件widgets-严格规划文件和目录结构-规范入口文件-开发完整的社交APP-前端客户端开发+数据联调|以优雅草商业项目为例做开发-flutter开发-全流程-商业应用级实战开发-优雅草央千澈
  • 3
    【08】flutter完成屏幕适配-重建Android,增加GetX路由,屏幕适配,基础导航栏-多版本SDK以及gradle造成的关于fvm的使用(flutter version manage)-卓伊凡换人优雅草Alex-开发完整的社交APP-前端客户端开发+数据联调|以优雅草商业项目为例做开发-flutter开发-全流程-商业应用级实战开发-优雅草Alex
  • 4
    当flutter react native 等混开框架-并且用vscode-idea等编译器无法打包apk,打包安卓不成功怎么办-直接用android studio如何打包安卓apk -重要-优雅草卓伊凡
  • 5
    【07】flutter完成主页-完成底部菜单栏并且做自定义组件-完整短视频仿抖音上下滑动页面-开发完整的社交APP-前端客户端开发+数据联调|以优雅草商业项目为例做开发-flutter开发-全流程-商业应用级实战开发-优雅草央千澈
  • 6
    【04】flutter补打包流程的签名过程-APP安卓调试配置-结构化项目目录-完善注册相关页面-开发完整的社交APP-前端客户端开发+数据联调|以优雅草商业项目为例做开发-flutter开发-全流程
  • 7
    零基础构建即时通讯开源项目OpenIM移动端-Flutter篇
  • 8
    flutter3-dart3-dymall原创仿抖音(直播+短视频+聊天)商城app系统模板
  • 9
    【09】flutter首页进行了完善-采用android studio 进行真机调试开发-增加了直播间列表和短视频人物列表-增加了用户中心-卓伊凡换人优雅草Alex-开发完整的社交APP-前端客户端开发+数据联调|以优雅草商业项目为例做开发-flutter开发-全流程-商业应用级实战开发-优雅草Alex
  • 10
    【06】flutter完成注册页面-密码登录-手机短信验证-找回密码相关页面-并且实现静态跳转打包demo做演示-开发完整的社交APP-前端客户端开发+数据联调|以优雅草商业项目为例做开发-flutter开发-全流程-商业应用级实战开发-优雅草央千澈