目录
-
- 前言
- GIF 与 Lottie 的区别
- Qt 6.8 与 Qt 6.11 的 Lottie 实现差异
-
- 模块结构:从一体插件到独立 Qt 模块
- 日志分类换名是个隐形升级成本
- 两条渲染路线
- 两版共有的限制清单
- 一个更隐蔽的差别:预合成层
- 实测:4 个素材在两版下的渲染差异
-
- 官方提供的判断手段
- 结论
前言
在QML里实现动态图的路径有两条:GIF 和 Lottie。
这篇文章先把两者的取舍做一点说明,然后对比 Qt 6.8 与 Qt 6.11 的 Lottie 实现差异——同一份 Lottie 素材,在这两个版本里可能画出完全不同的结果,这一点会直接决定你的工程该用哪个版本。
以下是Qt6.8.2中运行的效果:
只有第一张图基本展示了素材的动画,第二张图就缺少大量的信息,然后第三张和第四张就没有动。这个版本的lottie库还需要额外编译,很多特性不支持,导致了很多JSON格式的lottie动画都没法适用。
以下是Qt6.11中运行的效果:
所有的图都能动,但是第三张图的沙漏位置有点异常,我让AI帮我修复了一下(图中展示的是修复后的JSON文件)。
本文是 QML Lottie 系列第 1 篇,共 3 篇。 文中的素材来自:https://lottiefiles.com/
GIF 与 Lottie 的区别
GIF 的本质是逐帧位图:每一帧都是一张 8 位索引色的图片,靠 LZW 压缩串起来。
Lottie 的本质是矢量图形加关键帧动画的 JSON 描述:一个圆、一条路径、一次旋转都带关键帧,播放时由渲染器实时画出来。它来自 Adobe After Effects 的 Bodymovin 插件导出。
具体差异如下:
| 数据本质 | 逐帧位图(LZW 压缩) | 矢量图形 + 关键帧的 JSON |
| 颜色 | 单帧最多 256 色 | 32 位真彩,支持渐变 |
| 透明度 | 只有 1 位(全透明 / 全不透明),边缘必带锯齿 | 完整 alpha 通道,半透明与柔和边缘 |
| 缩放 | 位图,放大模糊 | 矢量,等比缩放 |
| 体积 | 与画幅、时长强相关,几百 KB 到数 MB | 同画幅通常 10~100 KB 量级 |
| 运行时控制 | 只能整体播 / 停 | 可指定帧、方向、循环次数、逐帧拖拽 |
| 单帧寻址 | 有,currentFrame 可读可写 | 有,gotoAndStop(frame) |
| 渲染开销 | 每帧解码位图,内存 ≈ 帧数 × 画幅 | 实时光栅化,开销与素材固有尺寸相关 |
| 二次编辑 | 只能重新导出 | JSON 可改,也能脚本批量改 |
| Qt 里的类型 | AnimatedImage(依赖 qgif.dll 图片插件) | LottieAnimation(Qt.labs.lottieqt) |
Qt 6.8 和 6.11 都自带 qgif.dll,所以 AnimatedImage 直接放 GIF 就能动:
import QtQuick
AnimatedImage {
source: "loop.gif"
playing: true
fillMode: Image.PreserveAspectFit
}
AnimatedImage 的接口就三个关键点:playing 控制播停、currentFrame 能读能写、frameCount 拿总帧数。想做「拖拽到第 5 帧」也能做,但 GIF 的帧是整张位图,拖拽看到的仍然是 8 位色的锯齿边缘。
Lottie 的接口丰富得多,这是它真正的价值所在:
import Qt.labs.lottieqt
LottieAnimation {
source: "animation.json"
autoPlay: false
loops: LottieAnimation.Infinite
quality: LottieAnimation.MediumQuality
direction: LottieAnimation.Forward
onStatusChanged: {
if (status === LottieAnimation.Ready)
gotoAndPlay(startFrame)
}
}
一次 gotoAndStop(87) 就能精确停在任意一帧,direction 一改动画就倒放,loops 换个数字就换循环次数——这些 GIF 都做不到。
选型上比较实用的判断:
- 简单的无限循环装饰图,画幅小、颜色少、不需要控制 → GIF 够用,切图方便。
- 需要半透明、渐变、放大不糊、指定帧 / 方向 / 循环,或者文件要瘦 → Lottie 更合适。
- 素材里用了 Lottie 规范里相对冷门的能力(预合成层、矢量遮罩、Merge Paths、渐变描边、嵌套 Repeater、表达式)→ 先别急着上 Lottie,这些在 Qt 这边未必被支持,而且出问题时不报错,只是画得不对。下面第 2 节和第 3 篇会具体展开。
Qt 6.8 与 Qt 6.11 的 Lottie 实现差异
两版的导入语句完全一样:
import Qt.labs.lottieqt
也就是说升级 Qt 时 QML 代码一个字都不用改。但引擎换了,换得还挺彻底。
模块结构:从一体插件到独立 Qt 模块
对比两版 Qt 安装目录里的东西:
| QML 模块名 | Qt.labs.lottieqt | 同 |
| QML 插件 | lottieqtplugin.dll | lottieplugin.dll |
| 实际实现 | 插件自身(Bodymovin + QPainter ) | Qt6Lottie.dll独立模块,另有 Qt6LottieVectorImageGenerator.dll、Qt6LottieVectorImageHelpers.dll |
| 对外 C++ 模块 | 无 | QtLottie、QtLottieVectorImageGenerator、QtLottieVectorImageHelpers |
| 矢量渲染路线 | 无 | lottietoqml 命令行、qt_target_qml_from_lottie() CMake 命令、VectorImage 运行时加载 |
| 帧缓存调节 | 无 | QLOTTIE_RENDER_CACHE_SIZE 环境变量(默认 2) |
6.8 的 290 KB 插件里塞着解析器和渲染器,6.11 的插件只剩 31 KB,实现放到了 Qt6Lottie.dll。Qt 6.11 的 LottieAnimation 类型原型仍然是 QQuickPaintedItem,但多出了一整套转成 Qt Quick 场景的能力。
日志分类换名是个隐形升级成本
如果你像我一样,用「数绘制帧」的方式做自动化验证:
$env:QT_LOGGING_RULES = "qt.lottieqt.bodymovin.render=true"
这条在 6.11 下会一行日志都拿不到。实测把 qt.lottieqt.* 全打开,6.11 下出现的分类是:
qt.lottieqt.lottie.render ← 绘制帧在这里
qt.lottieqt.lottie.render.thread
qt.lottieqt.lottie.parser
qt.lottieqt.lottie.update
bodymovin 这个名字在 6.11 里命中 0 行。消息体倒是没变,还是 Start to paint frame N,只是前面多了一串对象地址。改一个 QT_LOGGING_RULES 就能恢复,但不知道这件事的话很容易误判成「6.11 抓不到渲染日志」。
两条渲染路线
6.11 起,渲染 Lottie 有两种做法。
第一种是 LottieAnimation(两版都有):用 QPainter 渲染到中间缓冲。官方文档的说法是「这可能对插图的尺寸上限和目标硬件带来一些性能限制」。要注意一个反直觉的点:它的开销正比于素材固有尺寸,而不是显示尺寸——把 1200×1200 的素材缩到 177 px 显示,每帧仍然要光栅化 1200²。
第二种是 lottietoqml(6.11 新增):把 Lottie 转成用 Qt Quick(Shape 等)描述的 QML 场景,走硬件加速的 Scene Graph。官方原话是渲染可能更快、更省 CPU,而且 Qt Quick 支持可缩放矢量,转出来的 item 能平滑缩放。命令行直接转:
lottietoqml input.json output.qml
也可以挂进 CMake,编译期就转好:
find_package(Qt6 REQUIRED COMPONENTS LottieTools)
qt_target_qml_from_lottie(myapp
CURVE_RENDERER
OPTIMIZE_PATHS
FILES original/fingerprint.json
OUTPUTS generated/Fingerprint.qml
)
另外 6.11 的 VectorImage 支持在运行时直接加载 Lottie:
import QtQuick.VectorImage
VectorImage {
source: "animation.json"
}
配套的 Qt.labs.lottieqt.VectorImageHelpers 模块(LayerItem 类型,提供组合了整条祖先链的 transformMatrix)是给这些生成代码用的,官方明确说「不打算当作通用组件使用」,所以平时不用碰它。
两版共有的限制清单
这部分很重要,它解释了为什么有些素材「能跑但看着不对」。官方限制清单在两版里都存在:
| 通用 | 表达式;时间轴只支持帧模式,不支持时间模式 |
| 动画级 | assets(可复用的文字与图片)、chars 文字 |
| 图层 | ao 自动定向、bm 混合模式、maskProperties 矢量遮罩、sr 时间拉伸 |
| 形状 | gstroke 渐变描边、嵌套 Repeater;多条 trim path 同时生效时行为不可预期 |
| 效果 | 只支持 Slide 与 Layer Fill |
一个更隐蔽的差别:预合成层
上面的清单里没有「预合成层(precomp)」,但实测下来,这是两版差别最大的地方。
Lottie 里 ty: 0 的图层是预合成层,它引用 assets 里的另一套图层,相当于「把一段动画当成一个整体引用进来」。6.8 的解析器只认两种图层类型:
// qt 6.8.2 src/qtlottie/src/bodymovin/bmlayer.cpp
BMLayer *BMLayer::construct(QJsonObject definition, const QVersionNumber &version)
{
BMLayer *layer = nullptr;
int type = definition.value(QLatin1String("ty")).toInt();
switch (type) {
case 2:
layer = new BMImageLayer(definition, version); // 图片层
break;
case 4:
layer = new BMShapeLayer(definition, version); // 形状层
break;
default:
qCWarning(lcLottieQtBodymovinParser) << "Unsupported layer type:" << type;
}
return layer;
}
ty = 0(预合成)、ty = 3(空对象)、ty = 5(文字)全落进 default 分支,返回 nullptr,整层被丢弃,也不会递归展开它引用的子合成。运行时会看到这样的警告:
qt.lottieqt.bodymovin.parser: Unsupported layer type: 0
6.11 重写后的解析器处理了预合成层,同一份素材不再有这条警告,渲染结果也补全了。这一点在本篇最后的实测里有直接体现。
顺带一提,6.11 的 LottieAnimation 文档里仍然写着「只支持 Shape 层」,与实测不符——文档没跟上重写。以实测为准。
实测:4 个素材在两版下的渲染差异
光看文档不够,我实际跑了一遍对照。
方法是不用编译,直接用两版自带的 qml.exe 跑探针 QML:autoPlay: false,在 onStatusChanged(Ready) 里 gotoAndStop(固定帧),再用 grabToImage() 抓图,最后逐像素统计「不透明像素数」和「包围盒」。固定帧是为了避免播放进度不同带来的干扰。
结果(画布 640,素材按 560 等比缩放后比对):
| success @40 | 19183 / (108, 96, 495, 483) | 19183 / (108, 96, 495, 483) | 完全一致,连 PNG 字节数都一样 |
| document-ocr-scan @120 | 2680 / (150, 187, 449, 195) | 78843 / (150, 145, 449, 464) | 6.8 只画出一条 8 px 高的线 |
| loading-sand-clock @25 | 370046 / (294, 117, 785, 1079) | 237171 / (294, 117, 785, 1063) | 6.8 溢出更严重,已贴到画布底边 |
| delete-bin @20 | 79616 / (462, 589, 768, 947) | 118197 / (199, 156, 782, 947) | 6.11 左上方多出一大片内容 |
对着这张对照图看更直观(左边是 6.8,右边是 6.11):
- 第一张success动画,一模一样。
- 第二张文档扫描动画,6.8 只剩一条横线,6.11 才是完整的,差别来自预合成层被丢弃。
- 第三张沙漏动画,6.8 一塌糊涂,动画失效;6.11 基本正确,但沙仍然漏到漏斗下方(图中是AI修复后的效果)。
- 第四张是删除文件的动画,6.8 里的动画失效;6.11 则运行正常。
官方提供的判断手段
6.11 的文档里给了一条很实用的建议:把日志分类打开观察帧缓存。如果缓存长期是满的,说明动画太复杂、渲染跟不上,应该简化动画或优化 QML 场景。
$env:QT_LOGGING_RULES = "qt.lottieqt.lottie.render=true;qt.lottieqt.lottie.render.thread=true"
帧缓存的容量用 QLOTTIE_RENDER_CACHE_SIZE 调(默认 2),素材复杂时可以适当加大,代价是内存。
结论
- 新工程直接用 Qt 6.11。预合成层能正常展开,日志分类更清晰,还多了 lottietoqml 这条硬件加速的矢量路线。
- 老工程升级到 6.11,QML 代码不用改,但要顺手改两处:验证脚本里的日志分类名,以及如果素材里用了预合成层,升级后观感会变(大概率是变好,但也要重新确认一遍)。
- 不要指望「升级 Qt 就能修好所有素材问题」。矢量遮罩、Merge Paths、渐变描边这些在两版里都不支持,属于素材层面的问题,得改素材本身。
网硕互联帮助中心




评论前必须登录!
注册