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

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

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

以下代碼所示,經過 Text.rich 接入 TextSpanWidgetSpan 就能夠快速實現圖文混排的需求,而且能夠看出 WidgetSpan 不止支持圖片控件,它能夠接入任何你須要的 Widget ,好比 CardInkWell 等等。bash

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 的實現以下圖所示,主要能夠分爲三部分:MultiChildRenderObjectWidgetMultiChildRenderObjectElementRenderParagraphui

正如咱們知道的, Flutter 控件通常是由 WidgetElementRenderObeject 三部分組成,而在 RichText 中也是如此,其中:spa

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

WidgetSpan 到底是如何混入在文本繪製中呢?

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

如上代碼所示,這裏咱們首先看 RichText 的入口,能夠看到 RichText 開始是有一個 _extractChildren 方法,這個方法主要是將傳入 TextSpanchildren 裏,全部的 WidgetSpan 經過 visitChildren 方法給遞歸篩選出來,而後傳入給父類 MultiChildRenderObjectWidgetcode

爲何須要這麼作?在 《十6、詳解自定義佈局實戰》 中介紹過,MultiChildRenderObjectWidget 的 children 最終會經過 MultiChildRenderObjectElement 做爲橋樑,而後被插入到須要管理和繪製的 child 鏈表結構中,這樣在 RenderObject 中方便管理和訪問。cdn

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

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

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

  bool visitChildren(InlineSpanVisitor visitor);
複製代碼

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

混入的對象這部分在內容在 《十6、詳解自定義佈局實戰》 也介紹過,這裏只須要知道經過混入它們, RenderParagraph 就能夠得到前面經過 WidgetSpan 傳入到 MultiChildRenderObjectElement 的 children 鏈表,而且佈局計算大小等。

以後 RenderParagraph 中的 text 以後會被放置到 TextPainter 中使用,而且經過 _extractPlaceholderSpans 方法將全部的 PlaceholderSpans 篩選出來。

TextPainter 主要用於實現文本的繪製,這裏咱們暫時很少分析,_extractPlaceholderSpans 挑選出來的全部 PlaceholderSpans ,其實就是 WidgetSpan

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

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

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

  • _computeChildrenWidthWithMaxIntrinsics_computeChildrenWidthWithMaxIntrinsics 中會經過 PlaceholderSpan 去對應獲得 PlaceholderDimensions,獲得的 PlaceholderDimensions 會用於後續如 WidgetSpan 的大小繪製信息。

這個 PlaceholderDimensions 會經過 setPlaceholderDimensions 方法設置到 TextPainter 裏面, 這樣 TextPainterlayout 的時候,就會將 PlaceholderDimensions 賦予 WidgetSpan 大小信息。

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

因此以下代碼所示,_textPainter.layout 會執行 Span 的 build 方法,將 PlaceholderDimensions 設置到 WidgetSpan 裏面,而後還有經過 _paragraph.getBoxesForPlaceholders() 方法獲取到控件繪製須要的 leftright 等信息,這些信息來源是基於上面 text.build 的執行。

_paragraph.getBoxesForPlaceholders() 獲取到的 TextBox 信息,是基於後面咱們介紹在 Span 裏提交的 addPlaceholder 方法獲取。

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

以後以下代碼所示, WidgetSpanbuild 方法被執行,這裏會有一個 placeholderCountplaceholderCount 默認是從 0 開始,而在執行 addPlaceholder 方法時會經過 _placeholderCount++ 自增,這樣下一個 WidgetSpan 就會拿到下一個 PlaceholderDimensions 用於設置大小。

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

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

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

  • 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 方法被繪製。

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

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

資源推薦

相關文章
相關標籤/搜索