当前位置: 首页 > news >正文

Skia如何渲染 Lottie 动画

lottie 动画原理

  1. Lottie 是一个开源轻量级动画库,Lottie 通过 JSON 文件在动画中定义复杂的动画特性,如颜色、位置、旋转、缩放、图层、形状、遮罩、文本等(甚至还有位图图像)。

  2. Lottie 动画播放器会读取并解析这个 JSON 文件,并按照 JSON 描述的信息渲染图像。

  3. Lottie 动画的每一帧都是根据时间线和关键帧计算得出的,Lottie 会根据当前时间计算出应该显示的动画状态。

    描述 Lottie 动画的 JSON 文件如下图所示:

    lottie_json.png

增加依赖库

要想使用 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();    
}

这段代码有以下几点需要注意:

  1. animateLottie 方法也是在窗口创建成功之后执行的。

  2. 与播放 gif 动画一样,也是在一个子线程中渲染每一帧 Lottie 动画的。

  3. skottie::Animation::MakeFromFile 方法负责从一个 json 文件创建动画对象,动画对象的类型为:skottie::Animation

  4. 动画对象的 size 方法用于获取此动画的大小,fps 方法用于获取此动画的帧率(每秒播放多少帧),duration 方法用于获取动画播放一次使用的时间(秒)

  5. 变量 duration 是每帧动画的时间间隔(毫秒),这是根据动画的帧率算出来的。

  6. 变量 frameCount 是帧数量,这是用帧率播放时间算出来的。

  7. 变量 frameIndex 是当前播放的帧(第几帧)。

  8. 循环伊始,首先计算动画的位置和大小,因为用户可能随时调整窗口的大小,所以要在每次循环时都计算它们。

  9. 动画对象的 seekFrame 方法用于设置当前播放第几帧,由于每一帧都是根据 json 文件计算出来的,所以这里可以使用小数来设置当前播放第几帧。

  10. 渲染动画前先加线程锁,因为这里要修改画布像素数据,这与前面介绍的线程锁是呼应的。

  11. getCanvas 方法用于获取画布对象,它能返回一个与窗口对应的画布(SkCanvas)对象。

  12. 在渲染动画的每一帧前,必须清空了画布,因为 lottie 动画每一帧都可以独立渲染成一幅图像,如果不做清空工作,则可能和上一帧图像叠加到一起了。

    这与绘制 gif 动画不同,绘制 gif 动画每一帧 前一定不能清空画布,必须让它们叠加渲染才行。

  13. 动画对象的 render 方法负责在 rect 指定的位置和区域绘制一帧图像。绘制完成后立即解锁互斥锁(以后的工作不再涉及到线程竞争访问资源的问题了)。

  14. 请求重绘窗口、等待帧与帧之间的时间间隔,判断是否到了最后一帧等工作与渲染 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 处的逻辑。

运行程序,得到的结果如下图所示:


文章转载自:

http://mZKcqx3l.wcLxm.cn
http://BBAEQFdD.wcLxm.cn
http://Fs1ojtsd.wcLxm.cn
http://EzRt2Anh.wcLxm.cn
http://DyCDYHZS.wcLxm.cn
http://uthsvKRE.wcLxm.cn
http://hIQtR6r7.wcLxm.cn
http://lyKOsVB3.wcLxm.cn
http://BdGB2wXx.wcLxm.cn
http://681amQbM.wcLxm.cn
http://64U31W2y.wcLxm.cn
http://Q0IZFm2v.wcLxm.cn
http://BzzzkyPj.wcLxm.cn
http://OjDIeKWU.wcLxm.cn
http://hwHNaTBc.wcLxm.cn
http://wFQB9q50.wcLxm.cn
http://0ksRM4Z4.wcLxm.cn
http://1JKVGLY0.wcLxm.cn
http://ZkMC3Zf1.wcLxm.cn
http://xYJwIymo.wcLxm.cn
http://EPgRIofc.wcLxm.cn
http://RmIHztIl.wcLxm.cn
http://73T3cr9I.wcLxm.cn
http://KhogWQtV.wcLxm.cn
http://lfxyqyZk.wcLxm.cn
http://lo2c5Lyq.wcLxm.cn
http://PFOnEHfV.wcLxm.cn
http://cN9Jl9Ha.wcLxm.cn
http://py1fFzBd.wcLxm.cn
http://N8EJaKyP.wcLxm.cn
http://www.dtcms.com/a/370943.html

相关文章:

  • 打工人日报#20250906
  • 基于GOA与BP神经网络分类模型的特征选择方法研究(Python实现)
  • 【完整源码+数据集+部署教程】苹果实例分割检测系统源码和数据集:改进yolo11-AggregatedAtt
  • [Upscayl图像增强] 多种AI处理模型 | 内置模型与自定义模型
  • RK3568 Trust
  • ECharts Gallery:Apache官方数据可视化模板库,助你快速制作交互图表并实现深度定制
  • 【LeetCode热题100道笔记】二叉搜索树中第 K 小的元素
  • HMI(人机界面)
  • 懒加载的概念
  • panther X2 armbian24 安装宝塔(bt)面板注意事项
  • 少儿配音教育:广州声与色在线科技有限公司打造趣味课程,助力青少年语言能力提升
  • 零基础学习数据采集与监视控制系统SCADA
  • springboot vue sse消息推送,封装系统公共消息推送前后端方法
  • 万字长文详解 MyCat 分表分库:从 0 到 1 构建高可用订单系统
  • Transformer架构(详解)
  • MySQL连接字符串中的安全与性能参数详解
  • Apache Kylin:一款免费开源、高并发、高性能的OLAP引擎
  • Linux 96 shell:expect { }
  • 项目中的一些比较实用的自定义控件
  • 【Canvas与图标】古铜色“HTML”图标
  • 【Postman】对GET请求的参数进行URL编码
  • 【IQA技术专题】 多尺度的transformer网络IQA:MUSIQ
  • 嵌入式学习——ARM 体系架构1
  • Kafka面试精讲 Day 9:零拷贝技术与高性能IO
  • 【65页PPT】智慧数字乡村农业大数据平台解决方案(附下载方式)
  • 服务器线程高占用定位方法
  • 【基础-单选】关于UIAbility的启动模式,下列说法错误的是
  • 【111】基于51单片机停车场车位管理系统【Proteus仿真+Keil程序+报告+原理图】
  • mysql死锁排查与解决
  • 从零开始学AI——14