如何低成本实现Flutter富文本,看这一篇就够了!
作者:闲鱼技术-玄川
背景
闲鱼是国内最早使用Flutter 的团队,作为一个电商App商品详情页是非常重要场景,其中最主要的技术能力是文字混排。
我们面对文本类的需求是复杂而且多变,然而Flutter历史的几个版本,Text只能显示简单样式文本,它只有包含一些控制文本样式显示的属性,而通过TextSpan连接实现的RichText也只能显示多种文本样式(例如:一个基础文本片段和一个链接片段),这些远远达不到设计需要的能力。被产品和设计怂为啥别人别的平台能做,Flutter为何做不了,不管,必须支持。
因此,需要开发一个能力更强的文字混排组件就变得迫在眉睫。
富文本的原理
再讲文字混批组件设计实现前,先来讲讲系统RichText的富文本的原理。
-
创建过程
创建RichText节点的时候其实会创建以下几个对象:
- 先创建LeafRenderObjectElement实例。
- ComponentElement方法当中会调用RichText实例的CreateRenderObject方法,生成RenderParagraph 实例。
- RenderParagraph 会创建TextPainter 负责其就计算宽高和绘制文本到Canvas 的代理类,同时TextPainter 持有TextSpan 文本结构。
RenderParagraph实例最后会将自身登记到渲染模块的Dirty Nodes当中去,渲染模块会遍历Dirty Nodes 将进入RenderParagraph 渲染环节。
-
渲染过程
RenderParagraph 方法当中封装的是将文本绘制到 canvas 上面的逻辑,主要是用了一个叫做 TextPainter 的模块,其调用过程遵循RenderObject 调用。
- PerfromLayout 过程通过调用TextPaint的Layout,在期过程中通过TextSpan 结构树,依次通过AddText 添加各个阶段的文本,最后通过Paragraph的Layout 计算文本高度。
- Paint 过程,先绘制clipRect,接着通过TextPaint的Paint函数调用,Paragraph的Paint绘制文本,最后绘制drawRect。
设计思路
通过RichText的文本绘制原理,我们不难发现TextSpan记录了各段文本信息,TextPaint通过记录的信息调用Native接口计算宽高,以及将文本绘制到canvas上面。传统的方案实现复杂的混排,会通过HTML去做一个WebView的富文本,使用WebView在性能上自然不及原生实现,出于性能的考虑,我们设想通过通过原生的方式去实现图文混排。一开始的方案是设计几种特殊的Span(例如:ImageSpan,EmojiSpan等),通过Span记录的信息,在TextPaint的Layout 重新根据各种类型重新计算布局,在Paint过程再分别绘制特殊的Widget,然而这种方案对上面几个涉及的类封装破坏的特别大,需要将RichText、RenderParagraph 源码Copy 出来重新修改。最后设想是后可以通过特殊的文字先占位置,(例如:空字符串),然后在这个文字的位置上面把特殊的Span分别独立移动到上面。
然而上面这种方案会带来两个难点:
- 难点一:如何在文本中先占位,并且能制定任意想要的宽高。
通过Google 发现u200B字符代表ZERO WIDTH SPACE(宽带为0的空白),结合对TextPainter测试,我们发现layout出来的Width总是0,fontSize只决定了高度,结合TextStyle里面的letterSpacing
/// The amount of space (in logical pixels) to add between each letter /// A negative value can be used to bring the letters closer. final double letterSpacing;
这样我们就能任意的控制这个特殊文字的宽高度。
- 难点二:如何将特殊的Span移动到位置上面。
通过上面的测试不难发现,特殊的Span其实还是独立Widget和RichText并不融合。所以我们需要知道当前widget相对RichText空间的相对位置,并且结合Stack将其融合。结合TextPaint里面的getOffsetForCaret方法
/// Returns the offset at which to paint the caret. /// /// Valid only after [layout] has been called. Offset getOffsetForCaret(TextPosition position, Rect caretPrototype)
可以天然的获取到当前占位符相对位置。
实现方案
关键部分代码实现如下:
-
统一的占位SpaceSpan
SpaceSpan({ this.contentWidth, this.contentHeight, this.widgetChild, GestureRecognizer recognizer, }) : super( style: TextStyle( color: Colors.transparent, letterSpacing: contentWidth, height: 1.0, fontSize: contentHeight), text: '\u200B', recognizer: recognizer);
-
SpaceSpan 相对位置获取
for (TextSpan textSpan in widget.text.children) { if (textSpan is SpaceSpan) { final SpaceSpan targetSpan = textSpan; Offset offsetForCaret = painter.getOffsetForCaret( TextPosition(offset: textIndex), Rect.fromLTRB( 0.0, targetSpan.contentHeight, targetSpan.contentWidth, 0.0), ); ........ } textIndex += textSpan.toPlainText().length; }
-
RichtText和SpaceSpan融合
Stack( children: <Widget>[ RichText(), Positioned(left: position.dx, top: position.dy, child: child), ], ); }
效果
先上图看看效果
这种方案的优点是任意Widget可通过SpaceSpan和RichText进行组合,无论是图片、自定义标签、甚至是按钮都可以融合进来,同时对RichText本身封装性破坏较小。
未来
上面只是富文本显示的部分,依然存在着很多局限,还有较多需要优化的点,目前通过SpaceSpan 控件,必需要指定宽高,另外对于文本选择、自定义文字背景这些都是无法支持,其次对富文本编辑器的支持,可以使其编辑文字时,让图片、货币格式化等控件输入等。
原文链接
本文为云栖社区原创内容,未经允许不得转载。
低调大师中文资讯倾力打造互联网数据资讯、行业资源、电子商务、移动互联网、网络营销平台。
持续更新报道IT业界、互联网、市场资讯、驱动更新,是最及时权威的产业资讯及硬件资讯报道平台。
转载内容版权归作者及来源网站所有,本站原创内容转载请注明来源。
- 上一篇
AI中台——智能聊天机器人平台的架构与应用(分享实录)
内容来源:宜信技术学院第3期技术沙龙-线上直播|AI中台——智能聊天机器人平台 主讲人:宜信科技中心AI中台团队负责人王东 导读:随着“中台”战略的提出,目前宜信中台建设在思想理念及架构设计上都已经取得了很多成果。宜信是如何借助中台化的思想打造“AI中台”及相关的智能产品呢?本次直播,宜信科技中心AI中台团队负责人王东老师分享了宜信AI中台的具体实施路径,并重点介绍了AI中台的智能产品——智能聊天机器人平台,包括智能聊天机器人平台的背景理念、设计思想、技术架构和应用场景,该平台能提供什么样的能力,以及它如何快速地支持业务方,提供一种以中台化的思想来建设智能产品的实践思路。 视频回放:https://v.qq.com/x/page/q0904bcjlkn.html —————— 前两期技术沙龙分别分享了宜信AI中台和数据中台的建设实践,本次分享将先回顾AI中台的总体设计和实施路径,以及AI中台与数据中台的关系,再详细介绍基于中台思想建设的智能聊天机器人平台,包括其技术架构、技术原理、核心功能点、应用场景以及应用效果。 一、AI中台总体设计和实施步骤 1.1 业务演进与广泛的智能化需求 随...
- 下一篇
WebFlux定点推送、全推送灵活websocket运用
前言 WebFlux 本身提供了对 WebSocket 协议的支持,处理 WebSocket 请求需要对应的 handler 实现 WebSocketHandler 接口,每一个 WebSocket 都有一个关联的 WebSocketSession,包含了建立请求时的握手信息HandshakeInfo,以及其它相关的信息。可以通过 session 的receive()方法来接收客户端的数据,通过 session 的send()方法向客户端发送数据。 示例 下面是一个简单的 WebSocketHandler 示例: @Component public class EchoHandler implements WebSocketHandler { public Mono<Void> handle(WebSocketSession session) { return session.send( session.receive().map( msg -> session.textMessage("ECHO -> " + msg.getPayloadAsText...
相关文章
文章评论
共有0条评论来说两句吧...
文章二维码
点击排行
推荐阅读
最新文章
- CentOS7,8上快速安装Gitea,搭建Git服务器
- CentOS6,7,8上安装Nginx,支持https2.0的开启
- CentOS关闭SELinux安全模块
- SpringBoot2初体验,简单认识spring boot2并且搭建基础工程
- CentOS8编译安装MySQL8.0.19
- SpringBoot2整合MyBatis,连接MySql数据库做增删改查操作
- CentOS8安装MyCat,轻松搞定数据库的读写分离、垂直分库、水平分库
- CentOS8安装Docker,最新的服务器搭配容器使用
- CentOS7,CentOS8安装Elasticsearch6.8.6
- Red5直播服务器,属于Java语言的直播服务器