一,概述
Flutter 本身提供了大量Widget以供開發,但是難免有通過組合完成不了的效果,此時就需要我們自己來實現 RenderObject 了,本文會介紹一下實現一個 RenderObject 的基本步驟,幫助大家快速熟悉開發自定義控件的流程,當然這對於讀懂原生 Widget 的實現源碼也有很大的益處。
二,RenderObject 類繼承層級解析
首先,介紹一下 RenderObject 子類的繼承關系,通過 Android Studio 的 Hierarchy 功能可以直觀地對類繼承關系進行查看:


看過源碼分析系列相關文章中對 runApp()
方法的解析后應該知道,RenderView 對應的是 RenderObject 樹的根節點,打開該類的注釋,發現有這樣一句話:
The view has a unique child [RenderBox], which is required to fill the entire output surface.
意為 RenderView 根節點下只有唯一一個 RenderBox 作為葉節點,它的大小會充滿整個繪制表面,由此可以看出,RenderBox 就是繪制上使用的基類了。繼續觀察一下 RenderObject 的子類繼承樹,發現有 3 個 Mixin 以及 RenderAbstractViewport 和 RenderSliver 沒有繼承自 RenderBox,這些類都是干什么用的呢?
class RenderView extends RenderObject with RenderObjectWithChildMixin<RenderBox> {/// Creates the root of the render tree.////// Typically created by the binding (e.g., [RendererBinding]).////// The [configuration] must not be null.RenderView({RenderBox child,@required ViewConfiguration configuration,@required ui.Window window,}) : assert(configuration != null),_configuration = configuration,_window = window {this.child = child;}
}
這里簡單介紹下:
-
- RenderAbstractViewport 和 RenderSliver 主要處理滑動相關的控件展示,如 ListView 和 ScrollView。DebugOverflowIndicatorMixin 用於在 debug 下提示繪制是否溢出,該類僅用於 debug,自定義控件時一般用不到。
- 剩下的兩個 mixin 還是比較關鍵的:
RenderObjectWithChildMixin 用於為只有 1 個 child 的 RenderObject 提供 child 管理模型。
ContainerRenderObjectMixin 用於為有多個 child 的 RenderObject 提供 child 管理模型。
這兩個 mixin 是非常常用的,看一下 Hierarchy 可以發現基本上每個 RenderBox 都混入了他們,省去了自己管理 child 的代碼。
除此之外還有一個類也有相當多的子類:RenderProxyBox,接下來就分別詳細介紹一下繼承 RenderBox 和 RenderProxyBox 實現自定義控件的正確姿勢。
-
RenderBox
一個看源碼的好習慣就是看到一個新類先看注釋,第一句話如下:
A render object in a 2D Cartesian coordinate system.
這句話可以解釋 Box 的含義了,實際上就是表示使用了 2D 笛卡爾坐標系來標識位置,這與原生開發是一致的,坐標系原點位於左上,x 軸正向指向屏幕右側,y 軸正向指向屏幕下側。
葉節點與父節點
在安卓中,有 View 和 ViewGroup 的區分,前者不能有子 View,即為葉節點,后者可以有多個子 View,即父節點,那么 Flutter 中呢?
答案是都是 RenderBox,child 的邏輯區別以 mixin 來解決,如果想擁有 child,混入上一節所講的 RenderObjectWithChildMixin 或 ContainerRenderObjectMixin 就可以了。
控件的測量與布局
在 RenderBox 中,控件大小的值為 _size 成員,它只包含寬高兩個屬性值,我們可以通過該成員的 set 和 get 方法訪問或修改它的值。在測量時,parent 會傳給當前 RenderBox 一個大小的限制,為 BoxConstraints 類型,通過 constraints 這個 get 方法可以獲取到,最后測量得到的 size 必須滿足這個限制,在 Flutter 的 debug 模式下對 size 是否滿足 constraints 做了 assert 檢查,如果檢查未通過就會布局失敗。所以測量上我們要做的是下面兩點:
- 如果沒有 child,那么根據自身的屬性計算出滿足 constraints 的 size.
- 如果有 child,那么綜合自身的屬性和 child 的測量結果計算出滿足 constraints 的 size.
performResize 和 performLayout
通過查看 size 的注釋,發現測量的時機在 performResize()
和 performLayout()
方法中,問題來了,為什么有兩個測量的方法呢?分析下 RenderObject 類中調用它們的 layout 方法源碼:
if (sizedByParent) { try { performResize(); } catch (e, stack) {} }
try { performLayout(); } catch (e, stack) {}
可以看出只有 sizedByParent 為 true 時,performResize()
才會被調用,而 performLayout()
是每次布局都會被調用的。sizedByParent 意為該控件的大小是否能僅通過 parent 賦予它的 constraints 就可以被確定下來了,即該控件的大小與它自身的屬性和與它的 child 都無關,比如如果一個控件永遠充滿 parent 的大小,那么 sizedByParent 就應該返回 true。
這里還有另外一個限制,如果 sizedByParent 為 true,大小應在 performResize()
中就確認,並且不能在 performLayout()
方法中再修改了,此時 performLayout()
只負責布局 child。
回到 sizedByParent,為什么有這樣一個屬性呢?注釋中發現是為了優化性能,這里分析一下 RenderObject 中用到它的代碼:
if (!parentUsesSize || sizedByParent || constraints.isTight || parent is! RenderObject) { relayoutBoundary = this; } else { final RenderObject parent = this.parent; relayoutBoundary = parent._relayoutBoundary; }
可以看到如果 sizedByParent 為 true,relayoutBoundary 就設置為了自己,否則繼續向 parent 查找。除了 sizedByParent 以外,還有其他幾個判斷項,分別是 !parentUsesSize
(parent 的測量不依賴該 RenderObject 的大小)、constraints.isTight
(parent 賦予的限制是個定值)、parent is! RenderObject
(滿足該條件的只能是根節點 RenderView 了)。
relayoutBoundary
這里引出了另外一個問題,什么是 relayoutBoundary?
首先來講一下如何觸發布局的測量,之前有源碼分析系列有提到過,在每一幀的繪制 drawFrame 方法中,會對標記為 dirty 的 RenderObject 進行重新布局,我們可以通過調用 markNeedsLayout()
方法將 RenderObject 的布局狀態標記為 dirty。分析一下該方法的源碼:
void markNeedsLayout() { if (_needsLayout) { return; } if (_relayoutBoundary != this) { markParentNeedsLayout(); } else { _needsLayout = true; if (owner != null) { owner._nodesNeedingLayout.add(this); owner.requestVisualUpdate(); } } }
如果自身不是 relayoutBoundary,就繼續向 parent 查找,一直向上查找到是 relayoutBoundary 的 RenderObject,再將這個 RenderObject 標記為 dirty 的。這樣來看它的作用就比較明顯了,意思就是當一個控件的大小被改變時可能會影響到它的 parent,因此 parent 也需要被重新布局,那么到什么時候是個頭呢?答案就是 relayoutBoundary,如果一個 RenderObject 是 relayoutBoundary,就表示它的大小變化不會再影響到 parent 的大小了,於是 parent 也就不用重新布局了。
知道這點后可以再重新考慮一下之前設置 relayoutBoundary 的四個判斷條件,這么判斷的原因應該很明確了,這里就不具體講了。
葉節點
葉節點的測量和布局比較簡單,首先根據需求確認 sizedByParent的值,然后通過自身屬性和 constraints 計算出大小后調用 size 的 set 方法直接賦值給 size 就好了。由於是葉節點,是不用處理如何布局的問題的,只要知道自身的大小就足夠了。
父節點
父節點的流程就相對復雜一些,因為除了測量外還要對子節點進行布局,步驟如下:
- 根據 child 的個數選擇 RenderObjectWithChildMixin 或 ContainerRenderObjectMixin.
- 確認 sizedByParent 的值,如果 sizedByParent 為 true,直接在
performResize()
方法中確認自己的大小. - 在
performLayout()
方法中對 child 進行布局.
重點在於第三個步驟,下面進行詳細介紹。
首先要說明的是,與安卓的 onMeasure()
和 onLayout()
不同的是,Flutter 中測量和布局的過程都在 performLayout()
這一個方法中完成。
ParentData
首先要介紹的是一個名為 ParentData 的類,在 Flutter 的布局系統中,該類負責存儲父節點所需要的子節點的布局信息,當然該信息偶爾也會用於子節點的布局。
每個 RenderObject 類中都有 parentData 這樣一個成員,該成員只能通過 setupParentData 方法賦值,RenderObject 的子類可以通過重寫該方法將 ParentData 的子類賦值給 parentdata,以擴展 ParentData 的功能:
void setupParentData(covariant RenderObject child) { if (child.parentData is! ParentData) child.parentData = ParentData(); }
接下來看一下該類的 Hierarchy 結構:

