APNG(Animated Portable Network Graphics)是一个基于PNG(Portable Network Graphics)的位图动画格式,用途类似GIF,其诞生的目的是为了替代老旧的 GIF 格式。
说了这么多,它替代GIF?那有什么优势呢?
总结下来有以下几点:
(1)GIF最多支持 8 位 256 色,而APNG支持24 位真彩色和alpha通道,不会出现像GIF的锯齿;
(2)APNG图通过优化,图片大小和GIF差不多,甚至小一点。
这里使用了一个开源库来解析加载APNG图,apng-view
使用示例:
String url = "http://xxx.png";
imageView.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
ApngDrawable apngDrawable = ApngDrawable.getFromView(v);
if (apngDrawable == null) return;
if (apngDrawable.isRunning()) {
apngDrawable.stop(); // 停止播放动画
} else {
apngDrawable.setNumPlays(3); // 动画循环次数
apngDrawable.start(); // 开始播放动画
}
}
});
ApngImageLoader.getInstance().displayImage(url, imageView);
效果图:
先看看apng-view实现过程:
(1)图片的下载/加载:通过图片加载开源库Android-Universal-Image-Loader进行图片的下载/加载;
(2)通过下载成功后的图片文件构造ApngDrawable
对象;
(3)最后通过imageView.setImageDrawable
将ApngDrawable
和ImageView
绑定到一起;
所以,这个apng-view库中,最核心的就是ApngDrawable
这个类了。
那么这个ApngDrawable
里面究竟做了什么骚操作呢?
(1)prepare
先从图片文件读取这里说起,图片读取是在ApngDrawable
这个prepare()
方法中进行的;
// 文件路径:com/github/sahasbhop/apngview/ApngDrawable.java
private void prepare() {
// 1. 构造File对象
String imagePath = getImagePathFromUri();
if (imagePath == null) return;
baseFile = new File(imagePath);
if (!baseFile.exists()) return;
// 2. 读取一个APNG文件并尝试将其拆分帧
ApngExtractFrames.process(baseFile);
// 3. 读取APNG文件信息
readApngInformation(baseFile);
isPrepared = true;
}
代码的步骤,说得云里雾里的,且看这个ApngExtractFrames.process()
方法里具体实现吧;
// 文件路径:com/github/sahasbhop/apngview/assist/ApngExtractFrames.java
public static int process(final File orig) {
PngReaderBuffered pngr = new PngReaderBuffered(orig); // 这里应该是在读取了这个图片
pngr.end();
return pngr.frameIndex + 1;
}
这里用到了一个可以用来读取PNG的开源库pngj,大概知道这是在读图片了,读的过程中做了什么操作呢?
// 文件路径:com/github/sahasbhop/apngview/assist/ApngExtractFrames.java
protected void postProcessChunk(ChunkReader chunkR) {
//......
try {
String id = chunkR.getChunkRaw().id;
PngChunk lastChunk = chunksList.getChunks().get(chunksList.getChunks().size() - 1);
if (id.equals(PngChunkFCTL.ID)) { // FCTL代表,每一帧开头
frameIndex++;
frameInfo = ((PngChunkFCTL) lastChunk).getEquivImageInfo();
startNewFile(); // 开始新建一个文件,进行输入
}
if (id.equals(PngChunkFDAT.ID) || id.equals(PngChunkIDAT.ID)) { // 图像数据块
// 忽略这里的处理细节....
}
if (id.equals(PngChunkIEND.ID)) { // 这一帧结束
if (fo != null)
endFile(); // 结束这个文件输入,对应startNewFile方法
}
} catch (Exception e) {
throw new PngjException(e);
}
}
大概逻辑就是将APNG
图片读取后,拆解生成多个帧文件,存放起来;
接下来看下ApngDrawable#prepare()
中步骤三readApngInformation
具体做了什么吧;
// 文件路径:com/github/sahasbhop/apngview/ApngDrawable.java
private void readApngInformation(File baseFile) {
PngReaderApng reader = new PngReaderApng(baseFile); // 又读取了一次文件,这里和步骤二或许可以合并优化下
reader.end();
List<PngChunk> pngChunks = reader.getChunksList().getChunks(); // 拿到图片所有数据块
PngChunk chunk;
for (int i = 0; i < pngChunks.size(); i++) {
chunk = pngChunks.get(i);
if (chunk instanceof PngChunkACTL) {
numFrames = ((PngChunkACTL) chunk).getNumFrames(); //获取总帧数
if (numPlays > 0) {
//......
} else {
numPlays = ((PngChunkACTL) chunk).getNumPlays(); // 获取循环播次数
}
} else if (chunk instanceof PngChunkFCTL) {
fctlArrayList.add((PngChunkFCTL) chunk); // 收集帧动画控制的数据块
}
}
}
这个过程大体上就是在解析这个APNG文件的基本信息。
(2)start
那么到了这个动图的start
阶段了
// 文件路径:com/github/sahasbhop/apngview/ApngDrawable.java
public void start() {
if (!isRunning()) {
isRunning = true;
currentFrame = 0;
if (!isPrepared) {
prepare();
}
if (isPrepared) {
run();
if (apngListener != null) apngListener.onAnimationStart(this);
} else {
stop();
}
}
}
这个start
方法里其实也没做什么,只是通过标志位去判断执行prepare
、run
、stop
方法而已;
(3)run
动图播放的核心方法之一run
;
public void run() {
if (showLastFrameOnStop && numPlays > 0 && currentLoop >= numPlays) {
stop(); // 轮播次数用完且到最后一帧了就停止播放了
return;
}
if (currentFrame < 0) {
currentFrame = 0;
} else if (currentFrame > fctlArrayList.size() - 1) {
currentFrame = 0; // 因为没轮播完,所以当前帧序号从0开始
}
PngChunkFCTL pngChunk = fctlArrayList.get(currentFrame);
int delayNum = pngChunk.getDelayNum();
int delayDen = pngChunk.getDelayDen();
int delay = Math.round(delayNum * DELAY_FACTOR / delayDen);
scheduleSelf(this, SystemClock.uptimeMillis() + delay); // 定时器,循环走run
invalidateSelf(); // 通知draw再一次了
}
(4)stop
暂停动图的方法
public void stop() {
if (isRunning()) {
currentLoop = 0;
unscheduleSelf(this); // 停止定时器
isRunning = false;
if (apngListener != null) apngListener.onAnimationEnd(this);
}
}
(5)draw
动图播放的核心方法之二draw
;
APNG图是怎么给绘制出来的呢?
public void draw(Canvas canvas) {
if (currentFrame <= 0) {
drawBaseBitmap(canvas);
} else {
drawAnimateBitmap(canvas, currentFrame);
}
if (!showLastFrameOnStop && numPlays > 0 && currentLoop >= numPlays) {
stop(); // 不轮播了就停止
}
if (numPlays > 0 && currentFrame == numFrames - 1) { // 最后一帧了
currentLoop++; // 循环次数加一
if (apngListener != null) apngListener.onAnimationRepeat(this);
}
currentFrame++;
}
绘制动图的核心代码在drawAnimateBitmap
方法里:
private void drawAnimateBitmap(Canvas canvas, int frameIndex) {
Bitmap bitmap = getCacheBitmap(frameIndex); // 这里对帧bitmap做了缓存
if (bitmap == null) {
bitmap = createAnimateBitmap(frameIndex); // 没缓存直接通过帧文件创建bitmap
cacheBitmap(frameIndex, bitmap); // 缓存!
}
if (bitmap == null) return;
RectF dst = new RectF(0, 0,mScaling * bitmap.getWidth(),mScaling * bitmap.getHeight());
canvas.drawBitmap(bitmap, null, dst, paint); // 绘制
}
至此,核心代码逻辑大致分析差不多。
总结下来ApngDrawable
核心逻辑大致分三步:
(1)APNG拆分成多个帧文件:图片文件通过开源库pngj以PngChunk
的数据结构读到内存,然后遍历数据块,将APNG每一帧数据保存到本地文件中;
(2)读取APNG基本图片信息;
(3)开启定时器逐帧读取文件(读完后缓存一次)生成Bitmap绘制到View上;
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。