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.mzph.cn/pingmian/58639.shtml

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

相关文章

“基金申请精要:国自然基金撰写与SCI发表“

国自然基金项目撰写技巧与ChatGPT融合应用 随着社会经济发展和科技进步&#xff0c;基金项目对创新性的要求越来越高。国家级和省级等各类项目的申请层出不穷&#xff0c;项目书的撰写几乎占据了申请人的全年时间。申请人既需要提出独特且有前瞻性的研究问题&#xff0c;具备突…

java的依赖注入

java的依赖注入是个什么东西&#xff1f; 计算机专业相关知识2024-08-07 17:26河北 摘要 •帮你速读文章内容 java中的依赖注入&#xff08;Dependency Injection, DI&#xff09;是一种软件设计模式&#xff0c;用于减少代码间的耦合度。它允许一个对象&#xff08;被依赖的…

别再为质量问题头疼了,六西格玛黑带培训来救场!

六西格玛&#xff0c;这一源自摩托罗拉的先进质量管理理念&#xff0c;以其严谨的数据分析、持续的流程改进和追求卓越的核心价值&#xff0c;在全球范围内赢得了广泛的认可与应用。而六西格玛黑带&#xff0c;作为这一体系中的精英&#xff0c;不仅掌握了深厚的统计学知识&…

深度学习基础知识-Batch Normalization(BN)超详细解析

一、背景和问题定义 在深层神经网络&#xff08;Deep Neural Networks, DNNs&#xff09;中&#xff0c;层与层之间的输入分布会随着参数更新不断发生变化&#xff0c;这种现象被称为内部协变量偏移&#xff08;Internal Covariate Shift&#xff09;。具体来说&#xff0c;由…

优雅的LUA数据记录方法-serpent序列化+LUA Table

目录 简述如何集成&#xff1f;如何使用序列化 反序列化 参考 简述 项目里需要使用LUA脚本将数据记录到文件&#xff0c;要方便的增加、查找、删除&#xff0c;要方便的加载与存回。 使用序列化/反序列化 lua table可以很容易实现这些功能。 序列化将table转换为字符串 反序列…

2024双11海外购物新选择,逆向代购商家的营销利器

各大电商平台早已蓄势待发&#xff0c;迎接双11这场年度消费盛宴。与往年相比&#xff0c;今年的双11筹备工作启动得更早&#xff0c;国庆假期刚一结束&#xff0c;各大平台便进入了紧张的筹备阶段。对于遍布全球的海外购物者而言&#xff0c;无论是热衷于探索中国文化的外国人…

Cyber​​Panel upgrademysqlstatus 远程命令执行漏洞(QVD-2024-44346)

0x01 产品简介 CyberPanel是一个开源的Web控制面板,它提供了一个用户友好的界面,用于管理网站、电子邮件、数据库、FTP账户等。CyberPanel旨在简化网站管理任务,使非技术用户也能轻松管理自己的在线资源。 0x02 漏洞概述 该漏洞源于upgrademysqlstatus接口未做身份验证和…

【万户软件-注册安全分析报告-无验证方式导致安全隐患】

前言 由于网站注册入口容易被黑客攻击&#xff0c;存在如下安全问题&#xff1a; 1. 暴力破解密码&#xff0c;造成用户信息泄露 2. 短信盗刷的安全问题&#xff0c;影响业务及导致用户投诉 3. 带来经济损失&#xff0c;尤其是后付费客户&#xff0c;风险巨大&#xff0c;造…

大舍传媒:海外发稿传统新闻媒体发布新闻稿与门户媒体宣发的区别?

大舍传媒&#xff1a;海外发稿传统新闻媒体发布新闻稿与门户媒体宣发的区别&#xff1f; 在当今全球化的信息时代&#xff0c;新闻报道的传播渠道日益多元化&#xff0c;企业和个人在进行海外发稿时&#xff0c;往往面临着在传统新闻媒体和门户媒体之间的选择。那么&#xff0…

【在Linux世界中追寻伟大的One Piece】Socket编程TCP

目录 1 -> TCP socket API 2 -> V1 -Echo Server 2.1 -> 测试多个连接的情况 1 -> TCP socket API socket()&#xff1a; socket()打开一个网络通讯端口&#xff0c;如果成功的话&#xff0c;就像open()一样返回一个文件描述符。应用程序可以像读写文件一样用r…

