在前面這兩篇文章中,說了Flutter啓動時是如何去構建Widget.Element,RenderObject節點樹。node
而後這篇文章中,會分析一下Flutter中的佈局流程,以及點擊hitTest的調用流程markdown
基本的佈局流程代碼是在RenderObjcet這個類裏處理,可是這是一個最基礎的流程,不包含具體的座標體系,大小等。移動開發中,一般是使用笛卡爾座標。ide
RenderBox是繼承了RenderObjcet,實現了基於笛卡爾座標的佈局。佈局
本文從源碼的角度分析Flutter中layout的基礎流程,以及hitTest的調用流程。可是由於有些內容須要參考,能夠參考post
Widget,Element,RenderObject樹的構建和更新流程測試
RenderObject能夠理解爲一個節點的信息,描述着節點的佈局Layout,圖層Layer和繪製Paint信息。ui
在文章說到,RenderObject是由Widget建立的。當構建Widget樹的時候,也會一併建立RenderObject樹。this
若是一個Widget是跟UI信息有關的,基本基類都是RenderObjectWidget,對應的Element的基類都是RenderObjectElement,並且會對應有一個RenderObject。spa
在Widget更新的時候,會調用RenderObjectElement的update方法
。update以下
@override
void update(covariant RenderObjectWidget newWidget) {
super.update(newWidget);
...
widget.updateRenderObject(this, renderObject);
...
_dirty = false;
}
複製代碼
當Wiget是一個RenderObjectWidget的時候,更新的時候會調用RenderObjectElement的update方法。update方法就會反過來調用RenderObjectWidget的updateRenderObject方法。
而後Widget在updateRenderObject處理RenderObject。若是須要更新佈局的話,就調用RenerObject的markNeedsLayout方法去請求佈局更新。markNeedsLayout的實現以下
void markNeedsLayout() {
...
if (_relayoutBoundary != this) {
//若是當前節點不是佈局邊界,也就是該節點的佈局會影響到父佈局
//markParentNeedsLayout會向上遞歸調用markNeedsLayout()方法,直到父節點是佈局邊界爲止
markParentNeedsLayout();
} else {
_needsLayout = true;
...
//owner是PipelineOwner,用來統一管理佈局,圖層,繪製
owner!._nodesNeedingLayout.add(this);
owner!.requestVisualUpdate();
}
}
}
複製代碼
當調用markNeedsLayout的時候,不是立刻就改動UI界面,而是把這個改動記錄下來。當下次界面更新的時候,把全部的改動一次性修改
像之前提到Widget的構建流程中BuildOwner同樣,一樣存在一個調度中心PipelineOwner。他是負責處理RenderObject樹的佈局,圖層更新,和繪製流程。
當節點有佈局Layout更新需求時,就會調用會markNeedsLayout()方法,把自身添加到PipelineOwner中的_nodesNeedingLayout中列表中,
跟着會去調用PipelineOwner的requestVisualUpdate方法,這個方法會去註冊一個回調,當幀信號發出的時候,就會調用這個回調。回調執行時候,會調用RenderBinding的drawFrame方法(關於這個RenderBinding以及調用流程,能夠查看Flutter App的啓動流程,
這個方法以下
void drawFrame() {
assert(renderView != null);
pipelineOwner.flushLayout();
pipelineOwner.flushCompositingBits();
pipelineOwner.flushPaint();
if (sendFramesToEngine) {
renderView.compositeFrame(); // this sends the bits to the GPU
pipelineOwner.flushSemantics(); // this also sends the semantics to the OS.
_firstFrameSent = true;
}
}
複製代碼
能夠看出,當GPU幀信號發出的時候,會調用PipelineOwner的flushLayout()方法去更新界面上的佈局信息等,而後提交給GPU作渲染。
PS:本文重點講述的是佈局,加上圖層和繪製的處理流程和佈局的流程大體類似,因此這裏重點講得是flushLayout的過程。實現以下
void flushLayout() {
...
while (_nodesNeedingLayout.isNotEmpty) {
final List<RenderObject> dirtyNodes = _nodesNeedingLayout;
_nodesNeedingLayout = <RenderObject>[];
for (final RenderObject node in dirtyNodes..sort((RenderObject a, RenderObject b) => a.depth - b.depth)) {
if (node._needsLayout && node.owner == this)
node._layoutWithoutResize();
}
}
...
}
複製代碼
這裏會取出_nodesNeedingLayout,也就是全部須要更新佈局的節點,對每一個節點調用_layoutWithoutResize()方法。從這一步開始,就開始了節點的佈局流程了。
_layoutWithoutResize()方法,方法以下
void _layoutWithoutResize() {
...
performLayout();
...
_needsLayout = false;
markNeedsPaint();
}
複製代碼
能夠看到,基本上就只是調用了 performLayout()和 markNeedsPaint()這兩個方法
這裏performLayout()就是負責去算出節點自身的位置和大小的。RenderObject中沒有定義performLayout()的實現,具體得讓子類去實現。
並且理所固然的是,當佈局變化了,就須要重繪,因此這裏有調用了一個 markNeedsPaint()標記節點須要重繪。
若是咱們自定一個RenderObjct的子類,是須要實現performLayout()方法去實現的咱們的佈局方法的。若是有多個子節點。那麼咱們還須要調用子節點的 layout(Constraints constraints, { bool parentUsesSize = false }方法。咱們會對子節點約束傳入layout方法中,調用完子節點的layout方法後,咱們就能夠知道子節點所佔用的大小。從而去設置該節點的佈局
這個layout方法是定義在RenderObject方法中的。以下
void layout(Constraints constraints, { bool parentUsesSize = false }) {
...
RenderObject? relayoutBoundary;//是不是佈局邊界,也就是說子節點佈局改變會不會影響父佈局
if (!parentUsesSize || sizedByParent || constraints.isTight || parent is! RenderObject) {
//若是知足如下的條件,則表明該節點是佈局邊界
//1由父節點決定子節點的大小
//2父節點不須要用到子節點的大小
//3給定的約束能肯定惟一的大小
//4父節點不是一個RenderObject
relayoutBoundary = this;
} else {
//不然的話,relayoutBoundary就等於父節點的佈局邊界relayoutBoundary
relayoutBoundary = (parent! as RenderObject)._relayoutBoundary;
}
...
if (!_needsLayout && constraints == _constraints && relayoutBoundary == _relayoutBoundary) {
...
//若是佈局邊界沒有改變,約束沒有改變,也沒有標記爲_needsLayout,則直接結束
return;
}
//更新節點約束
_constraints = constraints;
if (_relayoutBoundary != null && relayoutBoundary != _relayoutBoundary) {
//若是自身佈局邊界改變了,則清空全部的子節點的邊界佈局,並標記_needsLayout爲true
//這樣當該節點layout發生變化的時候,子節點的layout也會發生變化
visitChildren(_cleanChildRelayoutBoundary);
}
//更新_relayoutBoundary
_relayoutBoundary = relayoutBoundary;
...
if (sizedByParent) {
...
//若是是父節點決定子節點的大小,則調用方法,
//performResize是處理節點的大小
//若是sizedByParent是true,則在performResize決定大小,不要在performLayout決定大小
//performResize根據約束_constraints去決定大小
performResize();
...
..
}
...
try {
//調用performLayout()方法
performLayout();
...
}
...
_needsLayout = false;
markNeedsPaint();
...
}
複製代碼
layout方法主要作了如下這幾個事情
_首先第一步這裏是肯定了佈局邊界_relayoutBoundary,這一點其實很重要,結合上面的markNeedsLayout方法來講,當調用markNeedsLayout方法的時候,就是根據 _relayoutBoundary去判斷是否須要一直往上調用markNeedsLayout方法。調用markNeedsLayout越多,影響的節點就會越多,更新的UI速度就會越慢。因此從界面優化的角度上來講,增長 _relayoutBoundary 能夠優化界面的流暢度。
具體能夠經過下方的這個條件去入手
!parentUsesSize || sizedByParent || constraints.isTight || parent is! RenderObject
複製代碼
總的來講,就是減小Widget樹的層級,以及儘可能使用
不影響父節點的Widget。
由父節點決定大小的Widget
能夠由約束肯定惟一的大小的Widget。
這些須要看具體的Widget實現。
到了第二步,根據sizedByParent字段的值,判斷是否調用performResize方法。若是sizedByParent爲true,則表明節點的大小隻有父節點調用layout時候提供的constraints有關係。那麼就調用performResize()這個方法去肯定節點的大小。通常來講,咱們都是經過performLayout()方法去決定節點的大小。可是若是調用了performResize(),就不該該再在performLayout()去改變節點的大小
到了第三步,咱們能夠看到,調用了performLayout()方法。結合前面的流程能夠看出方法的調用以下
父節點performLayout -> 子節點layout -> 子節點performLayout -> 子子節點layout -> 子子節點performLayout -> .......
複製代碼
就是一個節點在佈局的時候,若是存在子節點,就會調用子節點的layout方法並傳入約束,子節點進行佈局。而後一直重複這個過程,直到葉子節點爲止
在查看Flutter的佈局流程的水後,會常常在網上看到一張圖。
由父節點提供約束給子節點,子節點根據約束進行佈局,而後返回給父節點去進行佈局,完成佈局流程。其實這就是第三步所說的這個過程。
至此,大概的佈局流程就是這樣,以下方圖片所示
上方的這些佈局流程都是在RenderObjct的基礎上去展開的,但這只是定義了一個從上往下構建佈局的基本流程。可是不涉及到具體的座標系和節點大小。也就是說一個Widget顯示在界面上的那個位置,佔多少位置,光靠這個基礎的佈局流程是肯定不了的。
Flutter中提供了一個基於笛卡爾積的佈局方式RenderBox。RenderBox是繼承於RenderObjct。在RednderObjct的佈局流程上拓展了笛卡爾座標,節點的大小和命中測試等。Flutter中大部分的RenderObject都是繼承於RenderBox的。
若是你須要自定義座標體系的佈局,能夠繼承RenderObject。不然,繼承RenderBox是一個最好的選擇。
box.dart中定義了BoxConstraints和BoxParentData。分別繼承於Constraints和ParentData。RenderBox中的_constraints和parentData就是這兩種類型。
BoxConstraints定義以下
class BoxConstraints extends Constraints {
...
final double minWidth;//最小寬度
final double maxWidth;//最大寬度
final double minHeight;.//最小高度
final double maxHeight;//最大高度
...
}
複製代碼
BoxParentData定義以下
class BoxParentData extends ParentData {
...
Offset offset = Offset.zero;//基於笛卡爾積的起始點,
...
}
複製代碼
BoxConstraints肯定了節點的大小,BoxParentData肯定了節點的起始點。
每個節點都接受了父子節點傳遞BoxConstraints和BoxParentData,而後按照上方的佈局流程,那麼節點的起始點和大小都能肯定下來。
RenderBox中提供了幾個未實現的方法,子類須要提供實現
double computeMinIntrinsicWidth(double height) //算出最小寬度
double computeMaxIntrinsicWidth(double height) //算出最大寬度
double computeMinIntrinsicHeight(double width) //算出最小高度
double computeMaxIntrinsicHeight(double width) //算出最大高度
Size computeDryLayout(BoxConstraints constraints) //算出父節點給的約束下子節點的大小
複製代碼
經過這些辦法,節點能夠算出應該佔用的尺寸。Flutter中是不建議直接調用這些方法的,而是須要經過調用如下方法獲取
double getMinIntrinsicWidth(double height) //獲得最小寬度
double getMaxIntrinsicWidth(double height) //獲得最大寬度
double getMinIntrinsicHeight(double width) //獲得最小高度
double getMaxIntrinsicHeight(double width) //獲得最大高度
Size getDryLayout(BoxConstraints constraints) //獲得父節點給的約束下子節點的大小
複製代碼
在前面的layout過程當中,performLayout階段會調用子節點的layout方法,而後就能肯定子節點的大小。再經過子節點的getMinIntrinsicxxx或是getDryLayout方法去獲取寬高,獲取子節點的尺寸後就能夠進行自身的佈局。
順帶一提的是,xxxDryLayout方法是Flutter2.0之後纔有的,這個方法是用來替代performResize方法的。也就是說若是一個節點的大小隻有父節點的約束決定,那麼不該該在performLayout方法中算出節點的大小,而應該在computeDryLayout計算出節點的大小。
而另外xxxDryLayout方法能夠在不改變RenderObjct的其餘狀態的狀況下,算出節點應該佔用的大小。這裏的DryLayout中的Dry就是相對普通layout方法而言的,從上面可知,layout方法是會改變邊界佈局,約束等。
在佈局完成後,界面UI也顯示完整了,那麼這時候用戶點擊了某個Widget,這個點擊事件是怎麼傳遞呢?這裏以點擊事件爲例,說明事件傳遞的流程
上一篇文章提到,在App啓動的時候會初始化一系列Binding,其中有一個是GestureBinding。當點擊事件出現時,會調用GestureBinding的_handlePointerDataPacket方法,通過事件採用的操做最終會調用_handlePointerEventImmediately(PointerEvent event)方法,調用流程以下
_handlePointerEventImmediately以下
void _handlePointerEventImmediately(PointerEvent event) {
HitTestResult? hitTestResult;
if (event is PointerDownEvent || event is PointerSignalEvent || event is PointerHoverEvent) {
...
hitTestResult = HitTestResult();//存儲hitTest結果
hitTest(hitTestResult, event.position);//進行hitTest
if (event is PointerDownEvent) {
_hitTests[event.pointer] = hitTestResult;
}
...
} else if (event is PointerUpEvent || event is PointerCancelEvent) {
hitTestResult = _hitTests.remove(event.pointer);
} else if (event.down) {
...
hitTestResult = _hitTests[event.pointer];
}
...
}());
if (hitTestResult != null ||
event is PointerAddedEvent ||
event is PointerRemovedEvent) {
assert(event.position != null);
dispatchEvent(event, hitTestResult);//分發事件
}
}
複製代碼
能夠看到,這裏最主要是兩步
由於Binding的mixin的設計,這裏的hitTest方法會走到RenderBinding的hitTest方法中,以下
@override
void hitTest(HitTestResult result, Offset position) {
...
renderView.hitTest(result, position: position);
//這裏調用了super.hitTest,這個定義在GestureBing當中
//會把Bingding也放入到hitTestResult中
super.hitTest(result, position);
}
複製代碼
這裏會調用renderView.hitTest(result, position: position)方法。這裏的renderView就是App啓動的時候RenderObjct樹的根節點。它是RenderView類型的,繼承於RenderObject,mixin了RenderObjectWithChildMixin。其hitTest方法以下
bool hitTest(HitTestResult result, { required Offset position }) {
if (child != null)
child!.hitTest(BoxHitTestResult.wrap(result), position: position);
result.add(HitTestEntry(this));
return true;
}
複製代碼
由於mixIn了RenderObjectWithChildMixin,因此當調用了子節點的hitTest方法的時候,會走到RenderBox的hitTest方法。以下
bool hitTest(BoxHitTestResult result, { required Offset position }) {
...
if (_size!.contains(position)) {
if (hitTestChildren(result, position: position) || hitTestSelf(position)) {
result.add(BoxHitTestEntry(this, position));
return true;
}
}
return false;
}
複製代碼
這裏的hitTest調用hitTestChildren和hitTestSelf方法。這兩個方法默認返回false,應該交由具體的子類實現。
hitTestChildren方法用於處理判斷子節點是否命中測試,hitTestSelf判斷節點自己是否響應命中測試。若是命中,就往命中測試結果中添加該節點。
通常而言,hitTestChildren方法中通常都會調用子節點的hitTest方法,經過
hitTest -> hitTestChildren -> hitTest -> hitTestChildren -> ....
複製代碼
這個流程,會把全部符合命中測試的結果都存到GestureBinding的_handlePointerEventImmediately方法中的hitTestResult中,也就是說,在
獲得hitTestResult之後,就執行dispatchEvent方法,以下
void dispatchEvent(PointerEvent event, HitTestResult? hitTestResult) {
...
//便利result
for (final HitTestEntry entry in hitTestResult.path) {
...
//事情處理與分發
entry.target.handleEvent(event.transformed(entry.transform), entry);
...
}
}
複製代碼
由於這裏涉及不少的事件分發的處理,邊幅較大,因此不在這裏討論。
這裏主要分析了佈局流程,可是沒有詳細的具體例子(否則文章篇幅暴漲),可是讀者能夠閱讀源碼的時候能夠結合具體的例子去看,這裏推薦看Stack的實現,由於這個Widget的佈局計算相對簡單。