在Widget
篇中,提到了RenderObject
,那麼RenderObject
到底是個啥?我們來捋一捋!
官方解釋
An object in the render tree.
render樹上的一個物件。
Flutter門前有四棵樹,一棵是Widget
樹,一棵是Element
樹,一棵是Render
樹,還有一棵是layer
樹。接下來盤一盤這棵Render
樹。
Widget
樹是一張圖紙,那麼Render
樹就是這張圖紙對應的流水線。RenderObject
就是這條流水線上的操作員工。
我們在頂層配置好Widget
樹後,最終render
樹負責在手機螢幕上表現出你想要的畫面。
閱讀原始碼
美好的一天應當從閱讀原始碼開始!
abstract class RenderObject extends AbstractNode
with DiagnosticableTreeMixin
implements HitTestTarget {}
複製程式碼
可以看到RenderObject
也是連結串列結構,混入了DiagnosticableTreeMixin
樹狀結構的特性,並且實現了命中測試抽象類。
去掉debug和assert的程式碼,RenderObject
的結構功能清晰可見。
Layout 模組
abstract class RenderObject {
// LAYOUT 模組
// 傳遞資訊給child的儲存容器 通常為偏移量Offset
ParentData parentData;
// 此方法,在child加入到child列表之前,把parentData傳遞給child
void setupParentData(covariant RenderObject child) {
if (child.parentData is! ParentData)
child.parentData = ParentData();
}
// 新增一個render object 作為自己的child
@override
void adoptChild(RenderObject child) {
// 設定parentData
setupParentData(child);
// 標記更新
markNeedsLayout();
markNeedsCompositingBitsUpdate();
markNeedsSemanticsUpdate();
super.adoptChild(child);
}
// 刪除一個render object 作為自己的child
@override
void dropChild(RenderObject child) {
// 清除邊界
child._cleanRelayoutBoundary();
// 失去parentdata訪問許可權
child.parentData.detach();
child.parentData = null;
super.dropChild(child);
markNeedsLayout();
markNeedsCompositingBitsUpdate();
markNeedsSemanticsUpdate();
}
// 遍歷children
void visitChildren(RenderObjectVisitor visitor) { }
// render tree的管理者
@override
PipelineOwner get owner => super.owner;
// 告訴其owner將其插入render tree 並初次標記需要計算layout並且重繪
@override
void attach(PipelineOwner owner) {
super.attach(owner);
if (_needsLayout && _relayoutBoundary != null) {
_needsLayout = false;
markNeedsLayout();
}
if (_needsCompositingBitsUpdate) {
_needsCompositingBitsUpdate = false;
markNeedsCompositingBitsUpdate();
}
if (_needsPaint && _layer != null) {
_needsPaint = false;
markNeedsPaint();
}
if (_needsSemanticsUpdate && _semanticsConfiguration.isSemanticBoundary) {
_needsSemanticsUpdate = false;
markNeedsSemanticsUpdate();
}
}
// 是否需要佈局
bool _needsLayout = true;
// 記錄佈局邊界在哪
RenderObject _relayoutBoundary;
// parent的約束
@protected
Constraints get constraints => _constraints;
Constraints _constraints;
//標記需要重新layout
void markNeedsLayout() {
// 如果自己不是邊界,則讓parent.markNeedsLayout處理 一直推到邊界
if (_relayoutBoundary != this) {
markParentNeedsLayout();
} else {
// owner 把自己加入layout的髒表 請求重新整理
_needsLayout = true;
if (owner != null) {
owner._nodesNeedingLayout.add(this);
owner.requestVisualUpdate();
}
}
}
// 遍歷清除非邊界child relayout邊界記錄 當邊界發生變化時,會呼叫此方法清除邊界快取
void _cleanRelayoutBoundary() {
if (_relayoutBoundary != this) {
_relayoutBoundary = null;
_needsLayout = true;
visitChildren((RenderObject child) {
child._cleanRelayoutBoundary();
});
}
}
// 僅layout 不重新測量大小
void _layoutWithoutResize() {
performLayout();
markNeedsSemanticsUpdate();
_needsLayout = false;
markNeedsPaint();
}
void layout(Constraints constraints, { bool parentUsesSize = false }) {
RenderObject relayoutBoundary;
// 確定邊界
// 滿足以下四項條件 中的一項 則自己為邊界
// parentUsesSize parent是否關心自己的大小
// sizedByParent 由parent確認大小
// constraints.isTight 受嚴格約束
// parent 不為 RenderObject 即自己為root節點
if (!parentUsesSize || sizedByParent || constraints.isTight || parent is! RenderObject) {
relayoutBoundary = this;
} else {
// 否則使用parent的邊界
final RenderObject parent = this.parent;
relayoutBoundary = parent._relayoutBoundary;
}
_constraints = constraints;
// 如果邊界發生變化 則遍歷清空所有已記錄的邊界 重新設定
if (_relayoutBoundary != null && relayoutBoundary != _relayoutBoundary) {
visitChildren((RenderObject child) {
child._cleanRelayoutBoundary();
});
}
_relayoutBoundary = relayoutBoundary;
// sizedByParent 為true 時 才會呼叫performResize 確定大小
// 否則在performLayout中確定size
if (sizedByParent) {
performResize();
}
// 計算layout
performLayout();
markNeedsSemanticsUpdate();
_needsLayout = false;
markNeedsPaint();
}
// size是否只由parent的約束決定 預設為false 永遠不會錯
// 當為true時,確定size的工作要在performResize中計算
@protected
bool get sizedByParent => false;
// 子類重寫此方法 根據約束 計算自身的size
// 不能直接呼叫此方法 而是呼叫layout方法間接呼叫 只有sizedByParent為true時才會執行
@protected
void performResize();
// 子類重寫此方法
// 不能直接呼叫此方法 而是呼叫layout方法間接呼叫
// 作用1:當sizedByParent為false時 根據適應child計算自身size
// 作用2:遍歷呼叫child.layout 確定child的size和offsets
@protected
void performLayout();
}
複製程式碼
劃一下重點:
RenderObject
作為Render Tree
上的一個物件,但是自己卻不能直接改變這棵大樹。一舉一動都需要傳達給PipelineOwner
這位管理者去執行。PipelineOwner
管理著幾個汙汙的(dirty)小本本,記錄了需要變化的節點,根據這些小本本去實際更新Render Tree
。當RenderObject
需要發生改變時,通知owner把自己寫到小本本上(標記為dirty)。可以理解為軍訓過程中,有什麼問題報告給教官,教官統一安排。parent
通過setupParentData
方法,傳遞parentData
,通常為layout offset
。performResize
方法需要子類重寫。計算自身的大小performLayout
方法需要子類重寫。
- 當sizedByParent為false時,計算size。
- 2.遍歷呼叫child.layout,確定child的size和offsets。
- 核心方法是
layout
。
確定`_relayoutBoundary`佈局邊界
->呼叫`performResize`和`performLayout`方法計算大小和位置
->重繪
複製程式碼
由於這個流程滿足大多數場景,因此當我們真正開發時,只關心重寫performResize
和performLayout
的實現,而不會去重寫layout
方法。
Paint 模組
abstract class RenderObject {
// PAINTING 模組
RenderObject() {
// 是否需要混合圖層 = 是重繪邊界 || 總是混合圖層
_needsCompositing = isRepaintBoundary || alwaysNeedsCompositing;
}
// 當前是否為重繪邊界
bool get isRepaintBoundary => false;
// 是否總是新建圖層然後合併到原圖層
@protected
bool get alwaysNeedsCompositing => false;
// 快取的layer
ContainerLayer _layer;
// 是否_needsCompositing的值需要設定
bool _needsCompositingBitsUpdate = false;
// 標記_needsCompositing的值需要重新設定
void markNeedsCompositingBitsUpdate() {
if (owner != null)
owner._nodesNeedingCompositingBitsUpdate.add(this);
}
// 是否在一個新的圖層繪製然後合併到祖先圖層
// true:在新圖層繪製 但是新圖層會優先使用快取圖層 以提高效能
// false:不使用新圖層 此時快取圖層一定要置null
// 當前為repaintBoundary時 _needsCompositing=true 並且會自動給快取layer賦值為新的OffsetLayer 在此layer上繪製後 合併到祖先圖層
bool _needsCompositing;
// 遍歷設定需要使用圖層混合
void _updateCompositingBits() {
if (!_needsCompositingBitsUpdate)
return;
final bool oldNeedsCompositing = _needsCompositing;
_needsCompositing = false;
visitChildren((RenderObject child) {
child._updateCompositingBits();
if (child.needsCompositing)
_needsCompositing = true;
});
if (isRepaintBoundary || alwaysNeedsCompositing)
_needsCompositing = true;
if (oldNeedsCompositing != _needsCompositing)
markNeedsPaint();
_needsCompositingBitsUpdate = false;
}
// 是否需要繪製
bool _needsPaint = true;
// 標記需要重繪
void markNeedsPaint() {
_needsPaint = true;
// 如果當前為repaintBoundary 則通知owner需要重繪
if (isRepaintBoundary) {
if (owner != null) {
owner._nodesNeedingPaint.add(this);
owner.requestVisualUpdate();
}
}
// 如果當前不為root節點 則讓parent判斷
else if (parent is RenderObject) {
final RenderObject parent = this.parent;
parent.markNeedsPaint();
}
// 若當前為root節點 則直接通知owner重繪
else {
if (owner != null)
owner.requestVisualUpdate();
}
}
// 根節點初始化 見[RenderView]
void scheduleInitialPaint(ContainerLayer rootLayer) {
_layer = rootLayer;
owner._nodesNeedingPaint.add(this);
}
// 替換rootLayer 只有root節點才會呼叫
// 當裝置的畫素比device pixel ratio變化時 可能會呼叫此方法
void replaceRootLayer(OffsetLayer rootLayer) {
_layer.detach();
_layer = rootLayer;
markNeedsPaint();
}
// 子類重寫此方法 在對應的offset完成真正的繪製操作
// 不可直接呼叫此方法 而是用markNeedsPaint標記 讓owner去處理
// 如果只想繪製一個child 則用PaintingContext.paintChild 介面的方式去操作 避免直接操作render object
void paint(PaintingContext context, Offset offset) { }
}
複製程式碼
本模組中最重要的一點就是needsCompositing
這個變數。這個變數決定是否在新的layer
上繪製。在構造器中可以看到,它由isRepaintBoundary
和alwaysNeedsCompositing
決定。isRepaintBoundary
這個可以通過RepaintBoundary
包裹修改其為true
。alwaysNeedsCompositing
可以由子類修改。
因此我們可以通過使用RepaintBoundary
這個控制元件達到區域性重繪的目的,以提高效能。
SEMANTICS 語義化模組 和 HIT TESTING 命中測試模組
語義化即Semantics,主要是提供給讀屏軟體的介面,也是實現輔助功能的基礎,通過語義化介面可以讓機器理解頁面上的內容,對於有視力障礙使用者可以使用讀屏軟體來理解UI內容。除非有特殊的需求,一般接觸不到。
通過以下方法,處理命中測試結果。
// 重寫此方法處理事件
@override
void handleEvent(PointerEvent event, covariant HitTestEntry entry) { }
複製程式碼
RenderObject
中還有兩個比較重要的方法,也提一下。
// 是否重灌 只在debug模式下生效 也就是開發中的hot reload效果
void reassemble() {
// 標記為需要重新layout
markNeedsLayout();
// 標記重新設定是否使用新圖層
markNeedsCompositingBitsUpdate();
// 標記需要重繪
markNeedsPaint();
// 標記語義化需要更新
markNeedsSemanticsUpdate();
// 遍歷child的reassemble方法 全部標記一遍
visitChildren((RenderObject child) {
child.reassemble();
});
}
// 是否顯示在螢幕上 viewport中會使用到
void showOnScreen({
RenderObject descendant,
Rect rect,
Duration duration = Duration.zero,
Curve curve = Curves.ease,
}) {
if (parent is RenderObject) {
final RenderObject renderParent = parent;
renderParent.showOnScreen(
descendant: descendant ?? this,
rect: rect,
duration: duration,
curve: curve,
);
}
}
複製程式碼
PipelineOwner
The pipeline owner manages the rendering pipeline.
整個渲染流程的管理者,具體表現在如下幾個方法:
- flushLayout
遍歷需要relayout的render object節點,呼叫_layoutWithoutResize()
重寫計算佈局
- flushCompositingBits
遍歷需要CompositingBitsUpdate的節點,呼叫_updateCompositingBits()
方法更新needsCompositing
。通常在flushLayout
和flushPaint
之間執行。
- flushPaint
遍歷需要repaint的節點,通過PaintingContext
呼叫子render object
的_paintWithContext
方法觸發paint
繪製。
- flushSemantics
更新語義化
RenderBox
RenderBox
是RenderObject
的子類,是在2D笛卡爾座標系下對RenderObject
的進一步封裝。它主要封裝瞭如下幾個功能點:
- parentdata是
BoxParentData
只有offset
屬性 預設為Offset.zero
- 使用
BoxConstraints
作為其約束 - 使用
size
記錄其大小 - 測量自身最大最小寬高
- 測量基線
- 實現預設的命中測試方案
- 混入
RenderObjectWithChildMixin
單個child的實現->SingleChildRenderObjectWidget
- 混入
ContainerRenderObjectMixin
多個children的實現->MultiChildRenderObjectWidget
開發App在笛卡爾座標系上進行繪製,所以繼承RenderBox
就可以滿足大部分場景了。
總結
本節主要記錄了RenderObject
主要的功能和方法,理解這些內容可以幫助我們更好的理解Flutter UI底層原理,對我們實現自定義的控制元件也有幫助。至於具體如何根據佈局繪製到螢幕上,後文會使用例項分析。
搞懂原理真是傷頭髮啊!