一个易迁移、兼容性高的 Flutter 富文本方案

简介:flutter富文本演进

作者:闲鱼技术-新宿

背景

在闲鱼消息体系中,富文本在 UI 侧占了非常大的比重。最近消息部分在整体 Flutter 化,如何解决 Flutter 侧富文本问题,成为了项目早期的风险点。

在 Native 中,消息使用了 HTML 协议来承载富文本的解析与展示,由于消息的历史数据有落库的特性,我们必须在 Flutter 侧兼容这种协议。对于 Flutter,我们是否可以在兼容的基础上,进行能力的扩充与完善?

当前闲鱼也在升级 Flutter 1.12,所以我们不光要在当前版本支持图文混排,也需要快速迁移到高版本的系统方案。因此我们需要找到一个兼容性高、易迁移的富文本方案。

行业现状

行业内,对于旧版的 RichText (Flutter 1.7.3 之前)已有了解决方案,详见玄川:《如何低成本实现Flutter 富文本,看这一篇就够了!》。但这里并没有对富文本的整个链路的解决思路,且 Flutter 自身的 RichText 也在随着版本迭代进行演进,我们需要一套完整的演进方案。

事实上,Flutter 1.7.3 开始的 RichText 解决了我们的很多麻烦,它是怎么实现的呢?和旧版的实现有什么区别呢?带着问题,我们先来分析它的实现原理。

RichText 图文混排原理

Flutter 1.7.3 开始,RichText 不再继承自 LeafRenderObjectWidget,而是继承自 MultiChildRenderObjectWidget,从这就很容易看出,RichText 将是一个布局控件,内部可以有多个子控件。

创建过程:

richText

如上图,我们传给 RichText 的 text 参数为 InlineSpan,TextSpan、WidgetSpan都是其子类。

  1. RichText 初始化过程中会将 text 中的所有 WidgetSpan 递归筛选出来,传递给父类 MultiChildRenderObjectWidget。
  2. 创建 MultiChildRenderObjectElement,接着 RichText 会通过 createRenderObject,生成 RenderParagraph。
  3. RenderParagraph 初始化过程中会创建 TextPainter,这个是绘制的核心,这里将会进行 layout、paint 和事件分发操作;然后递归筛选 PlaceholderSpan (其实还是 WidgetSpan)。
渲染过程:

carbon

上图为 RenderParagraph 内的 performLayout 函数。

  1. 如上图 RenderParagraph 执行 performLayout 。首先 _layoutChildren:为子控件布局,目的为获取子控件大小,如果没有子控件则直接 return。这里所说的子控件就是 WidgetSpan。
  2. 第二步 _layoutTextWithConstraints,就是执行 _textPainter 的 layout 方法,这里会让 text(InlineSpan)进行 build,此时会按照它的树形结构遍历执行。

    1. TextWidget build 时会将自身的 text(这是真实的字符串)addText 给 builder。
    2. WidgetSpan build 时会将自身控件的 PlaceholderDimensions 信息,addPlaceholder 给 builder。这里其实就是添加占位符,占位符将与控件同大小。
    3. 紧接着 _paragraph 会进行一次布局,然后获取各个占位符位置存储下来。
  3. Paint 过程会先将带着占位符的文本绘制完成,然后遍历子控件按照 2-3 步骤中获得的占位符位置,设置偏移。

概括来说,新版本对比旧版本,底层多了个 _addPlaceholder 能力,用来占位混排的 Widget,并获取位置信息。

设计思路

我们以 HTML 协议为抓手,不光可以解决普通 HTML 字符串的解析与渲染,也可以对用户发送的带闲鱼自定义 emoji 的字符串进行能力的扩充。下图为大致的设计思路:

richText2

当前消息展示分为两种场景,一种为带有闲鱼自定义 emoji 表情的字符串:

你好[微笑],你的宝贝不错哦[呲牙],包邮吗?[坏笑][坏笑]

另一种为简单的 HTML 字符串:

"<font color="#888888">交易全程在闲鱼,</font><strong><font color="#F54444">你敢买,我敢赔!</font></strong><font color="#888888">若遇欺诈造成</font><strong><font color="#F54444">钱货两失,可获赔</font></strong><strong><font color="#F54444">最高5000元</font></strong>"

当然,还有最普通的纯文本。

对于这三种字符串,服务端并没有用类型来给我们区分,客户端拿到的都为字符串。端侧该如何处理且高效展示呢?

过程设计成这样:

  1. 首先对于确定为纯文字的控件,直接使用单 TextSpan 的 RichText,免去 Text 的封装。
  2. 使用RegExp(r'\[[^\]\[]+\]')匹配[微笑]等 emoji 占位符,替换为<img src=003_微笑.png width=22.400000 height=22.400000/>
  3. 取最后的 HTMLString ,使用html | Dart Package,进行 HTML 解析,生成 HTML Node Tree
  4. 递归 HTML Node Tree

    1. 文本标签映射为 TextSpan
    2. 图片标签映射为 FDImageSpan;Flutter 升级后将其替换为 WidgetSpan,其 child 设置为 Image Widget
    3. 链接标签映射为 TextSpan,定义 GestureRecognizer 相应手势

流程上,先将闲鱼自定义 emoji 占位符转为 HTML 元素,接着统一处理 HTML 字符串。然后将 HTML 字符串统一转为富文本。设计上,分为两层:数据解析层、渲染层。

richText1

