前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Flutter文字渲染模块总结(一)

Flutter文字渲染模块总结(一)

作者头像
用户9172902
发布2021-11-12 18:17:03
1.1K0
发布2021-11-12 18:17:03
举报
文章被收录于专栏:移动端动态化技术

1.文字渲染概述

1.1 字体存储

​ 把文字渲染到屏幕上主要是通过加载字体获得字形(Glyph)纹理,然后通过字体测量计算出字体左上角的位置和宽高,然后再把纹理贴到2D方块中。字体的存储主要有两种方式:

其一是位图字体,这是比较早起的纹理存储方式,主要是把字形存储到一张大纹理中,然后加载字体的时候主要是加载这张大纹理,如下图所示:

​ 这种方式的优点就是,字体被预先渲染好 ,效果会很高,但是你的程序会被限制在一个固定的分辨率,如果你对这些文字进行放大的话你会看到文字的像素边缘。每次想使用不同的字体时,你不得不重新生成位图字体。

​ 另一种更加灵活的方式就是矢量字体,其主要是通过一些数学公式(贝塞尔曲线),类似于矢量图像,根据需要的字体大小来生成纹理,可以很好的适配不同的分辨率,而没有任何质量损失。比如现在用的比较多的TrueType,这这方式字体加载就是将字形矢量路径绘制出来,得到字形对应的纹理,如下图所示:

​ 在渲染时,会动态生成需要用到的字符的字形位图并缓存起来,不同字号的字符需要不同的位图。这样字形的解析和渲染就会非常耗时,一般都会通过缓存机制进行优化, 比如Skia的文字绘制有两种方式:

  1. 文字绘制过程需要将文字解析为路径,然后绘制路径,缓存路径
  2. 将文字解析为Mask(32*32的A8图片),然后绘制模板,缓存模板

1.2. 渲染过程

​ 有了纹理,还需要确定文字方块的位置和大小信息,这些信息主要是通过字形的metrics信息来确定的,字形的metrics信息在文字排版的时候也会用到,主要的参数如下图所示:

当我们需要绘制一个字形的时候,首先需要找到左上角的的坐标,计算方式如下所示:

代码语言:javascript
复制
float xpos = x + bearingX * scale;
float ypos = y - (size.height - bearingY) * scale;
float width = size.width * scale;
float height = size.height * scale;

