前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >在Android中显示APNG动图

在Android中显示APNG动图

原创
作者头像
Clayman Twinkle
发布2019-04-04 18:16:46
16.6K1
发布2019-04-04 18:16:46
举报
文章被收录于专栏:Android原创

一、什么是APNG?

APNG(Animated Portable Network Graphics)是一个基于PNG(Portable Network Graphics)的位图动画格式,用途类似GIF,其诞生的目的是为了替代老旧的 GIF 格式。

二、与GIF对比

说了这么多,它替代GIF?那有什么优势呢?

总结下来有以下几点:

(1)GIF最多支持 8 位 256 色,而APNG支持24 位真彩色和alpha通道,不会出现像GIF的锯齿;

(2)APNG图通过优化,图片大小和GIF差不多,甚至小一点。

三、在Android中显示APNG动图

这里使用了一个开源库来解析加载APNG图,apng-view

使用示例:

代码语言:txt
复制
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源码分析

实现过程

先看看apng-view实现过程:

实现过程
实现过程

(1)图片的下载/加载:通过图片加载开源库Android-Universal-Image-Loader进行图片的下载/加载;

(2)通过下载成功后的图片文件构造ApngDrawable对象;

(3)最后通过imageView.setImageDrawableApngDrawableImageView绑定到一起;

所以,这个apng-view库中,最核心的就是ApngDrawable这个类了。

那么这个ApngDrawable里面究竟做了什么骚操作呢?

源码解读

(1)prepare

先从图片文件读取这里说起,图片读取是在ApngDrawable这个prepare()方法中进行的;

代码语言:txt
复制
// 文件路径: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()方法里具体实现吧;

代码语言:txt
复制
// 文件路径: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,大概知道这是在读图片了,读的过程中做了什么操作呢?

代码语言:txt
复制
// 文件路径: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具体做了什么吧;

代码语言:txt
复制
// 文件路径: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阶段了

代码语言:txt
复制
// 文件路径: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方法里其实也没做什么,只是通过标志位去判断执行preparerunstop方法而已;

(3)run

动图播放的核心方法之一run

代码语言:txt
复制
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

暂停动图的方法

代码语言:txt
复制
public void stop() {
	if (isRunning()) {
		currentLoop = 0;
		unscheduleSelf(this); // 停止定时器
		isRunning = false;
		if (apngListener != null) apngListener.onAnimationEnd(this);
	}
}

(5)draw

动图播放的核心方法之二draw

APNG图是怎么给绘制出来的呢?

代码语言:txt
复制
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方法里:

代码语言:txt
复制
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拆分成多个帧文件:图片文件通过开源库pngjPngChunk的数据结构读到内存,然后遍历数据块,将APNG每一帧数据保存到本地文件中;

(2)读取APNG基本图片信息;

(3)开启定时器逐帧读取文件(读完后缓存一次)生成Bitmap绘制到View上;

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 一、什么是APNG?
  • 二、与GIF对比
  • 三、在Android中显示APNG动图
  • 四、apng-view源码分析
    • 实现过程
      • 源码解读
      领券
      问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档