Flutter 中的圖文混排與原理解析

戀貓de小郭發表於2020-03-13

在移動開發中圖文混排是十分常見的業務需求,如下圖效果所示,本篇將介紹在 Flutter 中的圖文混排效果與實現原理。

Flutter 中的圖文混排與原理解析

事實上,針對如上所示的圖文混排需求,Flutter 官方提供了十分便捷的實現方式: WidgetSpan

如下程式碼所示,通過 Text.rich 接入 TextSpanWidgetSpan 就可以快速實現圖文混排的需求,並且可以看出 WidgetSpan 不止支援圖片控制元件,它可以接入任何你需要的 Widget ,比如 CardInkWell 等等。

Text.rich(TextSpan(
      children: <InlineSpan>[
        TextSpan(text: 'Flutter is'),
        WidgetSpan(
            child: SizedBox(
          width: 120,
          height: 50,
          child: Card(
              color: Colors.blue,
              child: Center(child: Text('Hello World!'))),
        )),
        WidgetSpan(
            child: SizedBox(
          width: size > 0 ? size : 0,
          height: size > 0 ? size : 0,
          child: new Image.asset(
            "static/gsy_cat.png",
            fit: BoxFit.cover,
          ),
        )),
        TextSpan(text: 'the best!'),
      ],
    )
複製程式碼

也就是說 WidgetSpan 支援在文字中插入任意控制元件,這大大提升了 Flutter 中富文字的自定義效果,比如上述演示效果中隨意改變圖片的大小。

那為什麼 WidgetSpan 可以如何方便地實現文字和 Widget 混合效果呢?這就要從 Text 的實現說起

實現原理

我們常用的 Text 控制元件其實只是 RichText 的封裝,而 RichText 的實現如下圖所示,主要可以分為三部分:MultiChildRenderObjectWidgetMultiChildRenderObjectElementRenderParagraph

Flutter 中的圖文混排與原理解析

正如我們知道的, Flutter 控制元件一般是由 WidgetElementRenderObeject 三部分組成,而在 RichText 中也是如此,其中:

  • RenderParagraph 主要是負責文字繪製、佈局相關;
  • RichText 繼承 MultiChildRenderObjectWidget 主要是需要通過 MultiChildRenderObjectElement 來處理 WidgetSpan 中 children 控制元件的插入和管理。

WidgetSpan 究竟是如何混入在文字繪製中呢?

在前面的使用中,我們首先是傳入了一個 TextSpanRichText ,並在 TextSpanchildren 中拼接我們需要的內容,那就從 RichText 開始挖掘其中的原理。

Flutter 中的圖文混排與原理解析

如上程式碼所示,這裡我們首先看 RichText 的入口,可以看到 RichText 開始是有一個 _extractChildren 方法,這個方法主要是將傳入 TextSpanchildren 裡,所有的 WidgetSpan 通過 visitChildren 方法給遞迴篩選出來,然後傳入給父類 MultiChildRenderObjectWidget

為什麼需要這麼做?在 《十六、詳解自定義佈局實戰》 中介紹過,MultiChildRenderObjectWidget 的 children 最終會通過 MultiChildRenderObjectElement 作為橋樑,然後被插入到需要管理和繪製的 child 連結串列結構中,這樣在 RenderObject 中方便管理和訪問。

另外我們知道 RichText 傳入的 text 其實是一個 InlineSpan ,而 TextSpan 就是 InlineSpan 的子類,WidgetSpan 也是 InlineSpan 的子類實現,它們的關係如下圖所示:

Flutter 中的圖文混排與原理解析

對於 InlineSpan 系列我們主要關注兩個方法:visitChildrenbuild 方法,它的子類 TextSpanWidgetSpan 都對這兩個方法有自己對應的實現。

  void build(ui.ParagraphBuilder builder, { double textScaleFactor = 1.0, List<PlaceholderDimensions> dimensions });

  bool visitChildren(InlineSpanVisitor visitor);
複製程式碼

Flutter 中的圖文混排與原理解析

接著看 RenderParagraph ,如上程式碼所示,RichText 中的 textInlineSpan) 會繼續被傳入到 RenderParagraph 中,RenderParagraph 繼承了 RenderBox 並混入的 ContainerRenderObjectMixinRenderBoxContainerDefaultsMixin 等。

混入的物件這部分在內容在 《十六、詳解自定義佈局實戰》 也介紹過,這裡只需要知道通過混入它們, RenderParagraph 就可以獲得前面通過 WidgetSpan 傳入到 MultiChildRenderObjectElement 的 children 連結串列,並且佈局計算大小等。

Flutter 中的圖文混排與原理解析

之後 RenderParagraph 中的 text 之後會被放置到 TextPainter 中使用,並且通過 _extractPlaceholderSpans 方法將所有的 PlaceholderSpans 篩選出來。

