代码拉取完成,页面将自动刷新
同步操作将从 CarGuo/GSYFlutterBook 强制同步,此操作会覆盖自 Fork 仓库以来所做的任何修改,且无法恢复!!!
确定后同步将在后台操作,完成时将刷新页面,请耐心等待。
在移动开发中图文混排是十分常见的业务需求,如下图效果所示,本篇将介绍在 Flutter 中的图文混排效果与实现原理。
![](https://user-gold-cdn.xitu.io/2020/3/13/170d1a3d468ff0d4?w=640&h=960&f=gif&s=3253050)
事实上,针对如上所示的图文混排需求,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`** 。
![](https://user-gold-cdn.xitu.io/2020/3/12/170cf20cd793c622?w=820&h=376&f=png&s=18080)
正如我们知道的, Flutter 控件一般是由 `Widget`、`Element` 和 `RenderObeject` 三部分组成,而在 `RichText` 中也是如此,其中:
- `RenderParagraph` 主要是负责文本绘制、布局相关;
- `RichText` 继承 `MultiChildRenderObjectWidget` 主要是需要通过 `MultiChildRenderObjectElement` 来处理 `WidgetSpan` 中 children 控件的插入和管理。
#### 那 `WidgetSpan` 究竟是如何混入在文本绘制中呢?
在前面的使用中,我们首先是传入了一个 `TextSpan` 给 `RichText` ,并在 `TextSpan` 的 `children` 中拼接我们需要的内容,那就从 `RichText` 开始挖掘其中的原理。
![](https://user-gold-cdn.xitu.io/2020/3/12/170cf27cec3d02a3?w=1656&h=1196&f=png&s=296044)
如上代码所示,这里我们首先看 `RichText` 的入口,可以看到 `RichText` 开始是有一个 `_extractChildren` 方法,这个方法主要是将传入 `TextSpan` 的 `children` 里,所有的 `WidgetSpan` 通过 `visitChildren` 方法给递归筛选出来,然后传入给父类 `MultiChildRenderObjectWidget`。
> 为什么需要这么做?在 [《十六、详解自定义布局实战》](https://mp.weixin.qq.com/s/zwKG0ehMRPoRidRPtGGUpQ) 中介绍过,`MultiChildRenderObjectWidget` 的 children 最终会通过 `MultiChildRenderObjectElement` 作为桥梁,然后被插入到需要管理和绘制的 child 链表结构中,这样在 `RenderObject` 中方便管理和访问。
另外我们知道 `RichText` 传入的 `text` 其实是一个 `InlineSpan` ,而 `TextSpan` 就是 `InlineSpan` 的子类,`WidgetSpan` 也是 `InlineSpan` 的子类实现,它们的关系如下图所示:
![](https://user-gold-cdn.xitu.io/2020/3/12/170cf329b2f84541?w=435&h=384&f=png&s=12383)
对于 `InlineSpan` 系列我们主要关注两个方法:**`visitChildren` 和 `build`** 方法,它的子类 `TextSpan` 和 `WidgetSpan` 都对这两个方法有自己对应的实现。
```
void build(ui.ParagraphBuilder builder, { double textScaleFactor = 1.0, List<PlaceholderDimensions> dimensions });
bool visitChildren(InlineSpanVisitor visitor);
```
![](https://user-gold-cdn.xitu.io/2020/3/13/170d1f88c35526f4?w=725&h=361&f=png&s=66530)
接着看 `RenderParagraph` ,如上代码所示,`RichText` 中的 `text`(`InlineSpan`) 会继续被传入到 `RenderParagraph` 中,`RenderParagraph` 继承了 `RenderBox` 并混入的 `ContainerRenderObjectMixin` 和 `RenderBoxContainerDefaultsMixin` 等。
> 混入的对象这部分在内容在 [《十六、详解自定义布局实战》](https://mp.weixin.qq.com/s/zwKG0ehMRPoRidRPtGGUpQ) 也介绍过,这里只需要知道通过混入它们, `RenderParagraph` 就可以获得前面通过 `WidgetSpan` 传入到 `MultiChildRenderObjectElement` 的 children 链表,并且布局计算大小等。
![](https://user-gold-cdn.xitu.io/2020/3/13/170d1f9f41f02c1e?w=700&h=103&f=png&s=22593)
之后 `RenderParagraph` 中的 `text` 之后会被放置到 `TextPainter` 中使用,并且通过 `_extractPlaceholderSpans` 方法将所有的 `PlaceholderSpans` 筛选出来。
`TextPainter` 主要用于实现文本的绘制,这里我们暂时不多分析,**而 `_extractPlaceholderSpans` 挑选出来的所有 `PlaceholderSpans` ,其实就是 `WidgetSpan`**。
> `WidgetSpan` 是通过继承 `PlaceholderSpans` 从而实现了 `InlineSpan`,而目前暂时 `PlaceholderSpans` 实现的类只有 `WidgetSpan`。
![](https://user-gold-cdn.xitu.io/2020/3/12/170cf3a24fb5fa55?w=1660&h=1310&f=png&s=276358)
挑选出来的 `List<PlaceholderSpan>` 们会在 `RenderParagraph` 计算宽高等方法中被用到,比如 `computeMaxIntrinsicWidth` 方法等,**其中主要有 `_canComputeIntrinsics` 、 `_computeChildrenWidthWithMaxIntrinsics` 、`_layoutText` 三个关键**方法,这三个方法结合处理了 `RenderParagraph` 中 Span 的尺寸和布局等。
![](https://user-gold-cdn.xitu.io/2020/3/12/170cf442a6aec39f?w=1174&h=382&f=png&s=70717)
- **`_canComputeIntrinsics`**: `_canComputeIntrinsics` 主要判断了 `PlaceholderSpan` 只支持的 `baseline` 配置。
![](https://user-gold-cdn.xitu.io/2020/3/12/170cf43051ecb143?w=1630&h=876&f=png&s=197844)
- **`_computeChildrenWidthWithMaxIntrinsics`**: `_computeChildrenWidthWithMaxIntrinsics` 中会**通过 `PlaceholderSpan` 去对应得到 `PlaceholderDimensions`**,得到的 `PlaceholderDimensions` 会用于后续如 `WidgetSpan` 的大小绘制信息。
> 这个 `PlaceholderDimensions` 会通过 `setPlaceholderDimensions` 方法设置到 `TextPainter` 里面, 这样 `TextPainter` 在 `layout` 的时候,就会将 `PlaceholderDimensions` 赋予 `WidgetSpan` 大小信息。
![](https://user-gold-cdn.xitu.io/2020/3/12/170cf47d04cfad49?w=1986&h=618&f=png&s=167597)
- **`_layoutText`**: `_layoutText` 方法会调用 `_textPainter.layout`, 从而执行 `_text.build` 方法,这个方法就会触发 `children` 中的 `WidgetSpan` 去执行 `build` 。
![](https://user-gold-cdn.xitu.io/2020/3/12/170cf49c154770a6?w=1666&h=372&f=png&s=77258)
所以如下代码所示,`_textPainter.layout` 会执行 Span 的 `build` 方法,将 `PlaceholderDimensions` 设置到 `WidgetSpan` 里面,然后还有**通过 `_paragraph.getBoxesForPlaceholders()` 方法获取到控件绘制需要的 `left`、`right` 等信息**,这些信息来源是基于上面 `text.build` 的执行。
![](https://user-gold-cdn.xitu.io/2020/3/13/170d2747536db400?w=1287&h=519&f=png&s=131061)
> _paragraph.getBoxesForPlaceholders() 获取到的 `TextBox` 信息,是基于后面我们介绍在 Span 里提交的 `addPlaceholder` 方法获取。
这些信息会在 `setParentData` 方法中被设置到 `TextParentData` 里,关于 `ParentData` 及其子类的作用,在[《十六、详解自定义布局实战》](https://mp.weixin.qq.com/s/zwKG0ehMRPoRidRPtGGUpQ) 同样有所介绍,这里就不赘述了,简单理解就是 `WidgetSpan` 绘制的时候所需要的 `offset` 位置信息会由它们提供。
![](https://user-gold-cdn.xitu.io/2020/3/13/170d2714de8544c5?w=765&h=373&f=png&s=60124)
之后如下代码所示, `WidgetSpan` 的 `build` 方法被执行,这里会有一个 `placeholderCount`, `placeholderCount` 默认是从 0 开始,而在执行 `addPlaceholder` 方法时会通过 `_placeholderCount++` 自增,这样下一个 `WidgetSpan` 就会拿到下一个 `PlaceholderDimensions` 用于设置大小。
> `addPlaceholder` 之后会执行到 Flutter Engine 中的流程了。
![](https://user-gold-cdn.xitu.io/2020/3/13/170d1adb94b94f3b?w=1154&h=673&f=png&s=126401)
最终 `RenderParagrash` 的 `paint` 方法会执行 `_textPainter.paint` 并把确定了大小和位置的 child 提交绘制。
![](https://user-gold-cdn.xitu.io/2020/3/13/170d2692f63b5cbc?w=673&h=671&f=png&s=85782)
是不是有点晕,结合下图所示,总结起来其实就是:
- `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` 方法被绘制。
![](https://user-gold-cdn.xitu.io/2020/3/13/170d2834036c5a2a?w=2342&h=1414&f=png&s=151074)
**`RichText` 中插入控件的管理巧妙的依托到 `MultiChildRenderObjectWidget` 中,从而复用了原本控件的管理逻辑,之后依托引擎计算位置从而绘制完成。**
至此,简简单单的 `WidgetSpan` 的实现原理解析完成~
### 资源推荐
- Github :https://github.com/CarGuo
- 开源 Flutter 完整项目:https://github.com/CarGuo/GSYGithubAppFlutter
- 开源 Flutter 多案例学习型项目: https://github.com/CarGuo/GSYFlutterDemo
- 开源 Fluttre 实战电子书项目:https://github.com/CarGuo/GSYFlutterBook
- 开源 React Native 项目:https://github.com/CarGuo/GSYGithubApp
![](https://user-gold-cdn.xitu.io/2020/3/13/170d20b33b114566?w=2569&h=2518&f=jpeg&s=856467)
此处可能存在不合适展示的内容,页面不予展示。您可通过相关编辑功能自查并修改。
如您确认内容无涉及 不当用语 / 纯广告导流 / 暴力 / 低俗色情 / 侵权 / 盗版 / 虚假 / 无价值内容或违法国家有关法律法规的内容,可点击提交进行申诉,我们将尽快为您处理。