上一篇文章中,我們分析了 Flutter 布局和渲染的大致實(shí)現(xiàn),這篇文章繼續(xù)介紹 Flutter 的布局過程。
介紹
App 在渲染視圖時(shí),需要在坐標(biāo)系中指定區(qū)域進(jìn)行繪制。Flutter 的坐標(biāo)系是二維空間,這和iOS和安卓的原生坐標(biāo)系是一樣的,屏幕的左上角是坐標(biāo)原點(diǎn)(0, 0),向屏幕右下方延伸,對(duì)應(yīng)寬高兩個(gè)軸。
因此我們需要獲取視圖的位置和大小,才能在坐標(biāo)系中布局和繪制,F(xiàn)lutter 布局的過程即是計(jì)算視圖位置和大小的過程。
原理分析
我們從幾個(gè)問題開始,對(duì) Flutter 布局實(shí)現(xiàn)進(jìn)行分析。
1,布局流程是怎樣的
我們知道,當(dāng)一個(gè)組件的位置和大小都確定了,即可在頁(yè)面中布局。布局流程,便是確定頁(yè)面中各組件的大小和位置,如果某個(gè)組件含有子組件,那么先確定子組件的大小,再確定子組件在其父組件中的位置,以此類推完成整個(gè) UI 的布局。
Flutter 使用盒約束布局,組件按照指定限制條件來決定自身如何占用布局空間,這有點(diǎn)類似前端的 Flex 彈性布局,row、column、flex-start、flex\end、margin、padding等在 Flutter 中都有對(duì)應(yīng)的布局屬性。盒約束布局首先計(jì)算這些限制條件,得出子組件在父組件中的位置和大小,再對(duì)組件進(jìn)行布局和繪制。
Flutter 的布局流程如下圖所示
這是一個(gè)組件渲染樹,每個(gè)節(jié)點(diǎn)對(duì)應(yīng)一個(gè)組件,每個(gè)節(jié)點(diǎn)都是一個(gè) RenderObject 對(duì)象。從根節(jié)點(diǎn)開始,父節(jié)點(diǎn)向子節(jié)點(diǎn)傳遞 Constraints 約束,Constraints 對(duì)象可以限制子節(jié)點(diǎn)的最大和最小寬高,子節(jié)點(diǎn)遵守這個(gè)約束。子節(jié)點(diǎn)確定了自身的寬高之后,傳給父節(jié)點(diǎn),父節(jié)點(diǎn)再根據(jù)子節(jié)點(diǎn)的大小計(jì)算自身的寬高,在這個(gè)步驟中父節(jié)點(diǎn)會(huì)計(jì)算子節(jié)點(diǎn)相對(duì)于父節(jié)點(diǎn)的位置偏移量,即子節(jié)點(diǎn)的位置。以此類推完成整個(gè)頁(yè)面的布局。
2,如何計(jì)算組件寬高
從根節(jié)點(diǎn)開始,向子節(jié)點(diǎn)傳遞 Constraints 約束,Constraints 有四個(gè)屬性,最小寬度minWidth,最大寬度maxWidth,最小高度minHeight,最大高度maxHeight,他們決定了子節(jié)點(diǎn)的大小。如果子節(jié)點(diǎn)有額外的約束條件,則進(jìn)行比對(duì)添加,然后再傳給下一級(jí)。
RenderBox 是真正布局的類,它繼承自 RenderObject,它有兩個(gè)方法 performResize() 和 performLayout() 實(shí)現(xiàn)組件自身的布局邏輯。performLayout 先執(zhí)行子組件的布局,子組件布局完成后得到子組件的大小,再執(zhí)行自身的布局,同時(shí)還要遵守上一級(jí)傳過來的大小限制,比較得到一個(gè)合適的大小。
我們以 Center 為例,說明一下這個(gè)過程。Center 組件繼承自 Align,它對(duì)應(yīng)的 RenderObject 是 RenderPositionedBox,而 RenderPositionedBox 是 RenderBox 的子類,所以 Center 組件的布局方法是在 RenderPositionedBox。我們看一下 RenderPositionedBox 的 performLayout 方法
@override
void performLayout() {
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));
}
如果子組件為空,Center 的寬高也為0。如果子組件存在,先布局子組件,并把約束傳進(jìn)去,這里的 constraints.loosen() 表示最小寬度和最小高度都為0,最大寬度和最大高度都為最大值。
如果需要縮放,則根據(jù)_widthFactor是否為null來進(jìn)行縮放,shrinkWrapWidth 和 shrinkWrapHeight 是縮放后的寬高。
alignChild 方法計(jì)算子組件的位置,入?yún)⒕褪歉附M件的大小減去子節(jié)點(diǎn)的大小,也就是父組件剩余的空間,然后分別對(duì)剩余長(zhǎng)寬除以2得到中值,即是子組件的位置。這里的x 和 y 是表示垂直居中,所以 x 和 y 都是0
void alignChild() {
final BoxParentData childParentData = child.parentData;
childParentData.offset = _resolvedAlignment.alongOffset(size - child.size);
}
Offset alongOffset(Offset other) {
final double centerX = other.dx / 2.0;
final double centerY = other.dy / 2.0;
return Offset(centerX + x * centerX, centerY + y * centerY);
}
3,如何對(duì)布局進(jìn)行性能優(yōu)化
布局更新中,父子節(jié)點(diǎn)會(huì)相互作用,子節(jié)點(diǎn)更新時(shí),會(huì)通知父節(jié)點(diǎn)更新。Flutter 采取了許多措施,盡可能減少需要重新布局的節(jié)點(diǎn),縮小布局范圍。
Flutter 有個(gè)渲染邊界(relayoutBoundary)的概念。處于渲染邊界上的組件大小是固定的,不會(huì)因子組件的變化而變化,邊界內(nèi)的組件更新,更不會(huì)影響到邊界外面的組件。滿足以下任一條件,則該組件是渲染邊界(relayoutBoundary)
parentUsesSize,父節(jié)點(diǎn)傳子節(jié)點(diǎn) Constraints 約束時(shí),同時(shí)將這個(gè)屬性傳傳遞給子組件。它為true時(shí)表示父節(jié)點(diǎn)的布局依賴子節(jié)點(diǎn),子節(jié)點(diǎn)更新時(shí),父節(jié)點(diǎn)也要相應(yīng)更新。為false,表示父組件不依賴子基點(diǎn)。
sizedByParent,這個(gè)是RenderObject的屬性,表示這個(gè)組件完全由父組件控制,不受子組件的影響。
constraints.isTight,表示組件是嚴(yán)格約束,不受子組件的影響。
parent is! RenderObject,父組件不是RenderObject,一般是根組件。
同時(shí)滿足以下三個(gè)條件,則說明該組件不需要重新布局,進(jìn)入下一步。
_needsLayout屬性為false:這說明該組件沒有被自己或孩子標(biāo)記為臟。
constraints == _constraints :父級(jí)的限制沒有發(fā)生變化。
relayoutBoundary沒有發(fā)生變化:說明該組件所屬的渲染邊界沒有發(fā)生變化。
代碼如下
void layout(Constraints constraints, { bool parentUsesSize = false }) {
RenderObject relayoutBoundary;
if (!parentUsesSize || sizedByParent || constraints.isTight || parent is! RenderObject) {
relayoutBoundary = this;
} else {
final RenderObject parent = this.parent;
relayoutBoundary = parent._relayoutBoundary;
}
if (!_needsLayout && constraints == _constraints && relayoutBoundary == _relayoutBoundary) {
return;
}
_constraints = constraints;
_relayoutBoundary = relayoutBoundary;
if (sizedByParent) {
try {
performResize();
} catch (e, stack) {
...
}
}
try {
performLayout();
} catch (e, stack) {
...
}
_needsLayout = false;
markNeedsPaint();
}
參考資料:[Flutter框架分析(六)-- 布局]https://juejin.im/post/5cda66b5e51d453ce55feab8#heading-0
[RenderObject和RenderBox]https://book.flutterchina.club/chapter14/render_object.html