布局過程
Layout(布局)過程主要是確定每一個(gè)組件的布局信息(大小和位置),F(xiàn)lutter 的布局過程如下:
- 父節(jié)點(diǎn)向子節(jié)點(diǎn)傳遞約束(constraints)信息,限制子節(jié)點(diǎn)的最大和最小寬高。
- 子節(jié)點(diǎn)根據(jù)約束信息確定自己的大小(size)。
- 父節(jié)點(diǎn)根據(jù)特定布局規(guī)則(不同布局組件會有不同的布局算法)確定每一個(gè)子節(jié)點(diǎn)在父節(jié)點(diǎn)布局空間中的位置,用偏移 offset 表示。
- 遞歸整個(gè)過程,確定出每一個(gè)節(jié)點(diǎn)的大小和位置。
可以看到,組件的大小是由自身決定的,而組件的位置是由父組件決定的。
下面是官網(wǎng)的一張圖,它用三句話描述了 Flutter 布局過程的精髓:
Flutter 中的布局類組件很多,根據(jù)孩子數(shù)量可以分為單子組件和多子組件,下面我們先通過分別自定義一個(gè)單子組件和多子組件來直觀理解一下Flutter的布局過程,之后會介紹一下布局更新過程和 Flutter 中的 Constraints(約束)。
單子組件布局示例
我們實(shí)現(xiàn)一個(gè)單子組件 CustomCenter
,功能基本和 Center
組件對齊,通過這個(gè)實(shí)例我們演示一下布局的主要流程。
首先,我們定義組件,為了介紹布局原理,我們不采用組合的方式來實(shí)現(xiàn)組件,而是直接通過定制 RenderObject
的方式來實(shí)現(xiàn)。因?yàn)榫又薪M件需要包含一個(gè)子節(jié)點(diǎn),所以我們直接繼承 SingleChildRenderObjectWidget
。
class CustomCenter extends SingleChildRenderObjectWidget {
const CustomCenter2({
Key? key, required Widget child})
: super(key: key, child: child);
RenderObject createRenderObject(BuildContext context) {
return RenderCustomCenter();
}
}
接著實(shí)現(xiàn) RenderCustomCenter
。這里直接繼承 RenderObject
會更接近底層一點(diǎn),但這需要我們自己手動實(shí)現(xiàn)一些和布局無關(guān)的東西,比如事件分發(fā)等邏輯。為了更聚焦布局本身,我們選擇繼承自RenderShiftedBox
,它是RenderBox
的子類(RenderBox
繼承自RenderObject
),它會幫我們實(shí)現(xiàn)布局之外的一些功能,這樣我們只需要重寫performLayout
,在該函數(shù)中實(shí)現(xiàn)子節(jié)點(diǎn)居中算法即可。
class RenderCustomCenter extends RenderShiftedBox {
RenderCustomCenter({
RenderBox? child}) : super(child);
void performLayout() {
//1. 先對子組件進(jìn)行l(wèi)ayout,隨后獲取它的size
child!.layout(
constraints.loosen(), //將約束傳遞給子節(jié)點(diǎn)
parentUsesSize: true, // 因?yàn)槲覀兘酉聛硪褂胏hild的size,所以不能為false
);
//2.根據(jù)子組件的大小確定自身的大小
size = constraints.constrain(Size(
constraints.maxWidth == double.infinity
? child!.size.width
: double.infinity,
constraints.maxHeight == double.infinity
? child!.size.height
: double.infinity,
));
// 3. 根據(jù)父節(jié)點(diǎn)子節(jié)點(diǎn)的大小,算出子節(jié)點(diǎn)在父節(jié)點(diǎn)中居中之后的偏移,然后將這個(gè)偏移保存在
// 子節(jié)點(diǎn)的parentData中,在后續(xù)的繪制階段,會用到。
BoxParentData parentData = child!.parentData as BoxParentData;
parentData.offset = ((size - child!.size) as Offset) / 2;
}
}
布局過程請參考注釋,在此需要額外說明有3點(diǎn):
- 在對子節(jié)點(diǎn)進(jìn)行布局時(shí),
constraints
是CustomCenter
的父組件傳遞給自己的約束信息,我們傳遞給子節(jié)點(diǎn)的約束信息是constraints.loosen()
,下面看一下loosen
的實(shí)現(xiàn)源碼:
BoxConstraints loosen() {
return BoxConstraints(
minWidth: 0.0,
maxWidth: maxWidth,
minHeight: 0.0,
maxHeight: maxHeight,
);
}
很明顯,CustomCenter
約束子節(jié)點(diǎn)最大寬高不超過自身的最大寬高。
-
子節(jié)點(diǎn)在父節(jié)點(diǎn)(
CustomCenter
)的約束下,確定自己的寬高;此時(shí)CustomCenter
會根據(jù)子節(jié)點(diǎn)的寬高確定自己的寬高,上面代碼的邏輯是,如果CustomCenter
父節(jié)點(diǎn)傳遞給它最大寬高約束是無限大時(shí),它的寬高會設(shè)置為它子節(jié)點(diǎn)的寬高。注意,如果這時(shí)將CustomCenter
的寬高也設(shè)置為無限大就會有問題,因?yàn)樵谝粋€(gè)無限大的范圍內(nèi)自己的寬高也是無限大的話,那么實(shí)際上的寬高到底是多大,它的父節(jié)點(diǎn)會懵逼的!屏幕的大小是固定的,這顯然不合理。如果CustomCenter
父節(jié)點(diǎn)傳遞給它的最大寬高約束不是無限大,那么是可以指定自己的寬高為無限大的,因?yàn)樵谝粋€(gè)有限的空間內(nèi),子節(jié)點(diǎn)如果說自己無限大,那么最大也就是父節(jié)點(diǎn)的大小。所以,簡而言之,CustomCenter
會盡可能讓自己填滿父元素的空間。 -
CustomCenter
確定了自己的大小和子節(jié)點(diǎn)大小之后就可以確定子節(jié)點(diǎn)的位置了,根據(jù)居中算法,將子節(jié)點(diǎn)的原點(diǎn)坐標(biāo)算出后保存在子節(jié)點(diǎn)的parentData
中,在后續(xù)的繪制階段會用到,具體怎么用,我們看一下RenderShiftedBox
中默認(rèn)的paint
實(shí)現(xiàn):
void paint(PaintingContext context, Offset offset) {
if (child != null) {
final BoxParentData childParentData = child!.parentData! as BoxParentData;
//從child.parentData中取出子節(jié)點(diǎn)相對當(dāng)前節(jié)點(diǎn)的偏移,加上當(dāng)前節(jié)點(diǎn)在屏幕中的偏移,
//便是子節(jié)點(diǎn)在屏幕中的偏移。
context.paintChild(child!, childParentData.offset + offset);
}
}
performLayout 流程
可以看到,布局的邏輯是在 performLayout
方法中實(shí)現(xiàn)的。我們梳理一下 performLayout
中具體做的事:
- 如果有子組件,則對子組件進(jìn)行遞歸布局。
- 確定當(dāng)前組件的大小(size),通常會依賴子組件的大小。
- 確定子組件在當(dāng)前組件中的起始偏移。
在Flutter組件庫中,有一些常用的單子組件比如 Align、SizedBox、DecoratedBox
等,都可以打開源碼去看看其實(shí)現(xiàn)。
下面我們看一個(gè)多子組件的例子。
多子組件布局示例
實(shí)際開發(fā)中我們會經(jīng)常用到貼邊左-右布局,現(xiàn)在我們就來實(shí)現(xiàn)一個(gè) LeftRightBox
組件來實(shí)現(xiàn)左-右布局,因?yàn)?code>LeftRightBox 有兩個(gè)孩子,用一個(gè) Widget 數(shù)組來保存子組件。
首先我們定義組件,與單子組件不同的是多子組件需要繼承自 MultiChildRenderObjectWidget
:文章來源:http://www.zghlxwxcb.cn/news/detail-485222.html
lass LeftRightBox extends MultiChildRenderObjectWidget {
LeftRightBox({
Key? key,
required List<Widget> children,
}) : assert(children.length == 2, "只能傳兩個(gè)children"),
super(key: key, children: children);
RenderObject createRenderObject(BuildContext context) {
return RenderLeftRight();
}
}
接下來需要實(shí)現(xiàn) RenderLeftRight
,在其 performLayout
中我們實(shí)現(xiàn)實(shí)現(xiàn)左-右布局算法:文章來源地址http://www.zghlxwxcb.cn/news/detail-485222.html
class LeftRightParentData extends ContainerBoxParentData<RenderBox> {
}
class RenderLeftRight extends RenderBox
with
ContainerRenderObjectMixin<RenderBox, LeftRightParentData>,
RenderBoxContainerDefaultsMixin<RenderBox, LeftRightParentData> {
// 初始化每一個(gè)child的parentData
void setupParentData(RenderBox child) {
if (child.parentData is! LeftRightParentData)
child.parentData = LeftRightParentData();
}
void performLayout() {
final BoxConstraints constraints = this.constraints;
RenderBox leftChild = firstChild!;
LeftRightParentData childParentData =
leftChild.parentData! as LeftRightParentData;
RenderBox rightChild = childParentData.nextSibling!;
//我們限制右孩子寬度不超過總寬度一半
rightChild.layout(
constraints.copyWith(maxWidth: constraints.maxWidth / 2),
parentUsesSize
到了這里,關(guān)于Flutter 筆記 | Flutter 核心原理(三)布局(Layout )過程的文章就介紹完了。如果您還想了解更多內(nèi)容,請?jiān)谟疑辖撬阉鱐OY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!