本文原作者: 戀貓de小郭,原文發佈於微信公衆號: GSYTech
https://mp.weixin.qq.com/s/whri3kskHFTEDer0ub51Jw
在移動開發中圖文混排是十分常見的業務需求,如下圖效果所示,本篇將介紹在 Flutter 中的圖文混排效果與實現原理。
事實上,針對如上所示的圖文混排需求,Flutter 官方提供了十分便捷的實現方式: WidgetSpan。
如下代碼所示,通過 Text.rich 接入 TextSpan 和 WidgetSpan 就可以快速實現圖文混排的需求,並且可以看出 WidgetSpan 不止支持圖片控件,它可以接入任何您需要的 Widget ,比如 Card、InkWell 等等。
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 。
正如我們知道的,Flutter 控件一般是由 Widget、Element 和 RenderObeject 三部分組成,而在 RichText 中也是如此,其中:
RenderParagraph 主要是負責文本繪製、佈局相關;
RichText 繼承 MultiChildRenderObjectWidget 主要是需要通過 MultiChildRenderObjectElement 來處理 WidgetSpan 中 children 控件的插入和管理。
那 WidgetSpan 究竟是如何混入到文本繪製中呢?
在前面的使用中,我們首先是傳入了一個 TextSpan 給 RichText,並在 TextSpan 的 children 中拼接我們需要的內容,那就從 RichText 開始挖掘其中的原理。
如上代碼所示,這裏我們首先看 RichText 的入口,可以看到 RichText 開始是有一個 _extractChildren 方法,這個方法主要是將傳入 TextSpan 的 children 裏所有的 WidgetSpan 通過 visitChildren 方法給遞歸篩選出來,然後傳給父類 MultiChildRenderObjectWidget。
爲什麼需要這麼做?MultiChildRenderObjectWidget 的 children 最終會通過 MultiChildRenderObjectElement 作爲橋樑,然後被插入到需要管理和繪製的 child 鏈表結構中,這樣在 RenderObject 中方便管理和訪問。
另外我們知道 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 等。
混入的對象這部分這裏只需要知道通過混入它們,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 及其子類的作用,簡單理解就是 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 的實現原理解析完成。
長按右側二維碼
查看更多開發者精彩分享
"開發者說·DTalk" 面向中國開發者們徵集 Google 移動應用 (apps & games) 相關的產品/技術內容。歡迎大家前來分享您對移動應用的行業洞察或見解、移動開發過程中的心得或新發現、以及應用出海的實戰經驗總結和相關產品的使用反饋等。我們由衷地希望可以給這些出衆的中國開發者們提供更好展現自己、充分發揮自己特長的平臺。我們將通過大家的技術內容着重選出優秀案例進行谷歌開發技術專家 (GDE) 的推薦。
點擊屏末 | 閱讀原文 | 即刻報名參與 "開發者說·DTalk"