一、说明
要实现flutter 编辑器的两边对齐,需要修改flutter engine层,flutter engine层修改需要自己编译flutter engine.不仅如此,因为flutter engine层东西多且杂,需要debug调试.
二、Flutter Engine 的编译
Flutter engine 编译官网
https://github.com/flutter/flutter/wiki/Setting-up-the-Engine-development-environment
只看官网有些蒙的,还有下面三个可以参考
https://www.jianshu.com/p/b0eaae0a9a90
https://jsshou.cn/blog/flutter/Flutter%E5%BC%95%E6%93%8E%E7%BC%96%E8%AF%91%E4%B8%8E%E8%B0%83%E8%AF%95.html#%E4%B8%8B%E8%BD%BD%E6%BA%90%E7%A0%81
https://www.jianshu.com/p/510c26c715ad/
其实这个编译我是踩了很多坑的,现在来理一理我的最终的步骤.
A、所需软件支持
- Mac可以编译Android、iOS产物,Linux可以编译Android产物,windows都不能编译(本篇以Mac编译,记录都是以此为前提)
- git工具
- Chromium
depot_tools
安装方式:
1. 选择一个目录例如 /Users/xxx/
git clone https://chromium.googlesource.com/chromium/tools/depot_tools.git
2. 配置环境变量
export PATH=${PATH}:/Users/aozhaoyang/Desktop/depot_tools
- python环境
- curl和unzip工具
- xcode
B、下载源码
1.提前到github上fork engine项目到自己的账号github.com/hc2088/engine.git下
2.创建一个文件夹engine,下载flutter engine 源码
3.在engine文件夹下创建.gclient,然后配置如下:
url 修改为你fork flutter engine后的git clone 地址
cd engine
touch .gclient
solutions = [
{
"managed": False,
"name": "src/flutter",
"url": "git@github.com:flutter/engine.git",
"custom_deps": {},
"deps_file": "DEPS",
"safesync_url": "",
},
]
4.在engine目录下gclient sync,同步flutter engine代码.这个最好是开科学上网.终端的科学上网也需要配置.
sshkey也需要到flutter engine仓库里面配置一下.
5.执行完成之后,看下src目录下是否有如下仓库,没有就手动加一下
git remote add origin https://github.com/flutter/buildroot.git

