Flutter —布局系统概述

老孟导读:此篇文章非常详细的讲解了 Flutter 布局系统的工作原理。

最近,我决定专注于Flutter基础知识。这次,我试图更好地理解“布局系统的工作原理”,并回答以下问题:
- 我的小部件的尺寸看起来不合适,怎么回事?
- 我只想将Widget放置在特定位置,但是没有任何属性可以控制它,为什么呢?
- 我一直看到诸如BoxConstraints,RenderBox和Size之类的术语。它们之间有什么关系?
- 对布局系统如何工作有一个大概的了解?
本文并不意味着对以上所有内容进行深入而详细的描述。但是,我们将对最重要的内容进行很好的概述,力图将一切可视化。
“两个阶段” 布局系统和约束
首先,小部件是Flutter SDK的构建块,但它们不负责将其自身绘制到屏幕中。每个小部件都与负责此操作的RenderBox对象相关联。这些框是2D直角坐标系,其大小表示为距原点的偏移。每个RenderBox还将与一个BoxConstraints对象相关联,该对象包含四个值:最大|最小宽度和最大|最小高度。 RenderBox可以选择具有所需的任何大小,但它必须遵守这些值/约束。小部件的大小/位置完全取决于这些RenderBox的属性。
原文:The same way Widgets build a Widget three, RenderBoxes make a render three.
我觉得three可能写错了,应该是tree,译文:以同样的方式小部件生成 组件树,RenderBoxes生成渲染树。

我们可以将Flutter的布局系统视为两阶段系统。在第一个阶段中,framework 以递归地方式沿着渲染树 把BoxConstraints传递给子组件。它为父组件提供了一种方式来调节/增强子组件的尺寸,并根据需要更新这些限制。换句话说,这是负责传播约束信息的阶段,让每个人知道其最大/最小值。
完成后,第二阶段开始。这次,每个RenderBox都将其选择的大小传递回其父对象。父级收集所有子级的大小,然后使用此几何信息将每个子级正确定位在自己的笛卡尔系统中。这个阶段负责确定大小和位置,在此阶段,父组件知道每个子组件的大小以及他们的位置。
那么,这到底意味着什么?
这意味着父组件有责任定义/限制/约束子组件的尺寸,并相对于其坐标系进行定位。换句话说,小部件可以选择其大小,但是它必须始终遵守从其父级收到的约束。此外,小部件不知道其在屏幕上的位置,但其父级知道。
如果您对小部件的大小或位置有疑问,请尝试查看(更新)其父组件。
Example
好的,让我们将所有内容可视化,尝试通过示例了解正在发生的事情。但是在此之前,以下是一些在调试约束时可能有用的术语,
下面的术语未翻译,因为这些术语本身比译文更好理解:
- If *max(w|h) = min (w|h)*, that is *tightly* constrained.
- If *min(w|h) = 0*, we have a *loose* constraint.
- If *max(w|h) != infinite*, the constraint is *bounded.*
- If *max(w|h) = infinite*, the constraint is *unbounded.*
- If *min(w|h) = infinite*, is just said to be *infinite*

我们将使用的是初始应用模板的修改版本。通常,您可以通过两种简单的方法来检查窗口小部件RenderBox及其属性:
通过代码执行:我们可以使用LayoutBuilder在布局系统第一阶段拦截BoxConstraints传播,并检查约束。然后,在第二阶段完成后,我们使用键来获取小部件的RenderBox并能够检查Size,Position。
或使用DevTools窗口小部件检查器

