在移動開發中圖文混排是十分常見的業務需求,以下圖效果所示,本篇將介紹在 Flutter 中的圖文混排效果與實現原理。git
事實上,針對如上所示的圖文混排需求,Flutter 官方提供了十分便捷的實現方式: WidgetSpan
。github
以下代碼所示,經過 Text.rich
接入 TextSpan
和 WidgetSpan
就能夠快速實現圖文混排的需求,而且能夠看出 WidgetSpan
不止支持圖片控件,它能夠接入任何你須要的 Widget
,好比 Card
、InkWell
等等。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
的實現以下圖所示,主要能夠分爲三部分:MultiChildRenderObjectWidget
、 MultiChildRenderObjectElement
和 RenderParagraph
。ui
正如咱們知道的, Flutter 控件通常是由 Widget
、Element
和 RenderObeject
三部分組成,而在 RichText
中也是如此,其中:spa
RenderParagraph
主要是負責文本繪製、佈局相關;RichText
繼承 MultiChildRenderObjectWidget
主要是須要經過 MultiChildRenderObjectElement
來處理 WidgetSpan
中 children 控件的插入和管理。WidgetSpan
到底是如何混入在文本繪製中呢?在前面的使用中,咱們首先是傳入了一個 TextSpan
給 RichText
,並在 TextSpan
的 children
中拼接咱們須要的內容,那就從 RichText
開始挖掘其中的原理。3d
如上代碼所示,這裏咱們首先看 RichText
的入口,能夠看到 RichText
開始是有一個 _extractChildren
方法,這個方法主要是將傳入 TextSpan
的 children
裏,全部的 WidgetSpan
經過 visitChildren
方法給遞歸篩選出來,而後傳入給父類 MultiChildRenderObjectWidget
。code
爲何須要這麼作?在 《十6、詳解自定義佈局實戰》 中介紹過,
MultiChildRenderObjectWidget
的 children 最終會經過MultiChildRenderObjectElement
做爲橋樑,而後被插入到須要管理和繪製的 child 鏈表結構中,這樣在RenderObject
中方便管理和訪問。cdn
另外咱們知道 RichText
傳入的 text
實際上是一個 InlineSpan
,而 TextSpan
就是 InlineSpan
的子類,WidgetSpan
也是 InlineSpan
的子類實現,它們的關係以下圖所示:
對於 InlineSpan
系列咱們主要關注兩個方法:visitChildren
和 build
方法,它的子類 TextSpan
和 WidgetSpan
都對這兩個方法有本身對應的實現。
void build(ui.ParagraphBuilder builder, { double textScaleFactor = 1.0, List<PlaceholderDimensions> dimensions });
bool visitChildren(InlineSpanVisitor visitor);
複製代碼
接着看 RenderParagraph
,如上代碼所示,RichText
中的 text
(InlineSpan
) 會繼續被傳入到 RenderParagraph
中,RenderParagraph
繼承了 RenderBox
並混入的 ContainerRenderObjectMixin
和 RenderBoxContainerDefaultsMixin
等。
混入的對象這部分在內容在 《十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
裏面, 這樣TextPainter
在layout
的時候,就會將PlaceholderDimensions
賦予WidgetSpan
大小信息。
_layoutText
: _layoutText
方法會調用 _textPainter.layout
, 從而執行 _text.build
方法,這個方法就會觸發 children
中的 WidgetSpan
去執行 build
。因此以下代碼所示,_textPainter.layout
會執行 Span 的 build
方法,將 PlaceholderDimensions
設置到 WidgetSpan
裏面,而後還有經過 _paragraph.getBoxesForPlaceholders()
方法獲取到控件繪製須要的 left
、right
等信息,這些信息來源是基於上面 text.build
的執行。
_paragraph.getBoxesForPlaceholders() 獲取到的
TextBox
信息,是基於後面咱們介紹在 Span 裏提交的addPlaceholder
方法獲取。
這些信息會在 setParentData
方法中被設置到 TextParentData
裏,關於 ParentData
及其子類的做用,在《十6、詳解自定義佈局實戰》 一樣有所介紹,這裏就不贅述了,簡單理解就是 WidgetSpan
繪製的時候所須要的 offset
位置信息會由它們提供。
以後以下代碼所示, WidgetSpan
的 build
方法被執行,這裏會有一個 placeholderCount
, placeholderCount
默認是從 0 開始,而在執行 addPlaceholder
方法時會經過 _placeholderCount++
自增,這樣下一個 WidgetSpan
就會拿到下一個 PlaceholderDimensions
用於設置大小。
addPlaceholder
以後會執行到 Flutter Engine 中的流程了。
最終 RenderParagrash
的 paint
方法會執行 _textPainter.paint
並把肯定了大小和位置的 child 提交繪製。
是否是有點暈,結合下圖所示,總結起來其實就是:
RichText
中傳入 TextSpan
, 在 TextSpan
的 children 中使用 WidgetSpan
,WidgetSpan
裏的 Widget
們會轉成 MultiChildRenderObjectElement
的 children
, 處理後獲得一個 child 鏈表結構;TextSpan
進入 RenderParagrash
,會抽取出對應 PlaceholderSpan
(WidgetSpan
),而後經過轉化爲 PlaceholderDimensions
保存大小等信息;TextPainter
會觸發 InlineSpan
的 build
方法,從而將前面獲得的 PlaceholderDimensions
傳遞到 WidgetSpan
中;WidgetSpan
中的控件信息經過 addPlaceholder
會被傳遞到 Paragraph
;TextPainter
中經過 addPlaceholder
的信息獲取,調用 _paragraph.getBoxesForPlaceholders()
獲取去控件繪製須要的 offset
;RenderParagrash
的 paint
方法被繪製。RichText
中插入控件的管理巧妙的依託到 MultiChildRenderObjectWidget
中,從而複用了本來控件的管理邏輯,以後依託引擎計算位置從而繪製完成。
至此,簡簡單單的 WidgetSpan
的實現原理解析完成~