在移动开发中图文混排是十分常见的业务需求,以下图效果所示,本篇将介绍在 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
的实现原理解析完成~