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
動畫.