Skia如何渲染 Lottie 动画
lottie 动画原理
Lottie
是一个开源轻量级动画库,Lottie
通过JSON
文件在动画中定义复杂的动画特性,如颜色、位置、旋转、缩放、图层、形状、遮罩、文本等(甚至还有位图图像)。Lottie
动画播放器会读取并解析这个JSON
文件,并按照 JSON 描述的信息渲染图像。Lottie
动画的每一帧都是根据时间线和关键帧计算得出的,Lottie
会根据当前时间计算出应该显示的动画状态。描述
Lottie
动画的JSON
文件如下图所示:
增加依赖库
要想使用 Skia 渲染 Lottie
动画,必须增加几个依赖库:
skottie.lib
sksg.lib
skunicode_core.lib
skunicode_icu.lib
skshaper.lib
harfbuzz.lib
其中与 Lottie
动画有直接关系的库为: skottie.lib
,其他几个库都是 skottie.lib
库依赖的库。
线程锁
在播放 gif 动画时,用一个全局的 SkBitmap
对象来存储 gif 动画每一帧的信息。
子线程解码 gif 动画时,向 SkBitmap
对象写入像素数据,主线程播放 gif 动画时,读取 SkBitmap 对象的像素数据,并把它们写入窗口画布中。
即使改变窗口大小时(改变窗口画布对应的像素数据),也不存在多线程数据竞争的问题。
这样的话,改变窗口大小时,主线程会操作窗口画布像素数据。子线程绘制 Lottie
动画描述的图像时,子线程也会操作窗口画布像素数据。这就涉及到多线程竞争访问数据资源的问题。
所以,要使用线程锁解决这个问题。
如下代码所示:
// #include <mutex>// std::mutex locker;
case WM_SIZE:
{w = LOWORD(lParam);h = HIWORD(lParam);if (surfaceMemory) {std::unique_lock guard(locker);delete[] surfaceMemory;surfaceMemory = new SkColor[w * h]{ 0xff000000 };guard.unlock();}break;
}
这是窗口大小改变时执行的代码,在这段代码中,使用线程锁来保护重置窗口画布数据的工作。
std::mutex locker
是一个互斥对象,std::unique_lock
对象可以锁住这个互斥对象。
当某个线程尝试锁住互斥对象 locker
时,会先检查是否有其他线程锁住了互斥对象 locker
,如果有,则等待其他线程释放互斥对象 locker
。如果没有,则立即锁住互斥对象 locker ,并继续执行。
当 guard
对象执行 unlock
方法时,会解锁互斥对象。如果此时正有某个线程等待这个互斥对象解锁,那么这个线程可以继续执行它的工作了。是不是有点像排队上厕所呢?
渲染动画
渲染 Lottie
动画的核心代码如下所示:
// #include "modules/skottie/include/Skottie.h"
// #include <thread>void animateLottie() {auto t = std::thread([&]() {std::wstring imgPath = L"D:\\project\\SkiaInAction\\动画Lottie\\demo4.json";auto pathStr = wideStrToStr(imgPath);sk_sp<skottie::Animation> animation = skottie::Animation::MakeFromFile(pathStr.data());auto size = animation->size();auto fps = animation->fps();auto duration = std::chrono::milliseconds((int)(1000 / fps));auto frameCount = animation->duration()*fps;auto frameIndex{ 0 };while (true){auto x = (w - size.fWidth) / 2;auto y = (h - size.fHeight) / 2;auto rect = SkRect::MakeXYWH(x, y, size.fWidth, size.fHeight);animation->seekFrame(frameIndex);std::unique_lock guard(locker);auto canvas = getCanvas();canvas->clear(0xff000000);animation->render(canvas, &rect);guard.unlock();InvalidateRect(hwnd, nullptr, false);std::this_thread::sleep_for(duration);frameIndex += 1; if (frameIndex > frameCount) {frameIndex = 0;}}});t.detach();
}
这段代码有以下几点需要注意:
animateLottie
方法也是在窗口创建成功之后执行的。与播放 gif 动画一样,也是在一个子线程中渲染每一帧
Lottie
动画的。skottie::Animation::MakeFromFile
方法负责从一个json
文件创建动画对象,动画对象的类型为:skottie::Animation
动画对象的
size
方法用于获取此动画的大小,fps
方法用于获取此动画的帧率
(每秒播放多少帧),duration
方法用于获取动画播放一次使用的时间(秒)变量
duration
是每帧动画的时间间隔(毫秒),这是根据动画的帧率
算出来的。变量
frameCount
是帧数量,这是用帧率
和播放时间
算出来的。变量
frameIndex
是当前播放的帧(第几帧)。循环伊始,首先计算动画的位置和大小,因为用户可能随时调整窗口的大小,所以要在每次循环时都计算它们。
动画对象的
seekFrame
方法用于设置当前播放第几帧,由于每一帧都是根据 json 文件计算出来的,所以这里可以使用小数来设置当前播放第几帧。渲染动画前先加线程锁,因为这里要修改画布像素数据,这与前面介绍的线程锁是呼应的。
getCanvas
方法用于获取画布对象,它能返回一个与窗口对应的画布(SkCanvas
)对象。在渲染动画的每一帧前,必须清空了画布,因为 lottie 动画每一帧都可以独立渲染成一幅图像,如果不做清空工作,则可能和上一帧图像叠加到一起了。
这与绘制 gif 动画不同,绘制 gif 动画每一帧 前一定不能清空画布,必须让它们叠加渲染才行。
动画对象的
render
方法负责在rect
指定的位置和区域绘制一帧图像。绘制完成后立即解锁互斥锁(以后的工作不再涉及到线程竞争访问资源的问题了)。请求重绘窗口、等待帧与帧之间的时间间隔,判断是否到了最后一帧等工作与渲染 gif 动画相同,这里就不再赘述了。
获取画布对象
本示例中,由于画布对象与窗口像素(surfaceMemory
)有关,所以要单独处理,如下代码所示:
SkCanvas* getCanvas() {static std::unique_ptr<SkCanvas> canvas;if (!canvas.get()) {SkImageInfo info = SkImageInfo::MakeN32Premul(w, h);canvas = SkCanvas::MakeRasterDirect(info, surfaceMemory, 4 * w);return canvas.get();}auto info = canvas->imageInfo();if (w != info.width() || h != info.height()) {SkImageInfo newInfo = SkImageInfo::MakeN32Premul(w, h);canvas = SkCanvas::MakeRasterDirect(newInfo, surfaceMemory, 4 * w);}return canvas.get();
}
这段代码中,在方法内定义了一个静态变量 canvas
。
方法内的静态变量的生命周期与应用程序的生命周期相同,不仅仅是其所在方法被调用期间,只有到程序结束时才会被销毁。
虽然这个静态变量的生命周期与整个应用程序的声明周期相同,但其作用域仍然局限于声明它的函数或方法内。这意味着在该方法之外无法直接访问这个静态变量。
每次函数被调用时,这个静态局部变量会保留其上次函数调用结束时的值。
当首次调用 getCanvas
方法时(渲染第一帧时),canvas.get()
方法返回的是一个空指针,所以会执行第一个if分支
,根据窗口大小创建 SkCanvas
对象。
当再次调用 getCanvas
方法时(渲染第二帧或第N帧时),canvas.get()
方法返回的不是空指针,所以第一个if分支
不会执行,
此时会判断 canvas
的大小是否与窗口大小一致,如果一致(用户没有改变窗口大小),则直接返回之前创建的canvas
指针。
如果不一致(用户改变了窗口大小),则根据新的窗口大小和 surfaceMemory
创建 canvas 对象。
值得注意的是,这个方法是在子线程中执行的,是受线程锁保护的,执行这个方法时,永不会执行 WM_SIZE
处的逻辑。
运行程序,得到的结果如下图所示: