您好,登錄后才能下訂單哦!
小編給大家分享一下Flutter之自定義控件RenderBox怎么用,相信大部分人都還不怎么了解,因此分享這篇文章給大家參考一下,希望大家閱讀完這篇文章后大有收獲,下面讓我們一起去了解一下吧!
首先,介紹一下 RenderObject 子類的繼承關系,通過 Android Studio 的 Hierarchy 功能可以直觀地對類繼承關系進行查看:
RenderObject 類繼承關系
看過源碼分析系列相關文章中對 runApp() 方法的解析后應該知道,RenderView 對應的是 RenderObject 樹的根節(jié)點,打開該類的注釋,發(fā)現(xiàn)有這樣一句話:
The view has a unique child [RenderBox], which is required to fill the entire output surface.
意為 RenderView 根節(jié)點下只有唯一一個 RenderBox 作為葉節(jié)點,它的大小會充滿整個繪制表面,由此可以看出,RenderBox 就是繪制上使用的基類了。繼續(xù)觀察一下 RenderObject 的子類繼承樹,發(fā)現(xiàn)有 3 個 Mixin 以及 RenderAbstractViewport 和 RenderSliver 沒有繼承自 RenderBox,這些類都是干什么用的呢?這里簡單介紹下:
RenderAbstractViewport 和 RenderSliver 主要處理滑動相關的控件展示,如 ListView 和 ScrollView。滑動相關的內(nèi)容就不在本文中講了,大家可以期待后續(xù)的文章。DebugOverflowIndicatorMixin 用于在 debug 下提示繪制是否溢出,該類僅用于 debug,自定義控件時一般用不到。
剩下的兩個 mixin 還是比較關鍵的:
RenderObjectWithChildMixin 用于為只有 1 個 child 的 RenderObject 提供 child 管理模型。
ContainerRenderObjectMixin 用于為有多個 child 的 RenderObject 提供 child 管理模型。
這兩個 mixin 是非常常用的,看一下 Hierarchy 可以發(fā)現(xiàn)基本上每個 RenderBox 都混入了他們,省去了自己管理 child 的代碼。
除此之外還有一個類也有相當多的子類:RenderProxyBox,接下來就分別詳細介紹一下繼承 RenderBox 和 RenderProxyBox 實現(xiàn)自定義控件的正確姿勢。
一個看源碼的好習慣就是看到一個新類先看注釋,第一句話如下:
A render object in a 2D Cartesian coordinate system.
這句話可以解釋 Box 的含義了,實際上就是表示使用了 2D 笛卡爾坐標系來標識位置,這與原生開發(fā)是一致的,坐標系原點位于左上,x 軸正向指向屏幕右側,y 軸正向指向屏幕下側。
在安卓中,有 View 和 ViewGroup 的區(qū)分,前者不能有子 View,即為葉節(jié)點,后者可以有多個子 View,即父節(jié)點,那么 Flutter 中呢?答案是都是 RenderBox,child 的邏輯區(qū)別以 mixin 來解決,如果想擁有 child,混入上一節(jié)所講的 RenderObjectWithChildMixin 或 ContainerRenderObjectMixin 就可以了。
在 RenderBox 中,控件大小的值為 _size 成員,它只包含寬高兩個屬性值,我們可以通過該成員的 set 和 get 方法訪問或修改它的值。在測量時,parent 會傳給當前 RenderBox 一個大小的限制,為 BoxConstraints 類型,通過 constraints 這個 get 方法可以獲取到,最后測量得到的 size 必須滿足這個限制,在 Flutter 的 debug 模式下對 size 是否滿足 constraints 做了 assert 檢查,如果檢查未通過就會布局失敗。所以測量上我們要做的是下面兩點:
如果沒有 child,那么根據(jù)自身的屬性計算出滿足 constraints 的 size.
如果有 child,那么綜合自身的屬性和 child 的測量結果計算出滿足 constraints 的 size.
通過查看 size 的注釋,發(fā)現(xiàn)測量的時機在 performResize() 和 performLayout() 方法中,問題來了,為什么有兩個測量的方法呢?分析下 RenderObject 類中調(diào)用它們的 layout 方法源碼:
if (sizedByParent) { try { performResize(); } catch (e, stack) {} } try { performLayout(); } catch (e, stack) {}
可以看出只有 sizedByParent 為 true 時,performResize() 才會被調(diào)用,而 performLayout() 是每次布局都會被調(diào)用的。
sizedByParent 意為該控件的大小是否能僅通過 parent 賦予它的 constraints 就可以被確定下來了,即該控件的大小與它自身的屬性和與它的 child 都無關,比如如果一個控件永遠充滿 parent 的大小,那么 sizedByParent 就應該返回 true。
這里還有另外一個限制,如果 sizedByParent 為 true,大小應在 performResize() 中就確認,并且不能在 performLayout() 方法中再修改了,此時 performLayout() 只負責布局 child。
回到 sizedByParent,為什么有這樣一個屬性呢?注釋中發(fā)現(xiàn)是為了優(yōu)化性能,這里分析一下 RenderObject 中用到它的代碼:
if (!parentUsesSize || sizedByParent || constraints.isTight || parent is! RenderObject) { relayoutBoundary = this; } else { final RenderObject parent = this.parent; relayoutBoundary = parent._relayoutBoundary; }
可以看到如果 sizedByParent 為 true,relayoutBoundary 就設置為了自己,否則繼續(xù)向 parent 查找。除了 sizedByParent 以外,還有其他幾個判斷項,分別是 !parentUsesSize(parent 的測量不依賴該 RenderObject 的大小)、constraints.isTight(parent 賦予的限制是個定值)、parent is! RenderObject(滿足該條件的只能是根節(jié)點 RenderView 了)。
這里引出了另外一個問題,什么是 relayoutBoundary?
首先來講一下如何觸發(fā)布局的測量,之前有源碼分析系列有提到過,在每一幀的繪制 drawFrame 方法中,會對標記為 dirty 的 RenderObject 進行重新布局,我們可以通過調(diào)用 markNeedsLayout() 方法將 RenderObject 的布局狀態(tài)標記為 dirty。分析一下該方法的源碼:
void markNeedsLayout() { if (_needsLayout) { return; } if (_relayoutBoundary != this) { markParentNeedsLayout(); } else { _needsLayout = true; if (owner != null) { owner._nodesNeedingLayout.add(this); owner.requestVisualUpdate(); } } }
如果自身不是 relayoutBoundary,就繼續(xù)向 parent 查找,一直向上查找到是 relayoutBoundary 的 RenderObject,再將這個 RenderObject 標記為 dirty 的。這樣來看它的作用就比較明顯了,意思就是當一個控件的大小被改變時可能會影響到它的 parent,因此 parent 也需要被重新布局,那么到什么時候是個頭呢?答案就是 relayoutBoundary,如果一個 RenderObject 是 relayoutBoundary,就表示它的大小變化不會再影響到 parent 的大小了,于是 parent 也就不用重新布局了。知道這點后可以再重新考慮一下之前設置 relayoutBoundary 的四個判斷條件,這么判斷的原因應該很明確了,這里就不具體講了。
葉節(jié)點的測量和布局比較簡單,首先根據(jù)需求確認 sizedByParent的值,然后通過自身屬性和 constraints 計算出大小后調(diào)用 size 的 set 方法直接賦值給 size 就好了。由于是葉節(jié)點,是不用處理如何布局的問題的,只要知道自身的大小就足夠了。
父節(jié)點的流程就相對復雜一些,因為除了測量外還要對子節(jié)點進行布局,步驟如下:
根據(jù) child 的個數(shù)選擇 RenderObjectWithChildMixin 或 ContainerRenderObjectMixin.
確認 sizedByParent 的值,如果 sizedByParent 為 true,直接在 performResize() 方法中確認自己的大小.
在 performLayout() 方法中對 child 進行布局.
重點在于第三個步驟,下面進行詳細介紹。
首先要說明的是,與安卓的 onMeasure() 和 onLayout() 不同的是,F(xiàn)lutter 中測量和布局的過程都在 performLayout() 這一個方法中完成。
首先要介紹的是一個名為 ParentData 的類,在 Flutter 的布局系統(tǒng)中,該類負責存儲父節(jié)點所需要的子節(jié)點的布局信息,當然該信息偶爾也會用于子節(jié)點的布局。
每個 RenderObject 類中都有 parentData 這樣一個成員,該成員只能通過 setupParentData 方法賦值,RenderObject 的子類可以通過重寫該方法將 ParentData 的子類賦值給 parentdata,以擴展 ParentData 的功能:
void setupParentData(covariant RenderObject child) { if (child.parentData is! ParentData) child.parentData = ParentData(); }
接下來看一下該類的 Hierarchy 結構:
ParentData 類繼承結構
先無視用于滑動的 Sliver 相關的類和用于表格布局的 TabelCellParentData,我們來分析一下剩余的 ParentData類的作用。
class ParentData { /// Called when the RenderObject is removed from the tree. @protected @mustCallSuper void detach() { } @override String toString() => '<none>'; }
這是所有 ParentData 的基類,沒有存儲任何信息也沒有實現(xiàn)功能,只定義了一個空實現(xiàn)的 detach() 方法,該方法會在 RenderObject 被移出 tree 的時候調(diào)用,這給子類提供了一個在 RenderObject 移出時更新信息的時機。
/// 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 負責根據(jù)自身邏輯將 child 的位置賦值到這里。
查看源碼后發(fā)現(xiàn)該類是個空類,只是為了方便子類混入 ContainerParentDataMixin。
該類使用頻率很高,基本上所有父節(jié)點的 ParentData 都混入了該類,該類需要與ContainerRenderObjectMixin 共同使用,主要解決了對 child 的管理,它用雙鏈表存儲了所有子節(jié)點并提供了方便的接口去獲取他們。對于開發(fā)者,一般來說只用到 ContainerRenderObjectMixin 中的 firstChild、lastChild、childCount,用來獲取首末 child,child的個數(shù),配合使用 ContainerParentDataMixin 中的 previousSibling、nextSibling就可以對 child 進行遍歷了。
這些 ParentData 的基類解決了 child 的布局位置信息的存儲和 child 的管理以及引用的獲取,再往下的子類就是與各布局的功能相關的類了,如 FlexParentData,存儲了 flex 和 fit 的值,分別表示該 child 的 flex 比重和 布局的 fit 策略。
測量一個 child 需要調(diào)用 RenderObject 中的 void layout(Constraints constraints, { bool parentUsesSize = false }),需要傳入兩個參數(shù),constraints 即為父節(jié)點對子節(jié)點大小的限制,該值根據(jù)父節(jié)點的布局邏輯確定。調(diào)用完這個方法后,就可以通過 child.size 拿到 child 測量后的大小了。另外一個參數(shù)是 parentUsesSize,該值用于確定 relayoutBoundary,意為 child 的布局變化是否影響 parent,根據(jù)實際情況傳入該值即可,默認為 false。
布局 child 即計算出 child 相對 parent 展示的位置,將該位置賦值給 childParentData 的 offset 中就可以了,該 offset 會在后面的繪制過程中用到。
繪制方法在 void paint(PaintingContext context, Offset offset) { } 中實現(xiàn),RenderBox 需要在該方法中實現(xiàn)對自身的繪制以及所有 child 的繪制。
通過 context.canvas 獲取到 Canvas 對象,之后就可以開始繪制了,需要注意每次繪制都要帶上 offset 的偏移量,否則繪制的位置會與布局階段的預期不同。
對于 child 可以遍歷所有 child 并調(diào)用 context.paintChild(child, childParentData.offset + offset)方法完成 child 的繪制。除了這種方法以外,F(xiàn)lutter 還提供了 RenderBoxContainerDefaultsMixin,該類提供了一些 RenderBox 默認的行為方法,如上面繪制 child 的流程調(diào)用該類中的 defaultPaint(PaintingContext context, Offset offset) 就可以了,可以簡化一些模板代碼。
與 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 新創(chuàng)建一個 layer,只有在不同 layer 的 RenderObject 才可以各自獨立進行繪制。該屬性很明顯是為了提高渲染效率而存在的,它能夠?qū)崿F(xiàn)區(qū)域重繪功能,具體原理如下:
類似觸發(fā)布局的方法,為了觸發(fā)繪制,需要調(diào)用 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(); } }
可以看出當調(diào)用 markNeedsPaint() 方法時,會從當前 RenderObject 開始一直向父節(jié)點查找,直到 isRepaintBoundary 為 true 時,才標記當前 RenderObject 為需要繪制的,也由此實現(xiàn)了區(qū)域重繪。當 RenderObject 繪制的很頻繁時,可以指定該值為 true,這樣在每幀繪制時可以縮小重繪范圍,僅重繪自身而不用重繪它的 parent,以此來提高性能。
對繪制區(qū)域的限制
根據(jù)上述流程完成布局與繪制后,我們理所應當?shù)目赡芾?GestureDetector 監(jiān)聽了一些手勢,但是運行起來后發(fā)現(xiàn)手勢完全沒有生效,這是因為我們漏掉了關于點擊事件處理相關方法的實現(xiàn)。在 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 是否在被點擊的范圍內(nèi),同時負責將被點擊的 RenderObject 添加到 HitTestResult 列表中,參數(shù) position 為點擊坐標,返回 true 則表示有 RenderObject 被點擊了,反之沒有。在默認實現(xiàn)中,簡單的判斷了 position 是否在 size 范圍內(nèi),如果在自身范圍內(nèi)的話,繼續(xù)判斷是否有 child 在點擊范圍內(nèi),若沒有 child 被點擊,再判斷自己是否被點擊了。一般在子類中實現(xiàn) hitTestSelf 和 hitTestChildren 即可。在 RenderBoxContainerDefaultsMixin 中有 hitTestChildren 的默認實現(xiàn),即根據(jù) child 的 hitTest 方法來判斷是否被點擊,如果沒有特殊邏輯,直接使用該方法即可。
除了 RenderBox 之外,還有一個類比較常用,那就是 RenderProxyBox,該類將布局繪制點擊事件等方法的處理全部交由 child 來實現(xiàn),可以理解為 child 的代理,具體代理了哪些方法可以參見 RenderProxyBoxMixin 的源碼。
通常對一個已有的 RenderObject 做一些附加處理時會用到該類,如常見的 Opacity、DecoratedBox 等控件就是用該類實現(xiàn)的,它的各屬性和 child 完全一致,因此我們專心處理對 child 的額外效果就可以了,避免了邏輯的拷貝。
回顧一下之前所講的內(nèi)容,本節(jié)總結一下 RenderBox 子類的常規(guī)寫法。
RenderBox子類的名稱一般以Render開頭。
根據(jù) child 的數(shù)量選擇混入 RenderObjectWithChildMixin 或 ContainerRenderObjectMixin,前者對應一個 child,后者對應多個 child。
RenderObject 的成員一般聲明為 private,配以 set 和 get 方法,get 方法直接返回該成員即可,用來在類中獲取該屬性,set 方法一般先判斷值是否與原值相同,若不同的話根據(jù)需要調(diào)用 markNeedsLayout 或 markNeedsPaint。
示例:
Axis get direction => _direction; Axis _direction; set direction(Axis value) { if (_direction != value) { _direction = value; markNeedsLayout(); } }
確定 sizedByParent 的值,若該值為 true,則還需要實現(xiàn) performResize(),然后在該方法中計算出 size,后續(xù) performLayout() 的過程中不能再對 size 進行改動。
對 child 的布局在 performLayout() 中實現(xiàn),布局后將 child 的 offset 放入 ParentData 中,注意調(diào)用 paintChild 時傳入正確的 parentUsesSize 屬性以優(yōu)化性能。如果需要擴展 ParentData,那么重寫 setupParentData 方法,ParentData 一般選擇繼承 ContainerBoxParentData。
在 paint 方法中實現(xiàn)自身與 child 的繪制,如果自身會頻繁繪制,記得重寫 isRepaintBoundary 的值為 true。
根據(jù)需要實現(xiàn)hitTestSelf 和 hitTestChildren。
繪制 child 和處理 child 點擊事件的默認邏輯在 RenderBoxContainerDefaultsMixin 中。
RenderObject 最終也需要對應到 Widget,除了熟知的 StatelessWidget 和 StatefulWidget 以外,直接對應到 RenderObject 的是 RenderObjectWidget,它有三個實現(xiàn)類:
SingleChildRenderObjectWidget,對應有一個 child 的 RenderObject.
MultiChildRenderObjectWidget,對應有多個 child 的 RenderObject.
LeafRenderObjectWidget 對應葉節(jié)點的 RenderObject.
繼承所需的類后,需要實現(xiàn) createRenderObject 和 updateRenderObject 兩個方法,前者用于創(chuàng)建新的 Object 實例,后者用于更新 RenderObject 的屬性,示例如下:
/// 連續(xù)點贊Widget,對應連續(xù)點贊一幀的信息描述 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 基類已經(jīng)處理了,一般不用我們關心了。
Flutter 原生提供了一些方便自定義功能的 Widget,如果可以滿足需求的話,直接使用這些 Widget 是最方便的,下面列舉一下:
自定義畫布:CustomPaint
自定義單 child 布局:CustomSingleChildLayout
自定義多 child 布局:CustomMultiChildLayout
動態(tài)指定 RepaintBoundary:RepaintBoundary
以上是“Flutter之自定義控件RenderBox怎么用”這篇文章的所有內(nèi)容,感謝各位的閱讀!相信大家都有了一定的了解,希望分享的內(nèi)容對大家有所幫助,如果還想學習更多知識,歡迎關注億速云行業(yè)資訊頻道!
免責聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉載和分享為主,文章觀點不代表本網(wǎng)站立場,如果涉及侵權請聯(lián)系站長郵箱:is@yisu.com進行舉報,并提供相關證據(jù),一經(jīng)查實,將立刻刪除涉嫌侵權內(nèi)容。