专栏首页Android原创在Android中显示APNG动图
原创

在Android中显示APNG动图

一、什么是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

使用示例:

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()方法中进行的;

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

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

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

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

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

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

我来说两句

0 条评论
登录 后参与评论

相关文章

  • 小程序开发踩坑指南

    小程序组件分为原生组件和非原生组件,原生组件属于客户端的组件,在WebView的渲染流程之外的,且层级在所有非原生组件之上(无论你如何改z-index都没用的)...

    Clayman Twinkle
  • 优雅的监听onActivityResult

    此时,我们可能会用到EventBus这种全局分发事件的方式来处理,但种感觉不够优雅。

    Clayman Twinkle
  • 用NDK编译FFmpeg4.1.3

    API、CPU、NDK以及TOOLCHAIN这个路径最后的文件夹名称(Mac下是darwin-x86_64、linux可能叫linux-x86_64、Windo...

    Clayman Twinkle
  • Leetcode 20. Valid Parentheses

    版权声明:博客文章都是作者辛苦整理的,转载请注明出处,谢谢! https://blog.csdn....

    Tyan
  • LeetCode 657 Robot Return to Origin

    这道题很简单,只需要假设当前节点是 0, 0,定义两个变量, i 和 j,默认值都为 0,每当向上 i + 1,向下 i - 1,向右 j + 1,向左 j -...

    一份执着✘
  • CCF考试——201612-2工资计算

      小明的公司每个月给小明发工资,而小明拿到的工资为交完个人所得税之后的工资。假设他一个月的税前工资(扣除五险一金后、未扣税前的工资)为S元,则他应交的个人所得...

    AI那点小事
  • 使用vb脚本让电脑自动加入域源码

    在企业用户中,一大部分用户都加入了域,用于公司的安全管理。加入域对于管理比较方便。但是新增的设备或者用户如何快速的加入已知的域呢?很简单,看代码:

    业余草
  • ES6 学习笔记之对象的拓展

    ES6 简洁方法后与一些面向对象的高级语言(如C++)差不多,函数名+参数+花括号。另外注意简洁写法的属性名是按字符串解析的。方法的属性名可以是一些关键字,由于...

    我与梦想有个约会
  • ASP.NET Core WebListener 服务器

    原文地址:WebListener server for ASP.NET Core By Tom Dykstra, Chris Ross WebListener是...

    潘成涛
  • Appium+python自动化(五)- 模拟器(超详解)

      Appium是做安卓自动化的一个比较流行的工具,对于想要学习该工具但是又局限于或许当前有些小伙伴没 android 手机来说,可以通过安卓模拟器来解决该问题...

    北京-宏哥

扫码关注云+社区

领取腾讯云代金券