原来Flutter背后的布局原理是这样的

news/2024/12/24 8:09:31/文章来源:https://www.cnblogs.com/98kk/p/18626430

文章首发博客网站,由于格式解析问题,你可以前往阅读原文

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

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

image

来看下面的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官网有一句简单的总结:Constraints go down. Sizes go up. Parent sets position(向下传递约束,向上传递尺寸,父级决定子级的位置)

布局系统依赖于一种独特的约束模型来确定组件的大小和位置。每个组件在布局阶段都会从父组件接收一组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)

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

对应的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)

宽松约束的最小宽度/高度为 0(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是向下传递约束向上传递大小,那么具体到代码上是怎么回事呢?

其实我们屏幕中看到的内容都是由RenderObjectWidget实现的,RenderObjectWidget的createRenderObject返回的RenderObject对象中的layoutpaint负责的具体的绘制信息;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);}
}

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

趁热打铁再来看看宽松约束的布局过程,这里我们来看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。同时,通过调试工具和布局小部件,开发者可以更高效地解决布局问题

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/857970.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

UML之包与包图

了解UML的人都知道UML中也有包的概念,包在UML中作用与面向对象编程语言中类似,它是管理对象的工具,也是解决对象同名冲突的手段。 在UML中,包的表示图形是一个左上角带标签的矩形,而包名可以标注于矩形中央(如下图所示,包名Package1位于矩形中央)或者左上角的标签之内。…

读数据保护:工作负载的可恢复性15公有云

公有云1. 云不是万能的 1.1. 其实根本就没有所谓的云,它只不过是别人的计算机而已 1.2. 云、SaaS以及Kubernetes,都没有改变数据保护与数据所有权的基本原则 1.3. 数据是你自己的,你必须负责给它们做备份1.3.1. 除非有人明确保证替你做备份,否则你还是必须自己做1.3.2. 就算…

Zed编辑器-Win中文汉化版(持续更新)

Zed编辑器-Win中文汉化版 介绍Zed 是一款专为团队协作设计的代码编辑器,由 Atom 编辑器的原作者主导开发。Zed 的核心目标是为开发者提供一个高效、流畅、且直观的编程环境,特别强调实时协作和团队合作。该编辑器由 Rust 语言编写,并内置了 rust-analyzer,主打“高性能”。…

CentOS系统搭建K8s集群

前情概要 关于在虚拟机中centos系统搭建k8s集群,前前后后花了很多个白天黑夜才搞定,采用不同的搭建方式搭建集群次数至少10次以上,期间看了无数文章和视频,也踩过无数坑,很多视频、文章的安装教程都存在一些差别,有些时候可能因为k8s安装版本不同或者缺少某些必要的设置导…

中台建设为什么需要领域驱动设计

一、数字化转型 数字化转型是企业能力全面体系化,系统化,数据化提升的过程,这种提升包括了技术能力,业务能力,组织架构合理性等多方面的提升。而随着多年来海量高频业务的发展,技术也在推动着持续进步,并且越来越多的技术方案趋向成熟,类似于阿里巴巴,腾讯,美团等,…

某小程序sign关键字逆向分析

声明 本文章所有内容仅用于学习交流,严禁用于其他目的。文中不提供完整代码,抓包内容、敏感网址及数据接口等均已脱敏处理。严禁将相关内容用于商业用途和非法用途,否则由此产生的一切后果与作者无关。未经许可,禁止转载本文,禁止修改后二次传播。擅自使用本文讲解的技术导…

乌鸡国

1-队伍进入副本以后,走到下图分岔点离队,每人负责一条线路,寻找树妖。2-寻找仙人,可以按照1的策略继续反方向走回去找仙人。3-寻找完仙人以后,会要求帮助国王清理荆棘木,五个号散开清理完即可。 4-进入皇宫,击杀拘灵妖怪、缚仙妖怪、囚神妖怪,需要在12回合内击杀三个妖…

梦幻神器-起-泪痕碗之念-2星

1-该任务需要5个随机指定三级药、7个2级家具,其中2级家具可以提前准备,三级药为NPC随机指定,无法提前准备。 2-前面跟着流程跑,第一场战斗是"清风",需要先击杀小怪,主怪清风在第四回合以后会说"我们放水吧",说了以后才可以击杀主怪清风。3-击杀清风…

STM32F103 SPI配置(SSD1306)

有关SPI通信协议我们在《通信协议-SPI》已经进行了详细的介绍,因此这一节不再重复介绍。 一、软件/硬件SPI 想要控制STM32产生SPI方式的通讯,可以采用软件模拟或硬件SPI这两种方式。 1.1 软件模拟 所谓软件模拟,即直接使用CPU内核按照SPI协议的要求控制GPIO输出高低电平。 1…

梦幻神器-起-莫愁铃之恩-1星

1-该任务需要提前准备5个三级药,金创药、佛光舍利子除外。 2-第一场战斗是击败地府守卫弟子,1星难度不大,注意"诡蝠之刑"的反伤即可,中了"诡蝠之刑"的单位可以适当防御。3-击杀地府守卫弟子以后跟着流程走,接下来需要给三个水晶注入灵气,需要先注入中…

我家一次用电超负荷时20A保险丝断了,但16A的空气开关却没有跳闸.

回答一: 转载自:https://zhidao.baidu.com/question/1970736314255432140.html这说明两者的保护时限不同。无论是保险丝还是空气开关,并不电流达到就立即跳,而且有一定的时间延时来积累热量,热量到了才会动作。 16A的空气开关没跳说明电流虽然超过20A,但时间很快,保险丝…