先無視用於滑動的 Sliver 相關的類和用於表格布局的 TabelCellParentData,我們來分析一下剩余的 ParentData類的作用。
ParentData
class ParentData { /// Called when the RenderObject is removed from the tree. @protected @mustCallSuper void detach() { } @override String toString() => '<none>'; }
這是所有 ParentData 的基類,沒有存儲任何信息也沒有實現功能,只定義了一個空實現的 detach()
方法,該方法會在 RenderObject 被移出 tree 的時候調用,這給子類提供了一個在 RenderObject 移出時更新信息的時機。
BoxParentData
/// Parent data used by [RenderBox] and its subclasses. class BoxParentData extends ParentData { /// The offset at which to paint the child in the parent's coordinate system. Offset offset = Offset.zero; @override String toString() => 'offset=$offset'; }
該類注釋寫的很明確,用於 RenderBox 和它的子類,只有一個 offset 屬性,該屬性用於存儲 child 的布局信息,也就是 child 應該被擺在哪個位置,通常在 child 大小確定后,parent 負責根據自身邏輯將 child 的位置賦值到這里。
ContainerBoxParentData
查看源碼后發現該類是個空類,只是為了方便子類混入 ContainerParentDataMixin。
ContainerParentDataMixin
該類使用頻率很高,基本上所有父節點的 ParentData 都混入了該類,該類需要與ContainerRenderObjectMixin 共同使用,主要解決了對 child 的管理,它用雙鏈表存儲了所有子節點並提供了方便的接口去獲取他們。對於開發者,一般來說只用到 ContainerRenderObjectMixin 中的 firstChild
、lastChild
、childCount
,用來獲取首末 child,child的個數,配合使用 ContainerParentDataMixin 中的 previousSibling
、nextSibling
就可以對 child 進行遍歷了。
這些 ParentData 的基類解決了 child 的布局位置信息的存儲和 child 的管理以及引用的獲取,再往下的子類就是與各布局的功能相關的類了,如 FlexParentData,存儲了 flex 和 fit 的值,分別表示該 child 的 flex 比重和 布局的 fit 策略。
測量 child 大小
測量一個 child 需要調用 RenderObject 中的 void layout(Constraints constraints, { bool parentUsesSize = false })
,需要傳入兩個參數,
- constraints 即為父節點對子節點大小的限制,該值根據父節點的布局邏輯確定。調用完這個方法后,就可以通過 child.size 拿到 child 測量后的大小了。
- parentUsesSize,該值用於確定 relayoutBoundary,意為 child 的布局變化是否影響 parent,根據實際情況傳入該值即可,默認為 false。
布局 child
布局 child 即計算出 child 相對 parent 展示的位置,將該位置賦值給 childParentData 的 offset 中就可以了,該 offset 會在后面的繪制過程中用到。
控件的繪制
繪制方法在 void paint(PaintingContext context, Offset offset) { }
中實現,RenderBox 需要在該方法中實現對自身的繪制以及所有 child 的繪制。
繪制自身內容
通過 context.canvas
獲取到 Canvas 對象,之后就可以開始繪制了,需要注意每次繪制都要帶上 offset 的偏移量,否則繪制的位置會與布局階段的預期不同。
繪制 child
對於 child 可以遍歷所有 child 並調用 context.paintChild(child, childParentData.offset + offset)
方法完成 child 的繪制。除了這種方法以外,Flutter 還提供了 RenderBoxContainerDefaultsMixin,該類提供了一些 RenderBox 默認的行為方法,如上面繪制 child 的流程調用該類中的 defaultPaint(PaintingContext context, Offset offset)
就可以了,可以簡化一些模板代碼。
repaintBoundary
與 relayoutBoundary 相對應,對於繪制,也有一個 isRepaintBoundary 屬性,與 relayoutBoundary 不同的是,這個屬性需要由我們自己設置,默認為 false。注釋中的第一句話表示了該屬性的含義:
Whether this render object repaints separately from its parent.
即該 RenderObject 的繪制是否與它的 parent 相獨立,如何做到獨立呢?看下 paintChild 方法的源碼:
void paintChild(RenderObject child, Offset offset) { if (child.isRepaintBoundary) { stopRecordingIfNeeded(); _compositeChild(child, offset); } else { child._paintWithContext(this, offset); } } void _compositeChild(RenderObject child, Offset offset) { // Create a layer for our child, and paint the child into it. if (child._needsPaint) { repaintCompositedChild(child, debugAlsoPaintedParent: true); } else { // 省略assert邏輯 } child._layer.offset = offset; appendLayer(child._layer); }
可以看出在繪制 child 時,如果 isRepaintBoundary 為 true,那么會為該 child 新創建一個 layer,只有在不同 layer 的 RenderObject 才可以各自獨立進行繪制。該屬性很明顯是為了提高渲染效率而存在的,它能夠實現區域重繪功能,具體原理如下:
類似觸發布局的方法,為了觸發繪制,需要調用 markNeedsPaint()
,分析下該方法的源碼:
void markNeedsPaint() { if (_needsPaint) return; _needsPaint = true; if (isRepaintBoundary) { if (owner != null) { owner._nodesNeedingPaint.add(this); owner.requestVisualUpdate(); } } else if (parent is RenderObject) { final RenderObject parent = this.parent; parent.markNeedsPaint(); } else { if (owner != null) owner.requestVisualUpdate(); } }
可以看出當調用 markNeedsPaint()
方法時,會從當前 RenderObject 開始一直向父節點查找,直到 isRepaintBoundary 為 true 時,才標記當前 RenderObject 為需要繪制的,也由此實現了區域重繪。當 RenderObject 繪制的很頻繁時,可以指定該值為 true,這樣在每幀繪制時可以縮小重繪范圍,僅重繪自身而不用重繪它的 parent,以此來提高性能。
對繪制區域的限制
控件的點擊事件處理
根據上述流程完成布局與繪制后,我們理所應當的可能利用 GestureDetector 監聽了一些手勢,但是運行起來后發現手勢完全沒有生效,這是因為我們漏掉了關於點擊事件處理相關方法的實現。在 RenderBox 中有三個方法與點擊事件相關:
bool hitTest(HitTestResult result, { @required Offset position }) { if (_size.contains(position)) { if (hitTestChildren(result, position: position) || hitTestSelf(position)) { result.add(BoxHitTestEntry(this, position)); return true; } } return false; } @protected bool hitTestSelf(Offset position) => false; @protected bool hitTestChildren(HitTestResult result, { Offset position }) => false;
hitTest
方法用來判斷該 RenderObject 是否在被點擊的范圍內,同時負責將被點擊的 RenderObject 添加到 HitTestResult 列表中,參數 position 為點擊坐標,返回 true 則表示有 RenderObject 被點擊了,反之沒有。在默認實現中,簡單的判斷了 position 是否在 size 范圍內,如果在自身范圍內的話,繼續判斷是否有 child 在點擊范圍內,若沒有 child 被點擊,再判斷自己是否被點擊了。一般在子類中實現 hitTestSelf
和 hitTestChildren
即可。在 RenderBoxContainerDefaultsMixin 中有 hitTestChildren 的默認實現,即根據 child 的 hitTest 方法來判斷是否被點擊,如果沒有特殊邏輯,直接使用該方法即可。
RenderProxyBox
除了 RenderBox 之外,還有一個類比較常用,那就是 RenderProxyBox,該類將布局繪制點擊事件等方法的處理全部交由 child 來實現,可以理解為 child 的代理,具體代理了哪些方法可以參見 RenderProxyBoxMixin 的源碼。
通常對一個已有的 RenderObject 做一些附加處理時會用到該類,如常見的 Opacity、DecoratedBox 等控件就是用該類實現的,它的各屬性和 child 完全一致,因此我們專心處理對 child 的額外效果就可以了,避免了邏輯的拷貝。
-
RenderBox 子類的常規寫法
回顧一下之前所講的內容,本節總結一下 RenderBox 子類的常規寫法。
命名
RenderBox子類的名稱一般以Render開頭。
mixin
根據 child 的數量選擇混入 RenderObjectWithChildMixin 或 ContainerRenderObjectMixin,前者對應一個 child,后者對應多個 child。
成員變量
RenderObject 的成員一般聲明為 private,配以 set 和 get 方法,get 方法直接返回該成員即可,用來在類中獲取該屬性,set 方法一般先判斷值是否與原值相同,若不同的話根據需要調用 markNeedsLayout
或 markNeedsPaint
。
示例:
Axis get direction => _direction; Axis _direction; set direction(Axis value) { if (_direction != value) { _direction = value; markNeedsLayout(); } }
布局、繪制、點擊事件
確定 sizedByParent 的值,若該值為 true,則還需要實現 performResize()
,然后在該方法中計算出 size,后續 performLayout()
的過程中不能再對 size 進行改動。
對 child 的布局在 performLayout()
中實現,布局后將 child 的 offset 放入 ParentData 中,注意調用 paintChild 時傳入正確的 parentUsesSize 屬性以優化性能。如果需要擴展 ParentData,那么重寫 setupParentData 方法,ParentData 一般選擇繼承 ContainerBoxParentData。
在 paint 方法中實現自身與 child 的繪制,如果自身會頻繁繪制,記得重寫 isRepaintBoundary 的值為 true。
根據需要實現hitTestSelf
和 hitTestChildren
。
繪制 child 和處理 child 點擊事件的默認邏輯在 RenderBoxContainerDefaultsMixin 中。
三,對應 Widget 的常規寫法
RenderObject 最終也需要對應到 Widget,除了熟知的 StatelessWidget 和 StatefulWidget 以外,直接對應到 RenderObject 的是 RenderObjectWidget,它有三個實現類:
- SingleChildRenderObjectWidget,對應有一個 child 的 RenderObject.
- MultiChildRenderObjectWidget,對應有多個 child 的 RenderObject.
- LeafRenderObjectWidget 對應葉節點的 RenderObject.
繼承所需的類后,需要實現 createRenderObject 和 updateRenderObject 兩個方法,前者用於創建新的 Object 實例,后者用於更新 RenderObject 的屬性,示例如下:
/// 連續點贊Widget,對應連續點贊一幀的信息描述 class _RawMultiLike extends SingleChildRenderObjectWidget { final List<List<_SplashImage>> splashImages; final _DescriptionInfo descriptionInfo; final Size screenSize; const _RawMultiLike({ Widget child, this.splashImages, this.descriptionInfo, this.screenSize, }): super(child: child); @override _RenderMultiLike createRenderObject(BuildContext context) { return _RenderMultiLike( splashImageInfos: splashImages, descriptionInfo: descriptionInfo, screenSize: screenSize, configuration: createLocalImageConfiguration(context), ); } @override void updateRenderObject(BuildContext context, _RenderMultiLike renderObject) { renderObject ..splashImageInfos = splashImages ..descriptionInfo = descriptionInfo ..screenSize = screenSize ..configuration = createLocalImageConfiguration(context); } }
Element 層在 Widget 基類已經處理了,一般不用我們關心了。
四,一些自定義控件相關的 Widget
Flutter 原生提供了一些方便自定義功能的 Widget,如果可以滿足需求的話,直接使用這些 Widget 是最方便的,下面列舉一下:
- 自定義畫布:CustomPaint
- 自定義單 child 布局:CustomSingleChildLayout
- 自定義多 child 布局:CustomMultiChildLayout
- 動態指定 RepaintBoundary:RepaintBoundary