import 'package:flutter/material.dart';
GlobalKey _keyMyApp = GlobalKey();
GlobalKey _keyMaterialApp = GlobalKey();
GlobalKey _keyHomePage = GlobalKey();
GlobalKey _keyScaffold = GlobalKey();
GlobalKey _keyAppbar = GlobalKey();
GlobalKey _keyCenter = GlobalKey();
GlobalKey _keyFAB = GlobalKey();
GlobalKey _keyText = GlobalKey();
void printConstraint(String name, BoxConstraints c) {
print(
'CONSTRAINT of $name: min(w=${c.minWidth.toInt()},h=${c.minHeight.toInt()}) max(w=${c.maxWidth.toInt()},h=${c.maxHeight.toInt()})',
);
}
void printSizes() {
printSize('MyApp', _keyMyApp);
printSize('MaterialApp', _keyMaterialApp);
printSize('HomePage', _keyHomePage);
printSize('Scaffold', _keyScaffold);
printSize('Appbar', _keyAppbar);
printSize('Center', _keyCenter);
printSize('Text', _keyText);
printSize('FAB', _keyFAB);
}
void printSize(String name, GlobalKey key) {
final RenderBox renderBox = key.currentContext.findRenderObject();
final size = renderBox.size;
print("SIZE of $name: w=${size.width.toInt()},h=${size.height.toInt()}");
}
void printPositions() {
printPosition('MyApp', _keyMyApp);
printPosition('MaterialApp', _keyMaterialApp);
printPosition('HomePage', _keyHomePage);
printPosition('Scaffold', _keyScaffold);
printPosition('Appbar', _keyAppbar);
printPosition('Center', _keyCenter);
printPosition('Text', _keyText);
printPosition('FAB', _keyFAB);
}
void printPosition(String name, GlobalKey key) {
final RenderBox renderBox = key.currentContext.findRenderObject();
final position = renderBox.localToGlobal(Offset.zero);
print("POSITION of $name: $position ");
}
void main() {
runApp(LayoutBuilder(
builder: (context, constraints) {
printConstraint('MyApp', constraints);
return MyApp();
},
));
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return LayoutBuilder(
key: _keyMyApp,
builder: (context, constraints) {
printConstraint('MaterialApp', constraints);
return MaterialApp(
key: _keyMaterialApp,
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
visualDensity: VisualDensity.adaptivePlatformDensity,
),
home: LayoutBuilder(
builder: (context, constraints) {
printConstraint('HomePage', constraints);
return HomePage(
key: _keyHomePage,
title: 'Flutter Demo Home Page',
);
},
),
);
},
);
}
}
class HomePage extends StatefulWidget {
HomePage({Key key, this.title}) : super(key: key);
final String title;
@override
_HomePageState createState() => _HomePageState();
}
class _HomePageState extends State<HomePage> {
int _counter = 0;
void _incrementCounter() {
setState(() {
_counter++;
});
}
@override
void initState() {
WidgetsBinding.instance.addPostFrameCallback(_afterLayout);
super.initState();
}
void _afterLayout(_) {
printSizes();
printPositions();
}
@override
Widget build(BuildContext context) {
return LayoutBuilder(builder: (context, constraints) {
printConstraint('Scaffold', constraints);
return Scaffold(
backgroundColor: Colors.purple,
key: _keyScaffold,
appBar: AppBar(
key: _keyAppbar,
title: Text(widget.title),
),
body: LayoutBuilder(
builder: (context, constraints) {
printConstraint('Center', constraints);
return Center(
key: _keyCenter,
child: LayoutBuilder(builder: (context, constraints) {
printConstraint('Text', constraints);
return Text(
'You have pushed the button this many times:',
key: _keyText,
style: TextStyle(color: Colors.white),
);
}),
);
},
),
floatingActionButton: LayoutBuilder(
builder: (context, constraints) {
printConstraint('FAB', constraints);
return FloatingActionButton(
key: _keyFAB,
onPressed: printSizes,
tooltip: 'Increment',
child: Icon(Icons.add),
);
},
),
);
});
}
}

让我们一步一步来看看发生了什么(在这里我们将忽略LayoutBuilders)。

在我们的示例中发生的第一件事是执行runApp(..)。此函数检查屏幕当前大小(在我们的示例中为392:759),然后创建一个BoxConstraints对象,其中包含将发送到我们的第一个小部件(MyApp)的约束。注意,max | min的宽度和高度都相等;因此,runApp使用了严格的约束-通过这样做,MyApp除了选择屏幕上的可用空间外,在选择其大小时将别无选择。