NCCL安装(Ubuntu等)

目录 一、NCCL的定义二、安装NCCL的原因1、加速多GPU通信2、支持流行的深度学习框架3、提高计算效率4、易于使用和集成5、可扩展性 三、NCCL安装方法1、下载安装包2、更新APT数据库3、使用APT安装libnccl2包&#xff0c;另外&#xff0c;如果需要使用NCCL编译应用程序&#xff…

PostgreSQL的学习心得和知识总结(一百五十七)|新的 COPY 选项 LOG_VERBOSITY

目录结构 注&#xff1a;提前言明 本文借鉴了以下博主、书籍或网站的内容&#xff0c;其列表如下&#xff1a; 1、参考书籍&#xff1a;《PostgreSQL数据库内核分析》 2、参考书籍&#xff1a;《数据库事务处理的艺术&#xff1a;事务管理与并发控制》 3、PostgreSQL数据库仓库…

Spring Boot 配置文件详解与最佳实践

目录 前言1. 配置文件的作用2. Spring Boot 主要配置内容2.1 Actuator 配置2.2 缓存配置2.3 核心配置2.4 数据库与数据迁移配置2.5 开发工具配置2.6 Docker Compose 配置2.7 JSON 配置2.8 安全配置 3. 多个配置文件的处理方法3.1 使用 Profile 文件区分环境3.2 结合优先级加载配…

【05-多处理器编程入门到放弃】课堂代码调试

lecture05是并发的第一节课。主要讲了入门&#xff08;两个API&#xff0c;create和join&#xff09;和放弃&#xff08;原来很自然的串行想法&#xff09; 并发线程模型最小线程库线程自问自答11思考题&#xff1a;3个T_sum线程&#xff0c;sum的结果最小是多少&#xff1f;补…

探索Python文档自动化的奥秘:揭开docxtpl库的神秘面纱

文章目录 探索Python文档自动化的奥秘&#xff1a;揭开docxtpl库的神秘面纱1. 背景介绍2. 库简介3. 安装指南4. 基础函数介绍5. 实际应用场景6. 常见问题及解决方案7. 总结 探索Python文档自动化的奥秘&#xff1a;揭开docxtpl库的神秘面纱 1. 背景介绍 在日常工作中&#xf…

Web大学生网页作业成品——家乡广州介绍设计与实现(HTML+CSS)(5个页面)

&#x1f389;&#x1f389;&#x1f389; 常见网页设计作业题材有**汽车、环保、明星、文化、国家、抗疫、景点、人物、体育、植物、公益、图书、节日、游戏、商城、旅游、家乡、学校、电影、动漫、非遗、动物、个人、企业、美食、婚纱、其他**等网页设计题目, 可满足大学生网…

【信息安全设计】系统安全设计方案,系统安全保护设施设计实施方案(Word原件)

1.1 总体设计 1.1.1 设计原则 1.2 物理层安全 1.2.1 机房建设安全 1.2.2 电气安全特性 1.2.3 设备安全 1.2.4 介质安全措施 1.3 网络层安全 1.3.1 网络结构安全 1.3.2 划分子网络 1.3.3 异常流量管理 1.3.4 网络安全审计 1.3.5 网络访问控制 1.3.6 完整性检查 1.…

【 纷享销客-注册安全分析报告-无验证方式导致安全隐患】

前言 由于网站注册入口容易被黑客攻击&#xff0c;存在如下安全问题&#xff1a; 1. 暴力破解密码&#xff0c;造成用户信息泄露 2. 短信盗刷的安全问题&#xff0c;影响业务及导致用户投诉 3. 带来经济损失&#xff0c;尤其是后付费客户&#xff0c;风险巨大&#xff0c;造…

Halcon-模板匹配(WPF)

halcon的代码 dev_open_window (0, 0, 512, 512, black, WindowHandle) read_image (Image, C:/Users/CF/Desktop/image.jpg) dev_display (Image)draw_rectangle1 (WindowHandle, Row1, Column1, Row2, Column2) gen_rectangle1 (Rectangle, Row1, Column1, Row2, Column2) r…