gif
动画原理
先了解一下gif
动画的原理:
gif
动画由一系列静态图像
(或叫帧)组成.这些图像按特定的顺序排列
,每一帧
都代表动画中的一个瞬间,帧图像
是支持透明
的.
每两帧之间
有指定的时间间隔
(一般小于60
毫秒),gif
播放器每渲染一帧静态图像
后,即等待此时间间隔
,依此逻辑不断循环渲染每一帧
,这样就是一个动画了(基于人眼的视觉暂留现象)
大部分
的gif动画文件
是基于一个压缩算法
生成的:如果前一帧
中包含的一部分像素
与后一帧
中包含的像素相同,则后一帧
中不必存储这些像素
,以此减少文件体积
.
也即,这类gif
动画的第一帧是一个完整的图像
,后面每一帧
存储的像素都是这一帧与前一帧不同像素数据
,没有相同像素数据
.
这类gif
动画要求播放器渲染每一帧
时都是在前一帧的基础上渲染的(叠加在前一帧
上面).
在窗口中播放gif
动画
在窗口中播放动画
的原理:每渲染一帧动画
即重画一次窗口
.
因为gif
动画帧与帧
之间等待时间
一般都比较短(此例动画帧间隔时间为50
毫秒).所以得修改窗口的基础代码
:
按全局变量
设置surfaceMemory
,并在创建窗口
成功后,即初化它指向的内存空间
.
每次执行绘画
方法后,不再释放surfaceMemory
指向的内存空间
,以避免每次重画都要重新申请内存
,造不必要的CPU
消耗.
改变窗口大小
时,再重置surfaceMemory
指向的内存空间
.
按全局变量
设置窗口句柄
,HWND hwnd
,这样在渲染每一帧
时请求重画窗口
.
具体见全部示例代码
.来看一下播放gif
动画的示例代码
:
//#include <thread>
SkBitmap* frameBitmap;
void animateGif()
{std::wstring imgPath = L"D:\\project\\SkiaInAction\\动画Gif\\demo.gif";auto pathStr = wideStrToStr(imgPath);std::unique_ptr<SkFILEStream> stream = SkFILEStream::Make(pathStr.data());std::unique_ptr<SkCodec> codec = SkCodec::MakeFromStream(std::move(stream));frameBitmap = new SkBitmap();auto t = std::thread([](std::unique_ptr<SkCodec> codec) {auto imgInfo = codec->getInfo().makeColorType(kN32_SkColorType);frameBitmap->allocN32Pixels(imgInfo.width(), imgInfo.height());int frameCount = codec->getFrameCount();std::vector<SkCodec::FrameInfo> frameInfo = codec->getFrameInfo();SkCodec::Options option;option.fFrameIndex = 0;option.fPriorFrame = -1;while (true){auto start = std::chrono::system_clock::now();codec->getPixels(imgInfo, frameBitmap->getPixels(), imgInfo.minRowBytes(), &option);InvalidateRect(hwnd, nullptr, false);auto end = std::chrono::system_clock::now();auto tSpan = end - start;auto ms = std::chrono::duration_cast<std::chrono::milliseconds>(tSpan);auto msCount = frameInfo[option.fFrameIndex].fDuration - ms.count();auto duration = std::chrono::milliseconds(msCount);std::this_thread::sleep_for(duration);if (option.fFrameIndex == frameCount - 1){option.fPriorFrame = -1;option.fFrameIndex = 0;}else{option.fPriorFrame = option.fPriorFrame + 1;option.fFrameIndex = option.fFrameIndex + 1;}}}, std::move(codec));t.detach();
}
这段代码
有以下几点注意:
1,animateGif
方法并不是在重画窗口
时执行的,而是在创建窗口
成功后执行的.
2,frameBitmap
是一个SkBitmap*
类型的全局变量
.用来存储一帧像素数据
.
3,创建了一个新的线程
以解码gif
图像中的每一帧
的数据
,这样做主要是为了不让解码工作
影响应用的主线程
.
4,每时每刻都在解码(包括线程等待std::this_thread::sleep_for
),如果不在一个独立的线程
中放置该工作
,主线程就会卡死.
5,codec
解码器的类型是std::unique_ptr<SkCodec>
(不能复制),所以不能在线程的匿名函数
中抓它,必须把它移动(std::move
)到匿名函数
内才可以.
6,通过线程对象
的解附
方法按后台线程
设置线程,让其自行运行(线程对象
的join
方法会阻塞主线程
),生产环境
下需自行增加处理异常
,释放线程资源
等保护性代码
.
刚开始执行线程方法
时,执行了一系列
准备工作:
得到ImageInfo
信息.
解码器(codec
)的getInfo
方法得到的ImageInfo
对象是gif
图像默认定义的,它有可能并不适合用来解码帧数据
到SkBitmap
对象.
因此基于它的基础信息
(长,宽等),创建了一个新的ImageInfo
对象,该对象的颜色
类型为:kN32_SkColorType
.
初化frameBitmap
,全局变量
的只能存储一帧数据
的内存空间
.
得到gif
文件中的帧数量
:codec->getFrameCount()
得到帧信息:std::vector<SkCodec::FrameInfo>frameInfo=codec->getFrameInfo();
SkCodec::FrameInfo
包含了很多与帧有关的信息,其中最重要的就是帧的等待时间
(单位:毫秒).
初化SkCodec::Options
SkCodec::Options
对象中fFrameIndex
表示当前正在播放第几帧
(默认为第0帧),fPriorFrame
表示上一帧是第几帧
.
准备好这些工作之后,开始正式解码gif
图像.
循环播放``gif
,所以解码工作
是在一个不会停止的当
循环中的.
在一些低端电脑上,解码工作较长,所以记录了该时间消耗
.
该工作使用std::chrono::system_clock
完成,得到的时间间隔单位
为毫秒.
解码器codec
的getPixels
方法负责把选项
中指定的帧解码到frameBitmap
指向的内存空间
中.
frameBitmap->getPixels()
得到的是frameBitmap
持有的像素数据
的地址.
InvalidateRect
是窗口接口
提供的方法,它负责向窗口发送重画消息
.
执行此方法
后,窗口将收到WM_PAINT
消息.
根据frameInfo
里记录的帧信息
,让线程等待一段时间再解码下一帧
.
注意这里在帧等待时间
(fDuration
)上减去了解码消耗的时间
,这样做可保证,程序即使在一些低端设备上也能流畅播放.
最后更新选项
里的当前帧
信息和上一帧信息
.
判断是否解码到了最后一帧
,如果是,则按第0帧设置.如果不是,则按下一帧设置
,接着解码下一帧.
整个循环中,最关键的信息
就是:在不断的改变frameBitmap
指向的内存空间的数据
,而且每改变一次(解码一帧),即请求一次重画窗口
.
重画方法(绘画
方法)的关键代码
为:
SkImageInfo info = SkImageInfo::MakeN32Premul(w, h);
auto canvas = SkCanvas::MakeRasterDirect(info, surfaceMemory, 4 * w);
if (frameBitmap) {auto x = (w - frameBitmap->width()) / 2;auto y = (h - frameBitmap->height()) / 2;canvas->writePixels(*frameBitmap, x, y);
}
这段代码很简单
,其主要意图是在窗口正中间
绘画frameBitmap
.因为每次重画frameBitmap
里的像素数据
都是一帧新的图像
,所以gif
就在窗口中播放起来了.
程序运行结果如下图所示:
程序中使用的gif
图像源自:github.com/ImageOptim/...
注意
gif
动画虽然兼容很好,但效果不好.
其最多只能处理256
色,不适合真彩色图片
.gif
虽然支持透明
效果,但其透明
效果在高分屏
上表现很差,图像颗粒感很强
,有锯齿
.
除gif
外,还有很多其他格式
的文件支持动画
,比如webp,apng,svga,lottie
等.
用本节示例代码
所展示的方式解码,播放大部分
非向量格式的动画文件
.
但像svga
,lottie
此类向量格式
的动画文件
,就需要写其他代码
来渲染了.
有时并不能根据一个文件的扩展名
来判断该文件的格式
.
Skia
解码器SkCodec
的getEncodedFormat
方法可取文件的真实
格式,如下代码所示:
//#include "include/codec/SkEncodedImageFormat.h"
std::unique_ptr<SkFILEStream> stream = SkFILEStream::Make(pathStr.data());
std::unique_ptr<SkCodec> codec = SkCodec::MakeFromStream(std::move(stream));
auto imgFormat = codec->getEncodedFormat();
if(imgFormat == SkEncodedImageFormat::kGIF){//......
}
在本文示例代码
中,通过一个独立的线程
来解码gif动画文件
中的每一帧
图像(codec->getPixels
),每解码一帧图像
即重画一次窗口
(InvalidateRect
),重画窗口时,会在窗口正中间
渲染解码得到的图像
,重画完成之后,等待一段时间(frameInfo[option. fFrameIndex].fDuration
)再解码下一帧图像
(option.fFrameIndex+=1
).
实际上Skia
提供了一个类型:modules\skresources\src\SkAnimCodecPlayer.h
来帮助播放动画
,大家也可用该类型的代码
实现来播放gif
动画.