6.确认flutter engine与flutter 版本是否一致
我是使用的fvm目录下的flutter,对应的路径为/Users/xxx/fvm/versions/3.3.7/bin/internal/engine.version,对应的flutter版本3.3.7,对应的flutter engine是857bd6b74c5eb56151bfafe91e7fa6a82b6fee25
如果当前flutter engine跟flutter对不上,执行回退操作,记住一定是在engine/src/flutter路径下,否则会找不到版本,如下:
git reset --hard 857bd6b74c5eb56151bfafe91e7fa6a82b6fee25
7.重新在engine/src下面执行gclient sync.时间有些长,耐心等待.
C、编译Android/IOS/HOST(桌面端)
按照端和芯片架构类型分别准备构建文件(可以只构建某一个端端一架构)
1.在engine/src目录下执行
android
./flutter/tools/gn --android --unoptimized --android-cpu=arm64
ios
./flutter/tools/gn --unoptimized --ios --mac-cpu=x64 --runtime-mode=debug
host
./flutter/tools/gn --unoptimized
- android:表示编译Android版本
- unoptimized: 表示不优化产物大小,这样会提高编译速度
- android-cpu: 表示打包的Android架构 如想编译macos的
编译产物
android
ninja -C out/android_debug_unopt_arm64
ios
ninja -C out/ios_debug_unopt
host
ninja -C out/host_debug_unopt
问题:目前m1上面不支持Android打包.
解决方案:https://github.com/flutter/flutter/issues/96745
1.下载补丁,在engine下面的flutter 下和depot_tools目录下进行git apply.
2.在engine下进行gclient sync
D、使用本地引擎运行Flutter
1.修改pubspec.yaml文件 在文件中添加下面代码
dependency_overrides:
sky_engine:
path: <FLUTTER_ENGINE_ROOT>/engine/src/out/host_debug_unopt/gen/dart-pkg/sky_engine
2.执行命令
flutter run --local-engine-src-path <FLUTTER_ENGINE_ROOT>/engine/src --local-engine=android_debug_unopt_arm64 -d deviceId
- local-engine-src-path:指定Flutter引擎存储库的路径,也就是src根目录的绝对路径
- local-engine:指定使用哪个引擎版本,比如
android_debug_unopt_arm64
E、debug flutter engine代码
可以使用vsCode,也可以使用xCode,我这是xcode
- 在Genrated.xcconfig(没有这个可以找下Flutter/Flutter-Generated)中加上 内容为
FLUTTER_ROOT=${FlutterSDK 路径}
FLUTTER_APPLICATION_PATH=${Demo工程路径}
FLUTTER_TARGET=${Demo工程路径}/lib/main.dart
FLUTTER_BUILD_DIR=build
SYMROOT=${SOURCE_ROOT}/../build/ios
FLUTTER_FRAMEWORK_DIR=${Flutter_Engine代码路径}/src/out/ios_debug_sim_unopt
FLUTTER_BUILD_NAME=1.0.0
FLUTTER_BUILD_NUMBER=1
FLUTTER_ENGINE=${Flutter_Engine代码路径}
LOCAL_ENGINE=${输出的路径(ios_debug_sim_unopt)}
ARCHS=${支持的架构(arm64)}
然后打上断点就可以调试啦~
三、Flutter Engine 两边对齐修改
思路:根据justify属性作为切入口,发现只要输入空格就可以实现两端对齐,然后修改flutter engine 代码,把计算空格部分代码修改成计算所有字符.
实现
把空格数量计算修改为所有字符的个数计算
计算空格间距修改为计算所有字符间距
算法解释:如果是开头的连续空格,不纳入计算间距,第一个和开头连续空格后的第一个字符也不纳入计算间距.(保持行开头非连续空格第一个字不动)
void TextLine::justify(SkScalar maxWidth) {
// Count words and the extra spaces to spread across the line
// TODO: do it at the line breaking?..
constexpr auto kWhiteSpaceNumOfStart = 2;
size_t allCharNums = 0;
SkScalar textLen = 0;
size_t posOfCharFirst = -1;
size_t firstResult = 0;
this->iterateThroughClustersInGlyphsOrder(
false, false, [&](const Cluster* cluster, bool ghost) {
textLen += cluster->width();
posOfCharFirst++;
if (posOfCharFirst == firstResult && firstResult < kWhiteSpaceNumOfStart &&
cluster->isWhitespaceBreak()) {
firstResult++;
return true;
}
if (posOfCharFirst == 0 || (posOfCharFirst == firstResult)) {
return true;
}
++allCharNums;
return true;
});
if (allCharNums == 0) {
return;
}
SkScalar step = (maxWidth - textLen) / allCharNums;
SkScalar shift = 0;
// Deal with the ghost spaces
auto ghostShift = maxWidth - this->fAdvance.fX;
// Spread the extra whitespaces
size_t posOfCharSecond = -1;
size_t result = 0;
this->iterateThroughClustersInGlyphsOrder(false, true, [&](const Cluster* cluster, bool ghost) {
posOfCharSecond++;
if (ghost) {
if (cluster->run().leftToRight()) {
shiftCluster(cluster, ghostShift, ghostShift);
}
return true;
}
auto prevShift = shift;
if (posOfCharSecond == result && result < kWhiteSpaceNumOfStart &&
cluster->isWhitespaceBreak()) {
result++;
return true;
}
if (posOfCharSecond == 0 || posOfCharSecond == result) {
return true;
}
shift += step;
shiftCluster(cluster, shift, prevShift);
return true;
});
SkAssertResult(nearlyEqual(shift, maxWidth - textLen));
this->fWidthWithSpaces += ghostShift;
this->fAdvance.fX = maxWidth;
}
上面代码可以按照自己的算法做任意排版.
四、如何使用本地打包产物
方案一
一开始以为使用如下命令就可以正常打包了,但是--local-engine只能指定一种架构的CPU,正常我们都是需要支持arm和arm64的,所以此方式不太行.
fvm flutter build apk -t lib/main.dart --local-engine=android_release_arm64 --local-engine-src-path=/Users/qimao/engine/src
方案二
使用编译好的产物,直接放到flutter的sdk的cache的engine中.
尝试了好几次,发现并不行.最后从参考文章作者了解到需要修改flutter.gradle脚本.因为1.12之前是直接通过本地产物编译,后面都是通过远程产物编译了.所以我们需要修改flutter.gradle脚本,变成本地编译.
参考文章
五、两端对齐
编辑器实际上就是一个可以编辑的富文本,在flutter中富文本是通过RichText实现的.可以从RichText的源码入手,首先可以看下它的渲染方法createRenderObject.
@override
RenderParagraph createRenderObject(BuildContext context) {
assert(textDirection != null || debugCheckHasDirectionality(context));
return RenderParagraph(text,
textAlign: textAlign,
textDirection: textDirection ?? Directionality.of(context),
softWrap: softWrap,
overflow: overflow,
textScaleFactor: textScaleFactor,
maxLines: maxLines,
strutStyle: strutStyle,
textWidthBasis: textWidthBasis,
textHeightBehavior: textHeightBehavior,
locale: locale ?? Localizations.maybeLocaleOf(context),
registrar: selectionRegistrar,
selectionColor: selectionColor,
);
}
它的实现是RenderParagraph,继续看它的源码,可以看到它的布局是交给_textPainter实现的.
void _layoutText({ double minWidth = 0.0, double maxWidth = double.infinity }) {
final bool widthMatters = softWrap || overflow == TextOverflow.ellipsis;
_textPainter.layout(
minWidth: minWidth,
maxWidth: widthMatters ?
maxWidth :
double.infinity,
);
}
继续查看的TextPainter的layout方法,它主要是创建了一个ui.Paragraph,通过它来进行布局.ui.Paragraph由ParagraphBuilder生成,通过设置ParagraphBuilder我们可以对文字进行各种TextStyle样式设置,以及设置宽高.其实已经可以控制文字的布局了:通过ui.Paragraph的paint去绘制文字.
void layout({ double minWidth = 0.0, double maxWidth = double.infinity }) {
assert(text != null, 'TextPainter.text must be set to a non-null value before using the TextPainter.');
assert(textDirection != null, 'TextPainter.textDirection must be set to a non-null value before using the TextPainter.');
// Return early if the current layout information is not outdated, even if
// _needsPaint is true (in which case _paragraph will be rebuilt in paint).
if (_paragraph != null && minWidth == _lastMinWidth && maxWidth == _lastMaxWidth) {
return;
}
if (_rebuildParagraphForPaint || _paragraph == null) {
_createParagraph();
}
_lastMinWidth = minWidth;
_lastMaxWidth = maxWidth;
// A change in layout invalidates the cached caret and line metrics as well.
_lineMetricsCache = null;
_previousCaretPosition = null;
_previousCaretPrototype = null;
_layoutParagraph(minWidth, maxWidth);
_inlinePlaceholderBoxes = _paragraph!.getBoxesForPlaceholders();
}
void _createParagraph() {
assert(_paragraph == null || _rebuildParagraphForPaint);
final InlineSpan? text = this.text;
if (text == null) {
throw StateError('TextPainter.text must be set to a non-null value before using the TextPainter.');
}
final ui.ParagraphBuilder builder = ui.ParagraphBuilder(_createParagraphStyle());
text.build(builder, textScaleFactor: textScaleFactor, dimensions: _placeholderDimensions);
_inlinePlaceholderScales = builder.placeholderScales;
_paragraph = builder.build();
_rebuildParagraphForPaint = false;
}
方案一思路形成:通过自定义ui.Paragraph中的ParagraphBuilder来控制每个文字的宽高,根据每个文字的宽度可以算出总宽度,然后通过手机屏幕的宽度减去文字总宽度就可以算出每个文字之间的间距,这样就可以确定每个文字的偏移量,最后在ui.Paragraph的paint中根据偏移量去绘制文字,就可以达到控制排版的效果.
我按照上面的思路去看quill框架代码,并修改quill绘制层源码,实现了排版功能.其中遇到了两问题.一是文字还没开始绘制,我们不知道当前文字宽度设置多大合适,如果是长英文单词需要设置的大一点,普通文字需要设置小一点.二是因为每个字的大小设置不一样,每一个字都需要创建一个ui.Paragraph.我测试了下,编辑器输入1万字的时候就开始卡顿.所以这个方案以失败告终.
继续看了下ui.Paragraph的layout代码,可以看到native,具体实现在flutter engine里面.至此dart framework层的RichText流程就完了.想要更深入研究只能看flutter engine代码了.
/// Computes the size and position of each glyph in the paragraph.
///
/// The [ParagraphConstraints] control how wide the text is allowed to be.
void layout(ParagraphConstraints constraints) {
_layout(constraints.width);
assert(() {
_needsLayout = false;
return true;
}());
}
void _layout(double width) native 'Paragraph_layout';
打开flutter engine 源码后,直接搜索Paragraph,可以找到flutter/lib/ui/text/paragraph.cpp.查看它的layout方法,发现Paragraph是个父类,并没有自己实现layout,它应该是有子类在做实现.
查看了下flutter/lib/ui/text/下并没有Paragraph的实现类,flutter engine库十分庞大,所以我通过断点发现它的子类ParagraphImpl,全局搜索找到third_party/skia/modules/skparagraph/src/ParagraphImpl.cpp.Skia是通用的图形渲染引擎.ParagraphImpl的layout的代码特别多,我们只需要看下行相关的排版核心代码.
void ParagraphImpl::layout(SkScalar rawWidth) {
...
if (fState < kLineBroken) {
this->resetContext();
this->resolveStrut();
this->computeEmptyMetrics();
this->fLines.reset();
this->breakShapedTextIntoLines(floorWidth);
fState = kLineBroken;
}
...
}
void ParagraphImpl::breakShapedTextIntoLines(SkScalar maxWidth) {
...
TextWrapper textWrapper;
textWrapper.breakTextIntoLines(
this,
maxWidth,
[&](TextRange textExcludingSpaces,
TextRange text,
TextRange textWithNewlines,
ClusterRange clusters,
ClusterRange clustersWithGhosts,
SkScalar widthWithSpaces,
size_t startPos,
size_t endPos,
SkVector offset,
SkVector advance,
InternalLineMetrics metrics,
bool addEllipsis) {
// TODO: Take in account clipped edges
auto& line = this->addLine(offset, advance, textExcludingSpaces, text, textWithNewlines, clusters, clustersWithGhosts, widthWithSpaces, metrics);
if (addEllipsis) {
line.createEllipsis(maxWidth, getEllipsis(), true);
}
fLongestLine = std::max(fLongestLine, nearlyZero(advance.fX) ? widthWithSpaces : advance.fX);
});
...
}
TextLine& ParagraphImpl::addLine(SkVector offset,
SkVector advance,
TextRange textExcludingSpaces,
TextRange text,
TextRange textIncludingNewLines,
ClusterRange clusters,
ClusterRange clustersWithGhosts,
SkScalar widthWithSpaces,
InternalLineMetrics sizes) {
// Define a list of styles that covers the line
auto blocks = findAllBlocks(textExcludingSpaces);
return fLines.emplace_back(this, offset, advance, blocks,
textExcludingSpaces, text, textIncludingNewLines,
clusters, clustersWithGhosts, widthWithSpaces, sizes);
}
layout布局的时候,会动态的根据文字的宽度把要显示的文字拆成多行.所以行(TextLine)就成为一段文字绘制的一个更小的单位.我们需要看下行相关的布局.TextLine代码也非常多,我找了下,并没有layout方法,只有paint方法.paint方法里面只有背景、文字本身、阴影、装饰等绘制.思路中断,我的导师王柯跟我说justify属性实际上是有两边对齐的效果的,只是中文符号和英文存在的时候不会对齐.我在TextLine中能看到一个justify的方法,代码如下:
void TextLine::justify(SkScalar maxWidth) {
// Count words and the extra spaces to spread across the line
// TODO: do it at the line breaking?..
size_t whitespacePatches = 0;
SkScalar textLen = 0;
bool whitespacePatch = false;
this->iterateThroughClustersInGlyphsOrder(false, false,
[&whitespacePatches, &textLen, &whitespacePatch](const Cluster* cluster, bool ghost) {
if (cluster->isWhitespaceBreak()) {
if (!whitespacePatch) {
whitespacePatch = true;
++whitespacePatches;
}
} else {
whitespacePatch = false;
}
textLen += cluster->width();
return true;
});
if (whitespacePatches == 0) {
return;
}
SkScalar step = (maxWidth - textLen) / whitespacePatches;
SkScalar shift = 0;
// Deal with the ghost spaces
auto ghostShift = maxWidth - this->fAdvance.fX;
// Spread the extra whitespaces
whitespacePatch = false;
this->iterateThroughClustersInGlyphsOrder(false, true, [&](const Cluster* cluster, bool ghost) {
if (ghost) {
if (cluster->run().leftToRight()) {
shiftCluster(cluster, ghostShift, ghostShift);
}
return true;
}
auto prevShift = shift;
if (cluster->isWhitespaceBreak()) {
if (!whitespacePatch) {
shift += step;
whitespacePatch = true;
--whitespacePatches;
}
} else {
whitespacePatch = false;
}
shiftCluster(cluster, shift, prevShift);
return true;
});
SkAssertResult(nearlyEqual(shift, maxWidth - textLen));
SkASSERT(whitespacePatches == 0);
this->fWidthWithSpaces += ghostShift;
this->fAdvance.fX = maxWidth;
}
这个其实就是我们要找的计算排版的核心逻辑:它会遍历当前行的每一个文字,计算出空格的数量和文字的总长度,然后使用行占用的最大宽度减去文字的总宽度,再除以空格的数量,就能得到每一个空格的偏移量.然后重新遍历整行文字,在每个空格地方加上之前算的偏移量.justify逻辑是可以做到等分空格的间距.我们做两端对齐,实际是需要等分每一个文字之间的间距,然后我们项目中还有首行缩进的功能,首行的第一个字前面是有两个空格的,我们在计算间距的时候不能把这两个空格加上,否则会导致首行第一个字母也会跟随文字变化而发生位置变化.我们最终修改代码逻辑如下:
void TextLine::justify(SkScalar maxWidth) {
// Count words and the extra spaces to spread across the line
// TODO: do it at the line breaking?..
constexpr auto kWhiteSpaceNumOfStart = 2;
size_t allCharNums = 0;
SkScalar textLen = 0;
size_t posOfCharFirst = -1;
size_t firstResult = 0;
this->iterateThroughClustersInGlyphsOrder(
false, false, [&](const Cluster* cluster, bool ghost) {
textLen += cluster->width();
posOfCharFirst++;
if (posOfCharFirst == firstResult && firstResult < kWhiteSpaceNumOfStart &&
cluster->isWhitespaceBreak()) {
firstResult++;
return true;
}
if (posOfCharFirst == 0 || (posOfCharFirst == firstResult)) {
return true;
}
++allCharNums;
return true;
});
if (allCharNums == 0) {
return;
}
SkScalar step = (maxWidth - textLen) / allCharNums;
SkScalar shift = 0;
// Deal with the ghost spaces
auto ghostShift = maxWidth - this->fAdvance.fX;
// Spread the extra whitespaces
size_t posOfCharSecond = -1;
size_t result = 0;
this->iterateThroughClustersInGlyphsOrder(false, true, [&](const Cluster* cluster, bool ghost) {
posOfCharSecond++;
if (ghost) {
if (cluster->run().leftToRight()) {
shiftCluster(cluster, ghostShift, ghostShift);
}
return true;
}
auto prevShift = shift;
if (posOfCharSecond == result && result < kWhiteSpaceNumOfStart &&
cluster->isWhitespaceBreak()) {
result++;
return true;
}
if (posOfCharSecond == 0 || posOfCharSecond == result) {
return true;
}
shift += step;
shiftCluster(cluster, shift, prevShift);
return true;
});
SkAssertResult(nearlyEqual(shift, maxWidth - textLen));
this->fWidthWithSpaces += ghostShift;
this->fAdvance.fX = maxWidth;
}
六、总结
总体分为两大块:一块是分析Flutter 引擎 c++代码并修改,另一块是定制engine引入项目打包.两部分难度都挺大,定制engine打包遇到的问题更难解决.目前定制engine的公司不多,网上没有成熟的解决方案,我们的方案是一点点摸索出来的,希望对你有所帮助.
如果你有实践过好的自定义flutter engine的方案,也可以在评论区留言~
网友评论