如何低成本实现Flutter富文本,看这一篇就够了!

简介: 作者:闲鱼技术-玄川背景闲鱼是国内最早使用Flutter 的团队,作为一个电商App商品详情页是非常重要场景,其中最主要的技术能力是文字混排。我们面对文本类的需求是复杂而且多变,然而Flutter历史的几个版本,Text只能显示简单样式文本,它只有包含一些控制文本样式显示的属性,而通过TextSpan连接实现的RichText也只能显示多种文本样式(例如:一个基础文本片段和一个链接片段),这些远远达不到设计需要的能力。

作者:闲鱼技术-玄川

背景

闲鱼是国内最早使用Flutter 的团队,作为一个电商App商品详情页是非常重要场景,其中最主要的技术能力是文字混排。

我们面对文本类的需求是复杂而且多变,然而Flutter历史的几个版本,Text只能显示简单样式文本,它只有包含一些控制文本样式显示的属性,而通过TextSpan连接实现的RichText也只能显示多种文本样式(例如:一个基础文本片段和一个链接片段),这些远远达不到设计需要的能力。被产品和设计怂为啥别人别的平台能做,Flutter为何做不了,不管,必须支持。

因此,需要开发一个能力更强的文字混排组件就变得迫在眉睫。

富文本的原理

再讲文字混批组件设计实现前,先来讲讲系统RichText的富文本的原理。

  • 创建过程
    RichText 创建

    创建RichText节点的时候其实会创建以下几个对象:

    1. 先创建LeafRenderObjectElement实例。
    2. ComponentElement方法当中会调用RichText实例的CreateRenderObject方法,生成RenderParagraph 实例。
    3. RenderParagraph 会创建TextPainter 负责其就计算宽高和绘制文本到Canvas 的代理类,同时TextPainter 持有TextSpan 文本结构。

    RenderParagraph实例最后会将自身登记到渲染模块的Dirty Nodes当中去,渲染模块会遍历Dirty Nodes 将进入RenderParagraph 渲染环节。

  • 渲染过程

    RenderParagraph 方法当中封装的是将文本绘制到 canvas 上面的逻辑,主要是用了一个叫做 TextPainter 的模块,其调用过程遵循RenderObject 调用。

    1. PerfromLayout 过程通过调用TextPaint的Layout,在期过程中通过TextSpan 结构树,依次通过AddText 添加各个阶段的文本,最后通过Paragraph的Layout 计算文本高度。
    2. 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 控件,必需要指定宽高,另外对于文本选择、自定义文字背景这些都是无法支持,其次对富文本编辑器的支持,可以使其编辑文字时,让图片、货币格式化等控件输入等。

相关文章
|
JSON Dart IDE
Flutter实现国际化
开发一个App,如果我们的App需要面向不同的语种(比如中文、英文、繁体等),那么我们需要对齐进行国际化开发
1410 0
Flutter实现国际化
|
7月前
|
存储 前端开发 API
flutter 富文本思考
flutter 富文本思考
77 2
Flutter 底部导航栏BottomNavigationBar,并关联PageView实现滑动切换
Flutter 底部导航栏BottomNavigationBar,并关联PageView实现滑动切换
355 0
|
Dart IDE 开发工具
Flutter 图文并茂列表实现
Flutter使用 ListView 完成列表的构建,界面实现的关键工作实际是布局子元素的拆分。剩下的实现方式存在多种,看各人喜好。但是,需要注意避免过多嵌套导致代码不好维护,并需要提高复用性。
882 2
Flutter 图文并茂列表实现
|
数据安全/隐私保护 UED
Flutter 使用自定义fluro 路由实现访问权限控制
本篇介绍了利用 Fluro 路由管理实现路由权限拦截的两种方式,两种方式各有好处,使用过程中可以根据实际情况决定使用哪一种方法。
636 1
Flutter 使用自定义fluro 路由实现访问权限控制
|
存储 数据安全/隐私保护
Flutter App页面路由及路由拦截实现
直接使用页面跳转会带来诸多缺陷,通过路由管理可以降低页面耦合,提高代码的可维护性和权限控制。本篇介绍了 Flutter 的路由管理和拦截实现。
1383 1
Flutter App页面路由及路由拦截实现
|
存储 前端开发
Flutter 实现多选底部弹窗
本篇介绍了底部弹窗实现多选的方式,其中实现的方式还可以有很多种,例如直接在自定义组件中使用有状态组件。这里介绍的方法可以作为一个参考,通过动态构建有状态组件能够简单快速地实现底部弹窗的多选功能。
700 0
Flutter 实现多选底部弹窗
|
前端开发 API Android开发
Flutter实现动画
对于一个前端的App来说,添加适当的动画,可以给用户更好的体验和视觉效果。所以无论是原生的iOS或Android,还是前端开发中都会提供完成某些动画的API。 Flutter有自己的渲染闭环,我们当然可以给它提供一定的数据模型,来让它帮助我们实现对应的动画效果。
208 0
Flutter实现动画
|
前端开发
在Flutter里实现一个开心农场地块布局!Web前端工程师也可以看看,作为Flutter入门。
为避免贴大段代码,文中代码部分仅作为参考,并非全部代码,请理解后自行补全或者下载源码进行学习。
285 0
在Flutter里实现一个开心农场地块布局!Web前端工程师也可以看看,作为Flutter入门。
|
编解码 Dart Java
【Flutter】Flutter 混合开发 ( Flutter 与 Native 通信 | Android 端实现 BasicMessageChannel 通信 )(一)
【Flutter】Flutter 混合开发 ( Flutter 与 Native 通信 | Android 端实现 BasicMessageChannel 通信 )(一)
305 0
【Flutter】Flutter 混合开发 ( Flutter 与 Native 通信 | Android 端实现 BasicMessageChannel 通信 )(一)