-
前言
-
零、GSY历程
-
一、Dart语言和Flutter基础
-
二、 快速开发实战篇
-
三、 打包与填坑篇
-
四、 Redux、主题、国际化
-
五、 深入探索
-
六、 深入Widget原理
-
七、 深入布局原理
-
八、 实用技巧与填坑
-
九、 深入绘制原理
-
十、 深入图片加载流程
-
十一、全面深入理解Stream
-
十二、全面深入理解状态管理设计
-
十三、全面深入触摸和滑动原理
-
十四、混合开发打包 Android 篇
-
十五、全面理解State与Provider
-
十六、详解自定义布局实战
-
十七、实用技巧与填坑二
-
十八、 神奇的ScrollPhysics与Simulation
-
十九、 Android 和 iOS 打包提交审核指南
-
二十、 Android PlatformView 和键盘问题
-
二十一、 Flutter 画面渲染的全面解析
-
Flutter 跨平台框架应用实战-2019极光开发者大会
-
Flutter 面试知识点集锦
-
全网最全 Flutter 与 ReactNative深入对比分析
-
Flutter 开发实战与前景展望 - RTC Dev Meetup
-
Flutter Interact 的 Flutter 1.12 大进化和回顾
-
Flutter 升级 1.12 适配教程
-
Spuernova 是如何提升 Flutter 的生产力
-
Flutter 中的图文混排与原理解析
-
Flutter 实现视频全屏播放逻辑及解析
-
Flutter 上的一个 Bug 带你了解键盘与路由的另类知识点
-
Flutter 上默认的文本和字体知识点
-
带你深入理解 Flutter 中的字体“冷”知识
-
Flutter 1.17 中的导航解密和性能提升
-
Flutter 1.17 对列表图片的优化解析
-
Flutter 1.20 下的 Hybrid Composition 深度解析
-
2020 腾讯Techo Park - Flutter与大前端的革命
-
带你全面了解 Flutter,它好在哪里?它的坑在哪里? 应该怎么学?
-
Flutter 中键盘弹起时,Scaffold 发生了什么变化
-
Flutter 2.0 下混合开发浅析
-
Flutter 搭建 iOS 命令行服务打包发布全保姆式流程
-
不一样角度带你了解 Flutter 中的滑动列表实现
-
Flutter 跨平台框架应用实战-2019极光开发者大会
在移动开发中图文混排是十分常见的业务需求,如下图效果所示,本篇将介绍在 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!'),
],
)
copy
也就是说 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);
copy
接着看 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
的实现原理解析完成~
资源推荐
- 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