然后将约束向下传播到Widget树。 MyApp,MaterialApp,HomePage和Scaffold都被告知相同的严格约束。因此,所有人将被迫填满整个屏幕。每个小部件都有机会向其子项通知不同的BoxConstraints(仍然尊重已收到的子项)。但是,在这种情况下,他们选择不这样做。

现在事情开始变得越来越有趣。Scaffold告知AppBar有关必须使用的BoxConstraints的信息,但是,这一次,它使用了宽松的约束(min h = 0)。它使AppBar有机会选择所需的任何高度,但仍必须使用width = 390。
AppBar是一种特殊的小部件,称为PreferredSizeWidget。这种类型的小部件不会对其子级施加任何约束。如果尝试使用LayoutBuilder获取Title的约束,则会出现错误。而是,AppBar以首选/默认大小响应Scaffold:高度= 80,宽度= 392(受接收到的约束的约束)
获得AppBar的大小后,Scaffold继续下一个子项:Center

好的,这里发生了很多事情。让我们尝试了解:
- Scaffold告知Center其约束,让其选择在 0 < width < 392 和 0 < height < 697 中选择。请注意,最大高度为759(屏幕最大高度)减去80(AppBar选择的高度)。
- Center转到其子组件“Text”,转发相同的约束。
- Text选择一个足以显示其数据的大小(279:16),然后回复Center。
- 借助手上的几何信息(大小),Center可以在其笛卡尔系统内正确定位文本。作为父母,Center有权选择其子组件位置,在这种情况下,它决定将其居中。
流程继续:

- 然后,Center为自己选择一个大小,而不是仅选择一个“足够”的大小(如“Text”一样),而是决定尽可能大,因此受到了限制。
- Scaffold收到Center所需的尺寸,并且流程继续向其最后一个孩子:FAB
- FAB收到约束,然后将其首选大小返回给Scaffold(56:56)
- 最后,Scaffold还具有将每个孩子都放置在其笛卡尔系统内所需的所有几何信息。
最后,对Scaffold以上的所有小部件重复该过程:

