Flutter-Engine 的定制实践:Text 绘制流程浅析及自定义underline的间距

前言

最近工作中处理的文本相关的内容较多,不论是刁钻的需求还是复杂的问题,最终都会引向一点“Flutter中的文本是如何绘制的?”。 这里我将以“调整下划线与文字的间距”为切入点并结合自定义Engine,记录一下我的个人分析和实践的结果,希望对各位有帮助,如有错误还望指出。

Text Widget

带下划线文本的显示

Flutter中,显示一行带有下划线的文本的代码如下:

Text('Flutter Demo Home Page', style: TextStyle(decoration: TextDecoration.underline //下划线),
)

其展示效果如下图:

在这里插入图片描述

为了调整下划线的间距,我们需要分析Text的实现原理。

Framework侧的实现原理浅析

为了便于大家对后文梳理过程有一个结构上的理解,先在此贴一下flutter架构图。

在这里插入图片描述

结构梳理

打开Text文件,可以看到其内部将文案转为一个TextSpan并作为参数传递给RichText

@override
Widget build(BuildContext context) {//...省略无关代码result = RichText(///...各种配置参数text: TextSpan(style: effectiveTextStyle,text: data,children: textSpan != null ? <InlineSpan>[textSpan!] : null,),);return result;
}

进一步查看RichiText源码可以发现其继承MultiChildRenderObjectWidget,内部通过createRenderObject()函数创建了RenderParagraph并将TextSpan传入其内,

class RichText extends MultiChildRenderObjectWidget {///...省略代码@overrideRenderParagraph createRenderObject(BuildContext context) {assert(textDirection != null || debugCheckHasDirectionality(context));return RenderParagraph(text,textAlign: textAlign,textDirection: textDirection ?? Directionality.of(context),softWrap: softWrap,overflow: overflow,textScaler: textScaler,maxLines: maxLines,strutStyle: strutStyle,textWidthBasis: textWidthBasis,textHeightBehavior: textHeightBehavior,locale: locale ?? Localizations.maybeLocaleOf(context),registrar: selectionRegistrar,selectionColor: selectionColor,);}
}

此类初始化的同时会创建一个TextPinter对(此对象非常重要,连接着文字的布局和绘制)。

  RenderParagraph(InlineSpan text, {///...各种配置参数}) : ///...省略无关代码_textPainter = TextPainter(text: text,textAlign: textAlign,textDirection: textDirection,textScaler: textScaler == TextScaler.noScaling ? TextScaler.linear(textScaleFactor) : textScaler,maxLines: maxLines,ellipsis: overflow == TextOverflow.ellipsis ? _kEllipsis : null,locale: locale,strutStyle: strutStyle,textWidthBasis: textWidthBasis,textHeightBehavior: textHeightBehavior,) {addAll(children);this.registrar = registrar;}

通过对RichTextTextSpan以及RenderParagraph的分析可发现,TextSpan的父类是InlineSpan,其另一实现是PlaceHolderSpan(WidgetSpan便是它的子类),而RichText则是将两者聚合起来交由RenderParagraph处理相关的点击,layout,paint等操作,如下图:

在这里插入图片描述

绘制流程浅析

我们进一步观察RenderParagraph的两个主要函数performLayout()paint(),发现它们最终都会转到调用textPainter的相关函数:


void _layoutTextWithConstraints(BoxConstraints constraints) {_textPainter..setPlaceholderDimensions(_placeholderDimensions)..layout(minWidth: constraints.minWidth, maxWidth: _adjustMaxWidth(constraints.maxWidth));
}@override
void performLayout() {//...省略代码_layoutTextWithConstraints(constraints);positionInlineChildren(_textPainter.inlinePlaceholderBoxes!);//...省略代码
}
@override
void paint(PaintingContext context, Offset offset) {//...省略代码_textPainter.paint(context.canvas, offset);//....
}

textPainter中,无论是布局还是绘制在对相关配置做了简单的调整和初始化后,便会进一步初始化并调用ui.Paragraph的相关函数。我们继续跟进ui.Paragraph这个类,发现它只是一层接口,具体实现是_NativeParagraph,而该类则代理的是engineParagraph的接口。

    abstract class Paragraph {///...}base class _NativeParagraph extends NativeFieldWrapperClass1 implements Paragraph {//此类由engine创建,并关联对应接口,如 layout函数@overridevoid layout(ParagraphConstraints constraints) {//framework转到engine侧_layout(constraints.width);assert(() {_needsLayout = false;return true;}());}//对应engine侧的接口@Native<Void Function(Pointer<Void>, Double)>(symbol: 'Paragraph::layout', isLeaf: true)external void _layout(double width);//...省略部分代码}

通过以上的梳理,我们可以得到一张大致的调用链:

在这里插入图片描述

至此framework层的使命便结束了,回顾整个流程,可以发现该层的实现很简单,多数是配置和初始化的操作,不涉及到具体的布局和绘制操作,接下来我们转到Engine

Engine

获取和编译源码

为了继续我们在engine的分析和调试,首先我们需要拥有engine源码和编译能力,由于这并不是本篇重点以及网络上已有大量前辈提供了相关文章,所以这里我仅对流程做简单介绍,并在文末贴出相关参考文章。

首先,你最好有个梯子xD,其次你需要下载配置谷歌的depot_tools,它用于负责管理flutter工程的依赖。之后在这个路径:你的flutter-sdk路径/bin/interval/engine.version,查看你当前flutter对应的engine版本号,如我的是:db49896cf25ceabc44096d5f088d86414e05a7aa

然后创建一个文件夹,并在此文件夹内运行fetch flutter拉取工程代码。拉取完成后,通过git切换分支到上面的那个版本号,再使用gclient sync 同步依赖,这样你就获取到了所有源码。

Setting-up-the-Engine-development-environment.

有了源码后,通过gnninja,你就可以进行编译了,例如我要编译android端的产物,就相继运行如下指令:

./flutter/tools/gn --android --android-cpu arm64 --unoptimized --no-stripped ./flutter/tools/gn --unoptimized --mac-cpu arm64 ninja -C out/android_debug_unopt_arm64 & ninja -C out/host_debug_unopt_arm64

至此,我们的准备工作就完成了,接着我们前面的分析。

绘制流程分析

_NativeParagraph代理的是engine../txt/paragraph.h这个类的接口,而从该类的注释可发现,此类也是一个接口:


// Interface for text layout engines.  The current implementation is based on
// Skia's SkShaper/SkParagraph text layout module.
class Paragraph {//...省略代码
}

注释中也提到了,实现是基于Skia's SkShaper/SkParagraph的模块,通过这条线索,我们在../skparagaraph/src/ParagraphImpl.cpp路径上找到了相关实现类,我们在它的绘制函数中增加输出日志以已确定判断是否正确:

提示: engine是一个极其庞大的工程,切勿在里面盲目瞎转,多看注释或者开断点调试。

void ParagraphImpl::paint(ParagraphPainter* painter, SkScalar x, SkScalar y) {//输出一个标记日志FML_LOG(ERROR) << "custom engine ::paint painter";for (auto& line : fLines) {line.paint(painter, x, y);}
}

engine重新编译,并运行demo 获得如下输出日志:

在这里插入图片描述

由此可以证明,我们找到的位置是正确的。绘制函数内的具体绘制则被拆分到TextLine.cpp中,

void ParagraphImpl::paint(ParagraphPainter* painter, SkScalar x, SkScalar y) {for (auto& line : fLines) {line.paint(painter, x, y);}
}

不过这个不是我们此文的目标,回到我们的问题调整下划线和文字的间距来。我们在同目录下可以找到Decorations.cpp文件,并定位到paint()函数,会发现其内部对AllTextDecorations进行了遍历,并绘制对应的装饰

void Decorations::paint(ParagraphPainter* painter, const TextStyle& textStyle, const TextLine::ClipContext& context, SkScalar baseline) {if (textStyle.getDecorationType() == TextDecoration::kNoDecoration) {return;}// Get thickness and positioncalculateThickness(textStyle, context.run->font().refTypeface());for (auto decoration : AllTextDecorations) {if ((textStyle.getDecorationType() & decoration) == 0) {continue;}calculatePosition(decoration,decoration == TextDecoration::kOverline? context.run->correctAscent() - context.run->ascent(): context.run->correctAscent());calculatePaint(textStyle);auto width = context.clip.width();SkScalar x = context.clip.left();SkScalar y = context.clip.top() + fPosition;bool drawGaps = textStyle.getDecorationMode() == TextDecorationMode::kGaps &&textStyle.getDecorationType() == TextDecoration::kUnderline;//根据装饰类型,进行绘制switch (textStyle.getDecorationStyle()) {case TextDecorationStyle::kWavy: {calculateWaves(textStyle, context.clip);fPath.offset(x, y);painter->drawPath(fPath, fDecorStyle);break;}case TextDecorationStyle::kDouble: {SkScalar bottom = y + kDoubleDecorationSpacing;if (drawGaps) {SkScalar left = x - context.fTextShift;painter->translate(context.fTextShift, 0);calculateGaps(context, SkRect::MakeXYWH(left, y, width, fThickness), baseline, fThickness);painter->drawPath(fPath, fDecorStyle);calculateGaps(context, SkRect::MakeXYWH(left, bottom, width, fThickness), baseline, fThickness);painter->drawPath(fPath, fDecorStyle);} else {draw_line_as_rect(painter, x,      y, width, fDecorStyle);draw_line_as_rect(painter, x, bottom, width, fDecorStyle);}break;}case TextDecorationStyle::kDashed:case TextDecorationStyle::kDotted:if (drawGaps) {SkScalar left = x - context.fTextShift;painter->translate(context.fTextShift, 0);calculateGaps(context, SkRect::MakeXYWH(left, y, width, fThickness), baseline, 0);painter->drawPath(fPath, fDecorStyle);} else {painter->drawLine(x, y, x + width, y, fDecorStyle);}break;case TextDecorationStyle::kSolid:if (drawGaps) {SkScalar left = x - context.fTextShift;painter->translate(context.fTextShift, 0);calculateGaps(context, SkRect::MakeXYWH(left, y, width, fThickness), baseline, fThickness);painter->drawPath(fPath, fDecorStyle);} else {//这里是绘制进行下划线的绘制draw_line_as_rect(painter, x, y, width, fDecorStyle);}break;default:break;}}
}

而我们要找的则是case TextDecorationStyle::kSolid枚举下的draw_line_as_rect函数,其通过x、y和width在文字下方绘制出一条横线,这里我们将y值+10.0看一下效果:

draw_line_as_rect(painter, x, y + 10.0, width, fDecorStyle);

运行结果如下图:

在这里插入图片描述

再贴一下原图:

在这里插入图片描述

可以看到,相较于原图,下划线与文字的间隙发生了预期的变化。

至此,本文的目标已经完成,谢谢大家的阅读。

参考文章

Setting-up-the-Engine-development-environment

Flutter Engine 源码调试

Compiling-the-engine.

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/461671.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

[A-14]ARMv8/ARMv9-Memory-内存模型的类型(Device Normal)

ver0.1 [看前序文章有惊喜。] 前言 前面花了很大的精力把ARM构建的VMSA中的几个核心的议题给大家做了介绍,相信大家已经能够理解并掌握ARM的内存子系统的工作原理大致框架。接下来我们会规划一些文章,对ARM内存子系统的一些细节做一下介绍,使ARM的内存子系统更加的丰满。本…

可编辑31页PPT | 智慧业务中台规划建设与应用总体方案

荐言分享&#xff1a;随着数字化转型的深入&#xff0c;企业面临着前所未有的挑战与机遇。为了高效整合内外部资源&#xff0c;快速响应市场变化&#xff0c;提升业务创新能力&#xff0c;智慧业务中台应运而生。智慧业务中台作为企业数字化转型的核心基础设施&#xff0c;旨在…

深入理解Docker,从入门到精通-Part1(基础使用)

一、Docker基本概念 Docker架构 基本组件的介绍 Docker Client 是用户界面&#xff0c;它支持用户与Docker Daemon之间通信 Docker Daemon Docker最核心的后台进程&#xff0c;运行于主机上&#xff0c;处理服务请求 Docker registry是中央registry&#xff0c;支持拥有公有与…

在macOS的多任务处理环境中,如何平衡应用的性能与用户体验?这是否是一个复杂的优化问题?如何优化用户体验|多任务处理|用户体验|应用设计

目录 一 多任务处理与应用性能 1. macOS中的多任务处理机制 2. 性能优化的基本策略 二 用户体验的关键要素 1. 响应速度 2. 界面友好性 3. 功能的直观性 三 平衡性能与用户体验的策略 1. 资源管理 2. 优化数据加载 3. 使用合适的线程模型 4. 实时监测和调整 四 使…

lvm逻辑卷管理

分区类型&#xff1a; 主分区扩展分区逻辑分区系统引导分区&#xff1a;存放系统的引导文件和linux的内核文件swap分区&#xff1a;交换分区&#xff0c;系统的物理内存不足时&#xff0c;从一些长时间未运行的程序当中释放一部分内存&#xff0c;释放出来的内存保存到swap分区…

openai api 文件分析/联网/画图代码示例

目的 使用https://4o.zhangsan.shop的API进行文件分析等功能。 完整代码 # pip install openai0.28 # 注意下方代码必须使用该版本 import openaidef query_gpt4(question):openai.api_key "sk-aQR1wbTsLpySgJDq3fFb026c225a44C8924750C1B67bCeD5"openai.api_ba…

Android编译环境构建(二)(可用于物理机、虚拟机、容器化Jenkins环境)

文章目录 需求环境要求文件下载Gradle Version:7.5cmdline-tools至此普通物理环境的Android编译环境已部署完毕 部署maven(可选)Jenkins配置Android构建环境 说明&#xff1a; 物理环境&#xff1a;物理机、虚拟机等 容器化环境&#xff1a;docker等 需求 Gradle Version:7.5 …

WPF+MVVM案例实战(十)- 水波纹按钮实现与控件封装

文章目录 1、运行效果1、封装用户控件1、创建文件2、依赖属性实现2、使用封装的按钮控件1.主界面引用2.按钮属性设置3 总结1、运行效果 1、封装用户控件 1、创建文件 打开 Wpf_Examples 项目,在 UserControlLib 用户控件库中创建按钮文件 WaterRipplesButton.xaml ,修改 Us…

Spring Boot解决 406 错误之返回对象缺少Getter/Setter方法引发的问题

目录 前言1. 问题背景2. 问题分析2.1 检查返回对象 3. 解决方案3.1 确保Controller返回Result类型3.2 测试接口响应 4. 原理探讨5. 常见问题排查与优化建议结语 前言 在Spring Boot开发中&#xff0c;接口请求返回数据是系统交互的重要环节&#xff0c;尤其在开发RESTful风格的…

FineReport 单元格的特殊应用场景

1、实现鼠标点击的行变色 创建报表 1.1、鼠标点击某行时该行高亮显示 JavaScript 代码如下&#xff1a; _g().addEffect(highlightRow, {color: red,trigger: mousedown, });结果 1.2、鼠标悬浮某行时该行变色&#xff0c;离开时恢复 其他一样&#xff0c;就改代码 JavaScr…

MacOS的powermetrics命令查看macbook笔记本的耗能情况,附带查看ANE的工作情况

什么是 powermetrics&#xff1f; powermetrics 是 macOS 系统自带的一个命令行工具&#xff0c;用于收集和分析系统能源消耗数据。通过它&#xff0c;我们可以深入了解 Mac 的硬件性能、软件行为以及能源使用情况&#xff0c;从而优化系统配置&#xff0c;提高电池续航时间。…

系统架构师-一文搞定架构风格

架构风格分类 五大架构风格简介子风格数据流风格面向数据流&#xff0c;按照一定的顺序从前向后执行程序批处理、管道-过滤器调用/返回风格构件与构件之间存在相互调用的关系&#xff0c;一般是显示的调用主程序/子程序、面向对象、层次结构&#xff08;层次型架构风格&#x…

第13课 数据处理

数轴是一维的&#xff0c;平面直角坐标系是二维的。单个学生的成绩是一维的&#xff0c;全班同学的成绩是二维的。 Python是强大的数据处理工具&#xff0c;可以处理多种数据文件。最基础的数据文件包括一维数据、二维数据、CSV格式数据文件。 这节课重点学习一维数据、二维数据…

3D人体建模的前沿探索:细数主流模型与技术进展

文章目录 一、前言二、主要内容SMPL文献内容&#xff1a;文献信息&#xff1a; SMPLX文献内容&#xff1a;文献信息&#xff1a; STAR文献信息&#xff1a; SCAPE文献内容&#xff1a;文献信息&#xff1a; BfSNet3. 文献内容&#xff1a; SMPLR文献内容&#xff1a;文献信息&a…

闪存学习_1:Flash-Aware Computing from Jihong Kim

闪存学习_1&#xff1a;Flash-Aware Computing from Jihong Kim 前言一、Storage Media&#xff1a;NAND Flash Memory1、概念2、编程和擦除操作3、读操作4、异地更新操作&#xff08;Out-Place Update&#xff09;5、数据可靠性6、闪存控制器&#xff08;SSD主控&#xff09;7…

【真题笔记】15年系统架构设计师要点总结

【真题笔记】15年系统架构设计师要点总结 分布式数据库中各种透明RAID 5IPv6 IPv4电子商务系统项目配置管理IPO图&#xff08;输入加工输出图&#xff09;桥接模式的UML图面向对象设计原则软件测试 在15年真题练习中&#xff0c;对错题模棱两可的考点进行重点记录与内容延申。…

软件测试基础知识总结

&#x1f345; 点击文末小卡片 &#xff0c;免费获取软件测试全套资料&#xff0c;资料在手&#xff0c;涨薪更快 基础篇 1. 什么是软件测试&#xff1f; 软件测试&#xff08;Software Testing&#xff09;的经典定义是&#xff1a;在规定的条件下对程序进行操作&#xff…

「微服务」持续测试如何做?

如今&#xff0c;软件开发对于速度和灵活性的持续追求&#xff0c;催生了各种超越传统界限的方法和实践。而作为现代 DevOps 实践的基石&#xff0c;持续测试的出现与发展&#xff0c;正好满足了加速软件交付的需求。下面&#xff0c;我将和您探讨持续测试的最新发展&#xff0…

智能家居10G雷达感应开关模块,飞睿智能uA级别低功耗、超高灵敏度,瞬间响应快

在当今科技飞速发展的时代&#xff0c;智能家居已经逐渐成为人们生活中不可或缺的一部分。从智能灯光控制到智能家电的联动&#xff0c;每一个细节都在为我们的生活带来便利和舒适。而在众多智能家居产品中&#xff0c;10G 雷达感应开关模块以其独特的优势&#xff0c;正逐渐成…

中国大学慕课视频资源分析

右键查看视频信息 关注点在 urls 这个参数&#xff0c;仔细分析就会发现其实是由若干个.ts拓展名和一个.m3u8拓展名的视频文件&#xff0c;每一个.ts视频文件的时长在10秒钟左右。 中国大学MOOC将课程的视频文件拆分成若干个这样的.ts片段&#xff0c;并且用.m3u8记录这些片段…