TextPainter 主要用於實現文字的繪製,這裡我們暫時不多分析,_extractPlaceholderSpans 挑選出來的所有 PlaceholderSpans ,其實就是 WidgetSpan

WidgetSpan 是通過繼承 PlaceholderSpans 從而實現了 InlineSpan,而目前暫時 PlaceholderSpans 實現的類只有 WidgetSpan

Flutter 中的圖文混排與原理解析

挑選出來的 List<PlaceholderSpan> 們會在 RenderParagraph 計算寬高等方法中被用到,比如 computeMaxIntrinsicWidth 方法等,其中主要有 _canComputeIntrinsics_computeChildrenWidthWithMaxIntrinsics_layoutText 三個關鍵方法,這三個方法結合處理了 RenderParagraph 中 Span 的尺寸和佈局等。

Flutter 中的圖文混排與原理解析

  • _canComputeIntrinsics_canComputeIntrinsics 主要判斷了 PlaceholderSpan 只支援的 baseline 配置。

Flutter 中的圖文混排與原理解析

  • _computeChildrenWidthWithMaxIntrinsics_computeChildrenWidthWithMaxIntrinsics 中會通過 PlaceholderSpan 去對應得到 PlaceholderDimensions,得到的 PlaceholderDimensions 會用於後續如 WidgetSpan 的大小繪製資訊。

這個 PlaceholderDimensions 會通過 setPlaceholderDimensions 方法設定到 TextPainter 裡面, 這樣 TextPainterlayout 的時候,就會將 PlaceholderDimensions 賦予 WidgetSpan 大小資訊。

Flutter 中的圖文混排與原理解析

  • _layoutText: _layoutText 方法會呼叫 _textPainter.layout, 從而執行 _text.build 方法,這個方法就會觸發 children 中的 WidgetSpan 去執行 build

Flutter 中的圖文混排與原理解析

所以如下程式碼所示,_textPainter.layout 會執行 Span 的 build 方法,將 PlaceholderDimensions 設定到 WidgetSpan 裡面,然後還有通過 _paragraph.getBoxesForPlaceholders() 方法獲取到控制元件繪製需要的 leftright 等資訊,這些資訊來源是基於上面 text.build 的執行。

Flutter 中的圖文混排與原理解析

_paragraph.getBoxesForPlaceholders() 獲取到的 TextBox 資訊,是基於後面我們介紹在 Span 裡提交的 addPlaceholder 方法獲取。

這些資訊會在 setParentData 方法中被設定到 TextParentData 裡,關於 ParentData 及其子類的作用,在《十六、詳解自定義佈局實戰》 同樣有所介紹,這裡就不贅述了,簡單理解就是 WidgetSpan 繪製的時候所需要的 offset 位置資訊會由它們提供。

Flutter 中的圖文混排與原理解析

之後如下程式碼所示, WidgetSpanbuild 方法被執行,這裡會有一個 placeholderCountplaceholderCount 預設是從 0 開始,而在執行 addPlaceholder 方法時會通過 _placeholderCount++ 自增,這樣下一個 WidgetSpan 就會拿到下一個 PlaceholderDimensions 用於設定大小。

addPlaceholder 之後會執行到 Flutter Engine 中的流程了。

Flutter 中的圖文混排與原理解析

最終 RenderParagrashpaint 方法會執行 _textPainter.paint 並把確定了大小和位置的 child 提交繪製。

Flutter 中的圖文混排與原理解析

是不是有點暈,結合下圖所示,總結起來其實就是:

  • RichText 中傳入 TextSpan , 在 TextSpan 的 children 中使用 WidgetSpanWidgetSpan 裡的 Widget 們會轉成 MultiChildRenderObjectElementchildren, 處理後得到一個 child 連結串列結構;
  • 之後 TextSpan 進入 RenderParagrash ,會抽取出對應 PlaceholderSpanWidgetSpan),然後通過轉化為 PlaceholderDimensions 儲存大小等資訊;
  • 之後進去 TextPainter 會觸發 InlineSpanbuild 方法,從而將前面得到的 PlaceholderDimensions 傳遞到 WidgetSpan 中;
  • WidgetSpan 中的控制元件資訊通過 addPlaceholder 會被傳遞到 Paragraph
  • 之後 TextPainter 中通過 addPlaceholder 的資訊獲取,呼叫 _paragraph.getBoxesForPlaceholders() 獲取去控制元件繪製需要的 offset
  • 有了大小和位置,最終文字中插入的控制元件,會在 RenderParagrashpaint 方法被繪製。

Flutter 中的圖文混排與原理解析

RichText 中插入控制元件的管理巧妙的依託到 MultiChildRenderObjectWidget 中,從而複用了原本控制元件的管理邏輯,之後依託引擎計算位置從而繪製完成。

至此,簡簡單單的 WidgetSpan 的實現原理解析完成~

資源推薦

Flutter 中的圖文混排與原理解析

相關文章