- Size信息继续沿渲染树传播。
- 每个小部件都使用此信息将每个孩子放置在笛卡尔系统内。
- Scaffold回复HomePage,HomePage回复MaterialApp,MaterialApp回复MyApp。直到最后再次到达Main。
- Main获取此“最终”窗口小部件,并将其最终绑定到屏幕中。
RenderBox树最终绑定在屏幕上。我们有一个正在运行的应用程序。
有趣的事情要记住
- 小部件不知道其在屏幕上的位置;它的父组件才知道。
- 小部件可以选择想要的大小,但必须根据其父级的限制。
- 约束向下传播,而大小向上传播。
- 尝试了解约束条件,它们可能在以后有用。
我希望所有这些都可以帮助您更好地了解Flutter布局系统的工作方式。
交流
老孟Flutter博客地址(330个控件用法):http://laomengit.com
欢迎加入Flutter交流群(微信:laomengit)、关注公众号【老孟Flutter】:
![]() |
![]() |
Flutter —布局系统概述的更多相关文章
- Flutter 布局控件完结篇
本文对Flutter的29种布局控件进行了总结分类,讲解一些布局上的优化策略,以及面对具体的布局时,如何去选择控件. 1. 系列文章 Flutter 布局详解 Flutter 布局(一)- Conta ...
- Flutter 布局(五)- LimitedBox、Offstage、OverflowBox、SizedBox详解
本文主要介绍Flutter布局中的LimitedBox.Offstage.OverflowBox.SizedBox四种控件,详细介绍了其布局行为以及使用场景,并对源码进行了分析. 1. Limited ...
- Flutter 布局(三)- FittedBox、AspectRatio、ConstrainedBox详解
本文主要介绍Flutter布局中的FittedBox.AspectRatio.ConstrainedBox,详细介绍了其布局行为以及使用场景,并对源码进行了分析. 1. FittedBox Scale ...
- Flutter 布局(四)- Baseline、FractionallySizedBox、IntrinsicHeight、IntrinsicWidth详解
本文主要介绍Flutter布局中的Baseline.FractionallySizedBox.IntrinsicHeight.IntrinsicWidth四种控件,详细介绍了其布局行为以及使用场景,并 ...
- Flutter 布局(二)- Padding、Align、Center详解
本文主要介绍Flutter布局中的Padding.Align以及Center控件,详细介绍了其布局行为以及使用场景,并对源码进行了分析. 1. Padding A widget that insets ...
- Flutter 布局详解
本文主要介绍了Flutter布局相关的内容,对相关知识点进行了梳理,并从实际例子触发,进一步讲解该如何去进行布局. 1. 简介 在介绍Flutter布局之前,我们得先了解Flutter中的一些布局相关 ...
- Flutter 布局(十)- ListBody、ListView、CustomMultiChildLayout详解
本文主要介绍Flutter布局中的ListBody.ListView.CustomMultiChildLayout控件,详细介绍了其布局行为以及使用场景,并对源码进行了分析. 1. ListBody ...
- Flutter 布局(七)- Row、Column详解
本文主要介绍Flutter布局中的Row.Column控件,详细介绍了其布局行为以及使用场景,并对源码进行了分析. 1. Row A widget that displays its children ...
- Flutter 布局(八)- Stack、IndexedStack、GridView详解
本文主要介绍Flutter布局中的Stack.IndexedStack.GridView控件,详细介绍了其布局行为以及使用场景,并对源码进行了分析. 1. Stack A widget that po ...
随机推荐
- Golang | Go语言多态的实现与interface使用
本文始发于个人公众号:TechFlow,原创不易,求个关注 今天是golang专题的第11篇文章,我们一起来聊聊golang当中多态的这个话题. 如果大家系统的学过C++.Java等语言以及面向对象的 ...
- AsyncTask被废弃了,换Coroutine吧
本文主要是学习笔记,有版权问题还请告知删文 鸣谢:guolin@第一行代码(第三版) 你是否也在最近的代码中看见了 AsyncTask 被一条横杠划掉了 这表明--他要被Google放弃了 Googl ...
- jQuery 购物车案例
h1 { text-align: center; } .cart { width: 1200px; height: 600px; margin: 0 auto; border: 1px solid # ...
- Java连接Redis,存储对象获取对象()byte和json),连接池
Java连接Redis Jedis连接Redis,Lettuce连接Redis Jedis连接Redis 1. 创建maven项目 2. 引入依赖 <dependencies> <d ...
- C#LeetCode刷题之#671-二叉树中第二小的节点(Second Minimum Node In a Binary Tree)
问题 该文章的最新版本已迁移至个人博客[比特飞],单击链接 https://www.byteflying.com/archives/4100 访问. 给定一个非空特殊的二叉树,每个节点都是正数,并且每 ...
- 跳转语句—break,continue,goto
#define _CRT_SECURE_NO_WARNINGS#include<stdio.h>#include<string.h>#include<stdlib.h&g ...
- Enumerable 下又有新的扩展方法啦,快来一起一睹为快吧
一:背景 1. 讲故事 前段时间将公司的一个项目从 4.5 升级到了 framework 4.8 ,编码的时候发现 Enumerable 中多了三个扩展方法: Append, Prepend, ToH ...
- JavaScript中的正则表达式详解
摘要:javascript中的正则表达式作为相当重要的知识,本文将介绍正则表达式的相关知识和用法. 正则表达式(Regular Expression)是一门简单语言的语法规范,是强大.便捷.高效的文本 ...
- 这样Review代码牛逼啦!
这样Review代码牛逼啦! 一个对项目负责的团队代码质量检查是必不可少的,有条件的团队经常有代码review习惯,这样可以使技术团队共同进步,但是一个庞大的工程做代码review其实是很麻烦的,所以 ...
- 虚拟化技术之kvm管理工具virsh常用基础命令(一)
在上一篇博客中,我们了解了KVM基础架构和部署以及图形管理工具virt-manager安装虚拟机的过程,回顾请参考https://www.cnblogs.com/qiuhom-1874/p/13499 ...

