云计算百科
云计算领域专业知识百科平台

QML 动画选择:GIF与Lottie 的区别,以及 Qt 6.8 与 6.11 的 Lottie 实现差异

目录

    • 前言
    • 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 插件导出。

具体差异如下:

维度GIFLottie
数据本质 逐帧位图(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 安装目录里的东西:

Qt 6.8.2Qt 6.11.1
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 等比缩放后比对):

素材 @固定帧Qt 6.8.2 不透明像素 / 包围盒Qt 6.11.1 不透明像素 / 包围盒结论
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、渐变描边这些在两版里都不支持,属于素材层面的问题,得改素材本身。
赞(0)
未经允许不得转载:网硕互联帮助中心 » QML 动画选择:GIF与Lottie 的区别,以及 Qt 6.8 与 6.11 的 Lottie 实现差异
分享到: 更多 (0)

评论 抢沙发

评论前必须登录!