如上图,有了前面原生 Flutter 图文混排支撑,我们在低版本可以仿照实现,低版本 RichText 继承自 LeafRenderObjectWidget,我们把 RichText 与其他 Widget 组成新的 MultiChildRenderObjectWidget,通过占位符正常渲染文本,之后获取占位符位置,设置对应 Widget 的位置。

Flutter SDK 升级过程中如何保持业务方无感知?先看下图:

richText0

对比发现,在 TextSpan 树中,我们继承自 TextSpan 的自定义 FDImageSpan,实际上可以直接对应到原生的 WidgetSpan,这里我们可以在 HTML Node Tree 映射到 TextSpan Tree 的过程中直接修改。而 FDRichText build 里,我们可以直接返回系统 RichText。这样的改动,对于使用方可以做到无感知。

效果

上图中是一种最为简单和常见的系统消息,为了突出安全警示,使用了较多的红色字体。模块中定义的三个富文本,均可定制样式。

效果2

上图为涉及交互的富文本,买家可以点击蓝色文字「那儿发货」,然后买家会自动发送「那儿发货」给卖家,卖家会根据预设的问题自动回复买家。点击会触发 HTML 字符串中的 href 自定义协议链接,客户端会触发 openURL 的操作,以此来实现交互。

效果3

这是普通用户可以编辑发送的富文本,丰富的闲鱼自定义 emoji,穿插在文字中,不仅增加了聊天乐趣,也增强了用户的表达。

未来

当前的展示部分仅仅是图文混排,新版本中的富文本支持任意 Widget,可玩性更高,所以我们对 HTML 标签描述可以进行扩充,这部分未来还需要持续探索。

由于篇幅有限,上文并没有讲述富文本编辑器。消息中用户输入框也需支持闲鱼自定义 emoji,当前版本的方案为直接使用占位符(比如“[微笑]”),并不展示实际的图片。我们回头再看一下 HTML 协议,对于新版的 TextField,我们可以支持吗?这就不仅仅局限在自定义 emoji 里了。

我们把目光转向发布和宝贝详情:

展望富文本

我们后续可能会支持上图中,发布和宝贝详情的富文本编辑和展示。对比两个详情页,很明显能看出,使用富文本的方式,在表达上更加富有冲击力,买家阅读起来能很容易抓住卖家想表达的关键信息。

可见,未来在富文本的编辑、展示基础能力统一之后,可以让更多业务收益。


阿里技术
阿里巴巴官方技术号,关于阿里巴巴经济体的技术创新、实战经验、技术人的成长心得均呈现于此。

阿里巴巴官方技术号,关于阿里巴巴经济体的技术创新、实战经验、技术人的成长心得均呈现于此。

3.1k 声望
6.2k 粉丝
0 条评论
推荐阅读
阿里云:加大NoSQL数据库软硬件一体化技术自研
简介:8月25日,在天池平台与阿里云数据库事业部联合主办的阿里云NoSQL数据库峰会上,阿里云公布NoSQL数据库自研2.0计划,进一步加大软硬件一体化技术体系的自研力度,通过聚焦软硬协同、多模融合、云原生三大方...

阿里云开发者阅读 448

一个 JSer 的 Dart 学习日志(四):异步编程
本文是“一个 JSer 的 Dart 学习日志”系列的第四篇,本系列文章主要以挖掘 JS 与 Dart 异同点的方式,在复习和巩固 JS 的同时平稳地过渡到 Dart 语言。鉴于作者尚属 Dart 初学者,所以认识可能会比较肤浅和片面,...

知名喷子阅读 1.5k

封面图
闲鱼app数据实时采集探索实验
前言本文章分享一下最近研究闲鱼app商品数据采集。技术栈PythonFridaJADXObjectionAndroid Studio思路使用Android Studio创建x86模拟器并运行,安装闲鱼和frida使用Objection hook URL类,打印调用栈分析出关键函...

Prasanta阅读 1.9k评论 4

Android 开发中的SSL pinning
在日常的安全渗透过程中,我们经常会遇到瓶颈无处下手,这时候如果攻击者从APP进行突破,往往会有很多惊喜。但是目前市场上的APP都会为防止别人恶意盗取和恶意篡改进行一些保护措施,比如模拟器检测、root检测、A...

xiangzhihong阅读 1k

Flutter 这一年:2022 亮点时刻
2022 年,我们非常兴奋的看到 Flutter 社区持续发展壮大,也因此让更多人体验到了令人难以置信的体验。每天有超过 1000 款使用 Flutter 的新移动应用发布到 App Store 和 Google Play,Web 平台和桌面应用程序数...

Flutter阅读 842

flutter系列之:在flutter中使用流式布局
我们在开发web应用的时候,有时候为了适应浏览器大小的调整,需要动态对页面的组件进行位置的调整。这时候就会用到flow layout,也就是流式布局。

flydean阅读 806

Flutter for Web 首次首屏优化——JS 分片优化
Flutter for Web(FFW)从 2021 年发布至今,在国内外互联网公司已经得到较多的应用。作为 Flutter 技术在 Web 领域的有力扩充,FFW 可以让熟悉 Flutter 的客户端同学直接上手写 H5,复用 App 端代码高效支撑业务...

阿里巴巴终端技术阅读 778

封面图

阿里巴巴官方技术号,关于阿里巴巴经济体的技术创新、实战经验、技术人的成长心得均呈现于此。

3.1k 声望
6.2k 粉丝
宣传栏