溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點擊 登錄注冊 即表示同意《億速云用戶服務條款》

Flutter之自定義控件RenderBox怎么用

發(fā)布時間:2021-08-23 13:39:25 來源:億速云 閱讀:140 作者:小新 欄目:開發(fā)技術

小編給大家分享一下Flutter之自定義控件RenderBox怎么用,相信大部分人都還不怎么了解,因此分享這篇文章給大家參考一下,希望大家閱讀完這篇文章后大有收獲,下面讓我們一起去了解一下吧!

    RenderObject 類繼承層級解析

    首先,介紹一下 RenderObject 子類的繼承關系,通過 Android Studio 的 Hierarchy 功能可以直觀地對類繼承關系進行查看:

    Flutter之自定義控件RenderBox怎么用

    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)自定義控件的正確姿勢。

    RenderBox

    一個看源碼的好習慣就是看到一個新類先看注釋,第一句話如下:

    A render object in a 2D Cartesian coordinate system.

    這句話可以解釋 Box 的含義了,實際上就是表示使用了 2D 笛卡爾坐標系來標識位置,這與原生開發(fā)是一致的,坐標系原點位于左上,x 軸正向指向屏幕右側,y 軸正向指向屏幕下側。

    葉節(jié)點與父節(jié)點

    在安卓中,有 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 檢查,如果檢查未通過就會布局失敗。所以測量上我們要做的是下面兩點:

    1. 如果沒有 child,那么根據(jù)自身的屬性計算出滿足 constraints 的 size.

    2. 如果有 child,那么綜合自身的屬性和 child 的測量結果計算出滿足 constraints 的 size.

    performResize 和 performLayout

    通過查看 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

    這里引出了另外一個問題,什么是 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é)點

    葉節(jié)點的測量和布局比較簡單,首先根據(jù)需求確認 sizedByParent的值,然后通過自身屬性和 constraints 計算出大小后調(diào)用 size 的 set 方法直接賦值給 size 就好了。由于是葉節(jié)點,是不用處理如何布局的問題的,只要知道自身的大小就足夠了。

    父節(jié)點

    父節(jié)點的流程就相對復雜一些,因為除了測量外還要對子節(jié)點進行布局,步驟如下:

    1. 根據(jù) child 的個數(shù)選擇 RenderObjectWithChildMixin 或 ContainerRenderObjectMixin.

    2. 確認 sizedByParent 的值,如果 sizedByParent 為 true,直接在 performResize() 方法中確認自己的大小.

    3. 在 performLayout() 方法中對 child 進行布局.

    重點在于第三個步驟,下面進行詳細介紹。

    首先要說明的是,與安卓的 onMeasure() 和 onLayout() 不同的是,F(xiàn)lutter 中測量和布局的過程都在 performLayout() 這一個方法中完成。

    ParentData

    首先要介紹的是一個名為 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 結構:

    Flutter之自定義控件RenderBox怎么用

    ParentData 類繼承結構

    先無視用于滑動的 Sliver 相關的類和用于表格布局的 TabelCellParentData,我們來分析一下剩余的 ParentData類的作用。

    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 移出時更新信息的時機。

    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 負責根據(jù)自身邏輯將 child 的位置賦值到這里。

    ContainerBoxParentData

    查看源碼后發(fā)現(xiàn)該類是個空類,只是為了方便子類混入 ContainerParentDataMixin。

    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 大小

    測量一個 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 即計算出 child 相對 parent 展示的位置,將該位置賦值給 childParentData 的 offset 中就可以了,該 offset 會在后面的繪制過程中用到。

    控件的繪制

    繪制方法在 void paint(PaintingContext context, Offset offset) { } 中實現(xiàn),RenderBox 需要在該方法中實現(xiàn)對自身的繪制以及所有 child 的繪制。

    繪制自身內(nèi)容

    通過 context.canvas 獲取到 Canvas 對象,之后就可以開始繪制了,需要注意每次繪制都要帶上 offset 的偏移量,否則繪制的位置會與布局階段的預期不同。

    繪制 child

    對于 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) 就可以了,可以簡化一些模板代碼。

    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 新創(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 方法來判斷是否被點擊,如果沒有特殊邏輯,直接使用該方法即可。

    RenderProxyBox

    除了 RenderBox 之外,還有一個類比較常用,那就是 RenderProxyBox,該類將布局繪制點擊事件等方法的處理全部交由 child 來實現(xiàn),可以理解為 child 的代理,具體代理了哪些方法可以參見 RenderProxyBoxMixin 的源碼。

    通常對一個已有的 RenderObject 做一些附加處理時會用到該類,如常見的 Opacity、DecoratedBox 等控件就是用該類實現(xiàn)的,它的各屬性和 child 完全一致,因此我們專心處理對 child 的額外效果就可以了,避免了邏輯的拷貝。

    RenderBox 子類的常規(guī)寫法

    回顧一下之前所講的內(nèi)容,本節(jié)總結一下 RenderBox 子類的常規(guī)寫法。

    命名

    RenderBox子類的名稱一般以Render開頭。

    mixin

    根據(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 中。

    對應 Widget 的常規(guī)寫法

    RenderObject 最終也需要對應到 Widget,除了熟知的 StatelessWidget 和 StatefulWidget 以外,直接對應到 RenderObject 的是 RenderObjectWidget,它有三個實現(xiàn)類:

    1. SingleChildRenderObjectWidget,對應有一個 child 的 RenderObject.

    2. MultiChildRenderObjectWidget,對應有多個 child 的 RenderObject.

    3. 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)處理了,一般不用我們關心了。

    一些自定義控件相關的 Widget

    Flutter 原生提供了一些方便自定義功能的 Widget,如果可以滿足需求的話,直接使用這些 Widget 是最方便的,下面列舉一下:

    自定義畫布:CustomPaint

    自定義單 child 布局:CustomSingleChildLayout

    自定義多 child 布局:CustomMultiChildLayout

    動態(tài)指定 RepaintBoundary:RepaintBoundary

    以上是“Flutter之自定義控件RenderBox怎么用”這篇文章的所有內(nèi)容,感謝各位的閱讀!相信大家都有了一定的了解,希望分享的內(nèi)容對大家有所幫助,如果還想學習更多知識,歡迎關注億速云行業(yè)資訊頻道!

    向AI問一下細節(jié)

    免責聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉載和分享為主,文章觀點不代表本網(wǎng)站立場,如果涉及侵權請聯(lián)系站長郵箱:is@yisu.com進行舉報,并提供相關證據(jù),一經(jīng)查實,將立刻刪除涉嫌侵權內(nèi)容。

    AI