Flutter 中的圖文混排與原理解析 | 開發者說·DTalk

本文原作者: 戀貓de小郭,原文發佈於微信公衆號: GSYTech 

https://mp.weixin.qq.com/s/whri3kskHFTEDer0ub51Jw

在移動開發中圖文混排是十分常見的業務需求,如下圖效果所示,本篇將介紹在 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 的實現如下圖所示,主要可以分爲三部分: MultiChildRenderObjectWidgetMultiChildRenderObjectElement RenderParagraph

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

  • RenderParagraph 主要是負責文本繪製、佈局相關;

  • RichText 繼承 MultiChildRenderObjectWidget 主要是需要通過 MultiChildRenderObjectElement 來處理 WidgetSpan 中 children 控件的插入和管理。

那 WidgetSpan 究竟是如何混入到文本繪製中呢?

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

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

爲什麼需要這麼做?MultiChildRenderObjectWidget 的 children 最終會通過 MultiChildRenderObjectElement 作爲橋樑,然後被插入到需要管理和繪製的 child 鏈表結構中,這樣在 RenderObject 中方便管理和訪問。

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

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

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

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

混入的對象這部分這裏只需要知道通過混入它們,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 及其子類的作用,簡單理解就是 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,會抽取出對應 PlaceholderSpan (WidgetSpan),然後通過轉化爲 PlaceholderDimensions 保存大小等信息;

  • 之後進去 TextPainter 會觸發 InlineSpanbuild 方法,從而將前面得到的 PlaceholderDimensions 傳遞到 WidgetSpan 中;

  • WidgetSpan 中的控件信息通過 addPlaceholder 會被傳遞到 Paragraph

  • 之後 TextPainter 中通過 addPlaceholder 的信息獲取,調用 _paragraph.getBoxesForPlaceholders() 獲取去控件繪製需要的 offset

  • 有了大小和位置,最終文本中插入的控件,會在 RenderParagrashpaint 方法被繪製。

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

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


長按右側二維碼

查看更多開發者精彩分享

"開發者說·DTalk" 面向中國開發者們徵集 Google 移動應用 (apps & games) 相關的產品/技術內容。歡迎大家前來分享您對移動應用的行業洞察或見解、移動開發過程中的心得或新發現、以及應用出海的實戰經驗總結和相關產品的使用反饋等。我們由衷地希望可以給這些出衆的中國開發者們提供更好展現自己、充分發揮自己特長的平臺。我們將通過大家的技術內容着重選出優秀案例進行谷歌開發技術專家 (GDE) 的推薦。

 

 點擊屏末 |  | 即刻報名參與 "開發者說·DTalk" 

 


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章