`

可以看出g会渲染到baseline以下的位置,所以height和bearingY不相等,这样y坐标就会往下移。比如渲染如下文字

它的方块信息如下所示:

2. Flutter文字渲染模块

Flutter文字渲染相关的模块比较核心的主要有包含两种种类型:

  • 支持混排的富文本RichText
  • 支持编辑的EditableText

2.1 RichText组件

RichText可以实现不同风格的Text放到一起渲染,还可支持图文混排,可以看一下它的用法:

​ 可以看到RichText主要是通过串联不同InlineSpan,实现不同风格的文字或者图文混排效果,目前InlineSpan主要包括两种,TextSpan和PlaceHolderSpan,继承关系如下图所示:

​ WidgetSpan继承至PlaceholderSpan,PlaceholderSpan会在文字排版的时候作为占位符参与排版,所以WidgetSpan可以在排版完之后得到准确的位置信息,将字节点绘制到正确的位置。

​ RichText继承至MultiChildRenderObjectWidget,对应的RenderObject是RenderParagraph,RenderParagraph最核心的两个逻辑主要是排版和渲染,而RenderParagraph的Layout和Paint过程最终都会调用到TextPainter对象,然后再由TextPainter触发Engine层最终的排版和渲染,整体框架如下图所示:

2.1.1 Layout

Layout主要分成三步:

  1. LayoutChildren

这个过程主要是收集PlaceholderSpan节点的信息,后面Text的排版过程需要用到

  1. LayoutText

这一步主要是文字排版,首先需要把刚才的placeholder信息更新到TextPainter

代码语言:javascript
复制
//render_paragraph.dart
void _layoutTextWithConstraints(BoxConstraints constraints) {
  _textPainter.setPlaceholderDimensions(_placeholderDimensions);
  _layoutText(minWidth: constraints.minWidth, maxWidth: constraints.maxWidth);
}

然后再看具体的TextPainter里面LayoutText过程:

  1. SetParentData

这里主要是把上一步排版后的placeholder的box信息更新的对应的child节点,主要需要计算offset和scale信息

2.1.2 Paint

Paint过程主要包括两个部分,文字渲染和占位Widget渲染,还有前后的裁剪处理,下面只贴出渲染部分

​ textPainter的paint的方法就是直接调canvas.drawParagraph,然后就是渲染占位的child,这里会同意用一个TransformLayer包一层,进行排版结果的变换,主要包括offset和scale信息,然后传人闭包里面绘制各自的child。

2.1.3 标脏逻辑

​ 由于文字的排版和渲染是个非常耗时的过程,不可能每一帧都重新执行Layout和Paint,而Flutter本身也会有针对重新排版和绘制的优化方式,所以可以通过控制Layout和Paint的标脏逻辑来进行优化。Flutter关于文字的标脏逻辑主要是通过在更新文字信息的时候进行比较,通过不同的比较结果确定是否下一帧的动作,比较结果主要有以下几种情况:

代码语言:javascript
复制
enum RenderComparison {
  /// The two objects are identical (meaning deeply equal, not necessarily  
  /// [dart:core.identical]).
  identical,

  /// The two objects are identical for the purpose of layout, but may be different
  /// in other ways.
  ///
  /// For example, maybe some event handlers changed.
  metadata,

  /// The two objects are different but only in ways that affect paint, not layout.
  ///
  /// For example, only the color is changed.
  ///
  /// [RenderObject.markNeedsPaint] would be necessary to handle this kind of
  /// change in a render object.
  paint,

  /// The two objects are different in ways that affect layout (and therefore paint).
  ///
  /// For example, the size is changed.
  ///
  /// This is the most drastic level of change possible.
  ///
  /// [RenderObject.markNeedsLayout] would be necessary to handle this kind of
  /// change in a render object.
  layout,
}

​ 可以看出这四种情况变化的剧烈层度也是逐渐增加的,前两个结果对于RenderObject都是无变化,后面两个一个是需要重新绘制,一个是需要重新排版,当然重新排版意味着重新绘制。来看一下它的比较过程:

中间比较两个style变化,不同的变化会产生不同的结果,比较过程如下图所示:

比如如果只是颜色信息的更改则只需要重新绘制,如果是其它字体信息的变更,则可能需要重新排版。

​ 可以看到如果只是颜色或者装饰的修改,只需要重绘即可,而如果是其它,比如字体大小,字体类型的变更则需要重新排版。通过上述标脏逻辑来实现渲染和排版的优化。

2.2 EditableText组件

​ Flutter的EditableTextWidget组件可能是所有Widget中最复杂的一个组件,包含了手势和键盘的交互,以及文本的编辑。先看一下其核心模块排版和渲染过程,不过因为EditableText不支持富文本的方式排版,所以其排版过程只是单纯的文字版本,所以只需要关注渲染这一块,当然还有交互。

2.2.1 Paint

这里面内容绘制主要包括四个部分,如下图所示:

  1. Caret绘制

光标绘制核心主要是坐标的计算,通过手势转换成文字排版的字型坐标,然后生成rect信息,最后结合alpha动画可以实现光标的闪烁。

  1. Selection绘制

选中区域的绘制核心也是手势交互的时候计算出字形Selection区域,然后找到selection对于的box进行绘制即可

  1. Text绘制

canvas.drawParagraph(_paragraph, offset)

  1. FloatingCaret

这里主要是也是光标Rect的计算,会根据Drag事件实时调整位置。

2.2.2 交互

  1. 手势识别

手势识别主要有两种:

一是Tap获取光标的位置,这一步需要将touch的屏幕坐标转换到字形的坐标,这里面代码比较复杂先不展示,计算步骤主要分如下几步:

1.根据Tap位置计算glyph坐标,需要基于排版结果

2.从当前glyph坐标向前或者向后搜索,找到第一个TextBox

3.根据TextBox左上角的坐标生成光标Rect,再绘制

二是LongPress获取选中区域,这一步主要是根据touch的屏幕坐标找到最近的一个单词(如果是英文),也需要基于排版信息。

核心逻辑主要在Engine层的文字排版。

  1. 键盘输入

3. 目前存在的问题

  1. 不可以同时支持编辑和图文混排

RichText只支持图文混排,不知道编辑;EditableText只支持编辑,不知道混排,目前官方并没有一个组件即可支持编辑,同时也可以支持混排

​ 主要是因为EdtiableText支持对应的RenderObject只普通TextSpan的输入,如果要支持混排则需要加入WidgetSpan,通过魔改一下,其实应该是可以做到编辑加混排,需要改一下Layout和Paint过程,当然配套的插件也需要更改,在我准备去尝试的时候,发现已经有大佬魔改出一个版本,有兴趣的可以试试。

https://github.com/Fearimdly/rich_text_field

  1. 缺乏一些更底层(low level)的接口

Flutter目前很多LibTxt的接口并没有开放出来,比如类似Android的LineBreaker,目前只支持整体段落的排版,所有有些效果没办法高效实现。

比如:

用文本填充非矩形形状

在非线性路径上书写文本

Android有drawTextOnPath这样的接口可以实现,Skia也提供了这样的接口,但目前Flutter并未开放出来。

​ 另外如果一个段落中每个字符都有一个固定的坐标,这种情况下Flutter要实现只能是为每一个字符都提供一个TextPainter,执行Layout和Paint,这样如果文字较多势必会非常耗时,官方类似这样的Issues讨论有很多,目前还是有很多问题亟待解决。

https://github.com/flutter/flutter/issues/35994

https://github.com/flutter/flutter/issues/16477

最后:上述所有内容只涉及到Framwork层的代码,但其实核心的文字排版和渲染的实现都在Engine层,大概看了一下,排版过程没怎么看懂,目前正在配置Engine调试,等后面通过Debug搞懂真正的排版逻辑再更新。

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2020-12-30,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 1.文字渲染概述
  • 1.1 字体存储
  • 1.2. 渲染过程
  • 2. Flutter文字渲染模块
  • 2.1 RichText组件
  • 2.1.1 Layout
  • 2.1.2 Paint
  • 2.1.3 标脏逻辑
  • 2.2 EditableText组件
  • 2.2.1 Paint
  • 2.2.2 交互
  • 3. 目前存在的问题
相关产品与服务
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档