-
void SimpleSVG::onDraw(tgfx::Canvas *canvas, const drawers::AppHost *host) {
auto width = host->width();
auto height = host->height();
if (svgDom == nullptr) {
auto svgPath = host->getSvg(svgName);
if (svgPath.empty()) {
return;
}
auto stream = tgfx::Stream::MakeFromFile(svgPath);
if (!stream) {
return;
}
svgDom = tgfx::SVGDOM::Make(*stream);
if (svgDom == nullptr) {
return;
}
// auto domSize = svgDom->getContainerSize();
// float maxWidth = 1000.0;
// if (domSize.width > 0.0 && domSize.width > maxWidth) {
// svgScale = maxWidth / domSize.width;
// }
// maxSize.width = domSize.width * svgScale;
// maxSize.height = domSize.height * svgScale;
// svgDom->setContainerSize(contentSize);
}
auto domSize = svgDom->getContainerSize();
// 计算初始矩阵
auto initialMatrix = tgfx::Matrix::I();
float scaleX = width / domSize.width;
float scaleY = height / domSize.height;
float initialScale = std::min(scaleX, scaleY);
initialMatrix.postScale(initialScale, initialScale);
// 计算居中平移量
float initialTranslateX = (width - domSize.width * initialScale) / 2.0f;
float initialTranslateY = (height - domSize.height * initialScale) / 2.0f;
initialMatrix.postTranslate(initialTranslateX, initialTranslateY);
// 计算用户手势矩阵
auto gestureMatrix = tgfx::Matrix::I();
float zoomScale = host->zoomScale();
auto contentOffset = host->contentOffset();
gestureMatrix.postScale(zoomScale, zoomScale);
gestureMatrix.postTranslate(contentOffset.x, contentOffset.y);
// 将两个矩阵合并
canvas->concat(gestureMatrix);
canvas->concat(initialMatrix);
svgDom->render(canvas);
}
|
Beta Was this translation helpful? Give feedback.
Replies: 44 comments 12 replies
-
了解到问题了,我去尝试复现一下transform的错误。可能是根节点上的transform处理的时候有问题。 |
Beta Was this translation helpful? Give feedback.
-
这里不应该直接用getContainerSize作为最终在Canvas上的渲染区域的大小,因为这个接口只是返回出来SVG根节点的width、height或者是viewBox。也就是case中的width="5888" height="5904"
|
Beta Was this translation helpful? Give feedback.
-
@0x1306a94 都开新帖回复吧,原帖下连续回复容易被折叠。接上面的图层渲染树模块的描述。如果你对Figma不是很熟悉,可以看看下面这个录屏应该有个直观的感受。可以确认一下你们的场景是否类似这样。这个视频里的文稿有20w个图层的矢量+图片内容。复杂度是极其高的,无论如何缩放都会非常丝滑并且精细。实际业务场景的矢量图层复杂度应该不太可能超过这个例子了。这个就是使用图层渲染树的瓦片渲染模式可以达到的效果。但是要根据实际业务场景的复杂度定制化调整一下瓦片相关的参数。 layers-20w.mov |
Beta Was this translation helpful? Give feedback.
-
关于模糊的问题,这个是自己可以控制的。设置几个分级的分辨率。到一定区间就创建一个更大的Surface并绘制缓存Image,然后缩小了使用。而不是用一个固定尺寸的图片缓存去放大,这一定是模糊的。你还有个简单的思路就是用户交互过程中先用现有的缓存去快速放大,这样缩放过程中允许一定短暂的模糊,但是CPU占用几乎没有会非常平滑,等用户操作停下的时候。再去更新新缩放系数下准确的图片缓存就好了。这个也是Figma里流畅缩放的策略。业务上其实有很多简易的优化性能的手段,结合业务特点充分使用缓存策略使用就行。不是必须死磕SVG的硬的绘制耗时。还有个优化方面的建议。SVG本身解析转换是有开销的。建议你保存为一个Picture,并扔掉原始的SVG。但不要直接把Picture转为Image图片,这样会锁定死分辨率清晰度也锁定死了。只持有原始的Picture就是支持无线缩放都清晰的。然后你每次缓存特定分辨率的Image的时候,就调用离屏Surface上的canvas去draw这个picture,并通过canvas来缩放就比较快一点,比每次去重新画SVG可以跳过解析的过程。 |
Beta Was this translation helpful? Give feedback.
-
|
Beta Was this translation helpful? Give feedback.
-
是的,SVGKit就是自己完成了SVG格式到CALayer树的转换,然后基于图层渲染树来高性能渲染以及交互。了解你的场景了,确实SVG格式会更加常用,因为各种软件都可以直接输出。那我们也研究一下有没可能像苹果那样,直接把SVG一次性转换为我们的tgfx图层树。 @YGaurora 记录一下这方向的需求吧。 |
Beta Was this translation helpful? Give feedback.
-
|
Beta Was this translation helpful? Give feedback.
-
你们的场景只是缩放比较多,但是图层内容其实不怎么变化是吧?如果存在频繁交互性并会一直修改图层,那么就建议使用图层树的渲染模式。如果只是相对固定的矢量背景。其实比较简单的操作还是推荐前面那种缓存策略。你先跑通离屏Surface缓存Image的方法。然后试试缩放过程使用缓存图片,只在缩放结束的时候更新当前分辨率的清晰图片缓存。并且始终用转换好为Picture的数据去更新缓存,而不使用原始SVG。这样简单处理估计也已经可以满足性能要求了。 |
Beta Was this translation helpful? Give feedback.
-
变化还是多的,比如某个区域售罄了,或者其他变化。以及座位选中/不选中 是否可用 等等各种状态都对应不同的UI效果。比如填充颜色不同等等 |
Beta Was this translation helpful? Give feedback.
-
那好像确实应该使用图层树的方式比较合适。还有个简单的现在就可以尝试的思路。其实我们的图层树分图层的逻辑是你会不会单独编辑修改这个图层。如果存在可能独立变更的,就尽量和其他图层分开,避免改动局部导致整体都刷新了。我们的图层树是能做到精确的局部刷新的,每次只更新变更的脏矩形区域。所以根据这个原则。你的大背景如果是个不会变的SVG,可以把这个SVG整体转为一个图层内容就行了。其他的小SVG也是同理。如果不是需要独立修改,可能也不是必须要把SVG内部继续再打散。但如果能把SVG拆小的建议尽量拆小,毕竟进去遍历完整的一个大Picture做裁剪区域判断也是有开销的。具体实现方式是写个自定义图层继承Layer,然后覆盖onUpdateContent()方法,从传入的LayerRecorder上获取canvas去绘制这个SVG就行了,图层内部会自己缓存为Picture的,并且这个方法只会执行一次。画完你就可以丢弃SVG对象了。具体可以参考:https://github.com/Tencent/tgfx/blob/main/drawers/src/layertree/CustomLayer.cpp 其他不是SVG的图层,你根据内置的那几种Layer直接创建就行。最后DisplayList记得设置为Tiled的RenderMode,并且建议开启allowZoomBlur以及把maxTileCount设置为一个大一点的值,默认值只足够不缩放情况下的快速移动,需要高性能缩放的话,建议设置网格数量为覆盖屏幕3-4倍的量。 |
Beta Was this translation helpful? Give feedback.
-
void SimpleSVG::onDraw(tgfx::Canvas *canvas, const drawers::AppHost *host) {
auto density = host->density();
auto width = host->width();
auto height = host->height();
if (cacheImage == nullptr) {
if (svgDom == nullptr) {
auto svgPath = host->getSvg(svgName);
if (svgPath.empty()) {
return;
}
auto stream = tgfx::Stream::MakeFromFile(svgPath);
if (!stream) {
return;
}
svgDom = tgfx::SVGDOM::Make(*stream);
if (svgDom == nullptr) {
return;
}
// svgDom->getRoot()->setTransform(tgfx::Matrix::I());
auto domSize = svgDom->getContainerSize();
float maxWidth = 1000.0 * density;
auto maxSize = domSize;
if (domSize.width > 0.0 && domSize.width > maxWidth) {
// float svgScale = maxWidth / domSize.width;
// maxSize.width = std::ceil(domSize.width * svgScale);
// maxSize.height = std::ceil(domSize.height * svgScale);
}
auto rootMatrix = svgDom->getRoot()->getTransform();
auto renderRect = tgfx::Rect::MakeSize(domSize);
renderRect = rootMatrix.mapRect(renderRect);
float fitScaleX = maxSize.width / renderRect.width();
float fitScaleY = maxSize.height / renderRect.height();
float fitScale = std::min(fitScaleX, fitScaleY);
float scaledRenderWidth = renderRect.width() * fitScale;
float scaledRenderHeight = renderRect.height() * fitScale;
float translateX = (maxSize.width - scaledRenderWidth) / 2.0f - renderRect.left * fitScale;
float translateY = (maxSize.height - scaledRenderHeight) / 2.0f - renderRect.top * fitScale;
tgfx::Matrix initialMatrix = tgfx::Matrix::I();
initialMatrix.postScale(fitScale, fitScale);
initialMatrix.postTranslate(translateX, translateY);
// const float fitScale = std::min(maxSize.width / renderRect.width(), maxSize.height / renderRect.height());
// const tgfx::Point rrCenter = tgfx::Point(renderRect.centerX(), renderRect.centerY());
// const tgfx::Point canvasCenter{maxSize.width * 0.5f, maxSize.height * 0.5f};
//
// tgfx::Matrix initialMatrix = tgfx::Matrix::I();
// initialMatrix.postTranslate(-rrCenter.x, -rrCenter.y);
// initialMatrix.postScale(fitScale, fitScale);
// initialMatrix.postTranslate(canvasCenter.x, canvasCenter.y);
auto context = canvas->getSurface()->getContext();
auto surface = tgfx::Surface::Make(context, static_cast<int>(maxSize.width), static_cast<int>(maxSize.height));
auto tempCanvas = surface->getCanvas();
tempCanvas->clear();
tempCanvas->concat(initialMatrix);
svgDom->render(tempCanvas);
cacheImage = surface->makeImageSnapshot();
}
}
if (cacheImage == nullptr) {
return;
}
float imageWidth = cacheImage->width();
float imageHeight = cacheImage->height();
// 3) 初始适配(基于 renderRect)
const float fitScale = std::min(width / imageWidth, height / imageHeight);
const tgfx::Point rrCenter = tgfx::Point(imageWidth * 0.5, imageHeight * 0.5); // 内容中心(root 变换后)
const tgfx::Point canvasCenter{width * 0.5f, height * 0.5f}; // 画布中心
// 把内容从自身坐标放到画布中心并按短边适配:
tgfx::Matrix initialMatrix = tgfx::Matrix::I();
initialMatrix.postTranslate(-rrCenter.x, -rrCenter.y); // 以内容中心为原点
initialMatrix.postScale(fitScale, fitScale); // 适配缩放
initialMatrix.postTranslate(canvasCenter.x, canvasCenter.y); // 放到画布中心
// 计算用户手势矩阵
auto gestureMatrix = tgfx::Matrix::I();
float zoomScale = host->zoomScale();
auto contentOffset = host->contentOffset();
gestureMatrix.postScale(zoomScale, zoomScale);
gestureMatrix.postTranslate(contentOffset.x, contentOffset.y);
// 将两个矩阵合并
canvas->concat(gestureMatrix);
canvas->concat(initialMatrix);
canvas->drawImage(cacheImage);
} ScreenRecording_08-07-2025.12-10-21_1.MP4 |
Beta Was this translation helpful? Give feedback.
-
你这个问题看起来应该是没有处理图片缓存的分级,不能只用一个大图然后全部缩小来绘制。把一个太大的图片缩放到太小,采样的结果部分细节可能是会丢失的,特别是倾斜的像素线条。建议还是用前面提的思路。你在缩放结束的时候,更新一次Surface的Image缓存为当前准确的分辨率。这样不缩放过程不卡顿,缩放结束也是最清晰的。还有一个思路可以快速试一下的就是离屏的那个Surface创建的时候开启一下mipmap参数,这样最后makeImageSnapshot出来的Image会开启mipmap,缩小采样的时候损失也会小一点。但是会占用更多的内存。自己可以根据实际表现选择下什么方案最合适。 |
Beta Was this translation helpful? Give feedback.
-
namespace drawers {
class ChooseSeatBaseMapLayer : public tgfx::Layer {
public:
const AppHost *host;
std::shared_ptr<tgfx::SVGDOM> svgDom;
static std::shared_ptr<ChooseSeatBaseMapLayer> Make() {
return std::shared_ptr<ChooseSeatBaseMapLayer>(new ChooseSeatBaseMapLayer());
}
ChooseSeatBaseMapLayer()
: host(nullptr)
, svgDom(nullptr) {
}
void Init(const AppHost *host, const std::string &mapName) {
this->host = host;
auto svgPath = host->getSvg(mapName);
if (svgPath.empty()) {
return;
}
auto stream = tgfx::Stream::MakeFromFile(svgPath);
if (!stream) {
return;
}
svgDom = tgfx::SVGDOM::Make(*stream);
invalidateContent();
}
protected:
void onUpdateContent(tgfx::LayerRecorder *recorder) override {
if (host == nullptr) {
return;
}
if (svgDom == nullptr) {
return;
}
auto canvas = recorder->getCanvas();
if (canvas == nullptr) {
return;
}
auto density = host->density();
auto width = host->width();
auto height = host->height();
auto domSize = svgDom->getContainerSize();
auto rootMatrix = svgDom->getRoot()->getTransform();
auto renderRect = tgfx::Rect::MakeSize(domSize);
renderRect = rootMatrix.mapRect(renderRect);
// 3) 初始适配(基于 renderRect)
const float fitScale = std::min(width / renderRect.width(), height / renderRect.height());
const tgfx::Point rrCenter = tgfx::Point(renderRect.centerX(), renderRect.centerY()); // 内容中心(root 变换后)
const tgfx::Point canvasCenter{width * 0.5f, height * 0.5f}; // 画布中心
// 把内容从自身坐标放到画布中心并按短边适配:
tgfx::Matrix initialMatrix = tgfx::Matrix::I();
initialMatrix.postTranslate(-rrCenter.x, -rrCenter.y); // 以内容中心为原点
initialMatrix.postScale(fitScale, fitScale); // 适配缩放
initialMatrix.postTranslate(canvasCenter.x, canvasCenter.y); // 放到画布中心
// 计算用户手势矩阵
auto gestureMatrix = tgfx::Matrix::I();
float zoomScale = host->zoomScale();
auto contentOffset = host->contentOffset();
gestureMatrix.postScale(zoomScale, zoomScale);
gestureMatrix.postTranslate(contentOffset.x, contentOffset.y);
// 将两个矩阵合并
// canvas->concat(gestureMatrix);
canvas->concat(initialMatrix);
svgDom->render(canvas);
}
};
std::shared_ptr<tgfx::Layer> ChooseSeatLayerTree::buildLayerTree(const AppHost *host) {
auto root = tgfx::Layer::Make();
auto basemapLayer = ChooseSeatBaseMapLayer::Make();
basemapLayer->Init(host, baseMapName);
root->addChild(basemapLayer);
return root;
}
} // namespace drawers ScreenRecording_08-07-2025.12-18-56_1.MP4 |
Beta Was this translation helpful? Give feedback.
-
哦, 我大概知道问题是什么了。这应该是一个已知的特性缺失。我们还没有支持极细的线条绘制。当一条线的宽度缩小到快接近于0的时候,会处于看得见和看不见的随机状态中。这个叫做hairline渲染模式,就算线条宽度等于0的时候也要绘制一条连续的极细的线(靠调整透明度让它看起来更细),这个特性我们近期会排期下,开发好了回复你。你可以先关注其他性能层面的策略问题。 |
Beta Was this translation helpful? Give feedback.
-
auto surface = tgfx::Surface::Make(context, static_cast<int>(maxSize.width), static_cast<int>(maxSize.height), tgfx::ColorType::RGBA_8888, 1, true); ![]() |
Beta Was this translation helpful? Give feedback.
-
这个是应该只是矩阵计算的顺序问题,你可以参考Matrix DisplayList::getViewMatrix()的代码,DisplayList上都是先应用zoomScale,然后再应用contentOffset偏移的。你遇到的问题应该只是设置的值顺序反了。设置给DisplayList的contentOffset是要用已经缩放过后的偏移量就对了。 |
Beta Was this translation helpful? Give feedback.
-
auto zoomScale = app->zoomScale();
auto contentOffset = app->contentOffset();
_displayList->setZoomScale(zoomScale);
_displayList->setContentOffset(contentOffset.x * zoomScale, contentOffset.y * zoomScale); |
Beta Was this translation helpful? Give feedback.
-
不是这里设置的地方要改什么。是看你算contentOffset和zoomScale的地方。如果你要限制最大最小缩放值,就直接修改zoomScale的结果,然后要基于这个zoomScale再去计算偏移量。不是简单的乘以或除以一个缩放值。就想象原始的图片在画布的左上角。你先只缩放它会摆在什么位置,然后你基于这个结果加一个你想要的偏移量。是这样计算的。 |
Beta Was this translation helpful? Give feedback.
-
DisplayList的这个值就是和iOS的UIScrollView的两个属性完全一样的定义。你用那个操作一下。那边对的这个就是对的。你现在是要做限制的话,就是得到缩放值之后先改动缩放值。然后基于这个缩放值变换完视图之后,看现在的偏移量,再算移到中心需要的偏移量。 |
Beta Was this translation helpful? Give feedback.
-
我现在就是利用的取巧的方式,直接用UIScrollView计算的zoomScale contentOffset 我先改成tgfx demo 里面那样自己加手势看看 |
Beta Was this translation helpful? Give feedback.
-
UIScrollView的缩放值和contentOffset是不加限制的结果吧?然后你想要加一下缩放最大和最小的限制。那contentOffset就需要重新计算,不能直接用UIScrollView给的contentOffset。每个contentOffset都是已经包含了之前的缩放值的影响。应该是这里的问题。你先设置contentOffset始终为0,然后只缩放视图。观察一下结果就比较好理解偏移量应该怎么计算了。这个就是分为两步走的,先只缩放,然后基于当前的结果计算出你想挪到中心需要的最终偏移量。contentOffset的定义永远是缩放完成之后的视图你再要叠加多少偏移,按照实际缩放后的坐标系叠加。 |
Beta Was this translation helpful? Give feedback.
-
还有个问题,Layer 可以设置为固定的尺寸吗?目前看我看一个Layer的尺寸是根据子元素计算出来的。 |
Beta Was this translation helpful? Give feedback.
-
@domchen 利用 - (void)updateRendererWithScrollViewState {
#if USE_UISCROLLVIEW_TRICK
CGFloat zoomScale = self.scrollView.zoomScale;
float contentScaleFactor = self.backendView.contentScaleFactor;
// 获取 UIScrollView 的 contentOffset 和 zoomContentView 的 frame.origin
// 它们都是点的单位
CGPoint scrollViewContentOffset = self.scrollView.contentOffset;
CGPoint zoomContentViewOrigin = self.zoomContentView.frame.origin;
// tgfx 所需的平移量是两者相减
// contentOffset的增加,意味着内容向左移动,所以需要减去
tgfx::Point tgfxOffsetPoints{
static_cast<float>(zoomContentViewOrigin.x - scrollViewContentOffset.x),
static_cast<float>(zoomContentViewOrigin.y - scrollViewContentOffset.y),
};
// 将最终平移量转换为像素
tgfx::Point tgfxOffsetPixels{
static_cast<float>(tgfxOffsetPoints.x * contentScaleFactor),
static_cast<float>(tgfxOffsetPoints.y * contentScaleFactor),
};
// 将缩放和偏移量传递给 tgfx 渲染器
[self updateZoom:zoomScale contentOffset:tgfxOffsetPixels];
#endif
} ScreenRecording_08-11-2025.19-04-17_1.MP4另外基于手势自行处理也对了,只是没有实现回弹/阻尼效果 |
Beta Was this translation helpful? Give feedback.
-
Layer的尺寸都是测量出来的,并没有一个「设置尺寸」的概念。你想要它是多大的就控制它的matrix,对它测量的结果进行缩放和位移。Layer容器也不存在自动布局。所以你设置一个尺寸给它也没有任何意义。你按照你想要的布局边界,去摆放它内部的子图层位置就行。 |
Beta Was this translation helpful? Give feedback.
-
感觉能够直接设置尺寸还是有必要的吧?尤其是在图层列表中 |
Beta Was this translation helpful? Give feedback.
-
Figma那个不是容器的尺寸,就是一个Rect图形,可以加描边加填充的。我的意思就是不存在什么无意义的宽高属性。就是内容决定的。ShapeLayer的内容就是Path,可以把一个Rect指定为ShapeLayer的内容。那么测量的尺寸就是你需要的了。你想一下给一个空Layer设置宽高的具体意义什么?没有意义。这是一个纯渲染方案,不做自动布局。TextLayer是个例外,它是有宽高属性的。是因为文本需要布局和自动换行。其他的图层就是根据自己的语义决定要不要宽高。没有统一的无意义的宽高。 |
Beta Was this translation helpful? Give feedback.
-
明白了🫡 |
Beta Was this translation helpful? Give feedback.
-
嗯,没有内容就是空图层,不会画任何内容。实际上整个DisplayList只关心Layer的基类。其他的Layer子类都是和基类完全解耦的,只负责提供onUpdateContent那个方法要记录的Picture。比如ShapeLayer就是一个自定义Layer的常用示例,你甚至可以把ShapeLayer拷贝到项目里直接使用,或者各种修改是都可以的。它本身只依赖引擎的公开API。你也可以根据业务需求继承Layer放自己的任何自定义属性,包括加宽高属性。如果这些属性会参与到你计算onUpdateContent方法的内容的话。如果需要自动布局功能,也是可以自己通过自定义Layer扩展的。 |
Beta Was this translation helpful? Give feedback.
-
@domchen 下面代码中三个 void SeatCraftCoreRenderer::draw(bool force) {
if (_backend == nullptr) {
return;
}
auto window = _backend->getWindow();
if (window == nullptr) {
return;
}
auto device = window->getDevice();
auto context = device->lockContext();
if (context == nullptr) {
return;
}
auto surface = window->getSurface(context);
if (surface == nullptr) {
device->unlock();
return;
}
auto canvas = surface->getCanvas();
auto appPtr = _app.get();
_gridLayer->prepare(canvas, appPtr, force);
_seatLayer->prepare(canvas, appPtr, force);
_minimapLayer->prepare(canvas, appPtr, force);
bool hasContentChanged = _gridLayer->hasContentChanged() ||
_seatLayer->hasContentChanged() ||
_minimapLayer->hasContentChanged();
if (!hasContentChanged && !force && !_invalidate) {
device->unlock();
return;
}
canvas->clear();
canvas->save();
_gridLayer->draw(canvas, appPtr);
_seatLayer->draw(canvas, appPtr);
_minimapLayer->draw(canvas, appPtr);
canvas->restore();
context->flushAndSubmit();
window->present(context);
device->unlock();
_invalidate = false;
} 另外在Android平台,我一开始也是参考tgfx demo 使用 TextureView 但发现交互时渲染延迟很大. |
Beta Was this translation helpful? Give feedback.
-
1.你应该只用一个DisplayList放所有图层,DisplayList是根容器,不应该创建多个。单个DisplayList内部的缓存开销是非常高的,你这样太浪费了。除非你几个层是缩放系数不同的,比如有个交互层是不缩放的,那最好那个层控制内容比较简单,然后采用Direct模式渲染,这样减少内存浪费。并且存在多个DisplayList的情况下,要遍历所有DisplayList的hasContentChanged属性,任何一个为true都要触发渲染。 |
Beta Was this translation helpful? Give feedback.
这里不应该直接用getContainerSize作为最终在Canvas上的渲染区域的大小,因为这个接口只是返回出来SVG根节点的width、height或者是viewBox。也就是case中的width="5888" height="5904"
因为根节点的变换矩阵可能存在旋转和平移,渲染区域会是一个矩形区域,而不是只要有Size宽高就行了。接口最初的设计就是返回根节点的属性并不是实际渲染区域。