gif的解析,显示,在google上有一位作者开放了源码,于是我也下载研究了下,的确可用, 不过些许问题,如,解析大一些的文件就会出现oome。
https://github.com/archko/GifView.git 这个是用c写的,android底层的gif也是相同的lib.
ndk编译时需要指定android-8以上,否则图像会有问题.
5.0以下系统可用,以上的暂时不可用.
我修改了下源码,模拟器上会因为图片过大无法解析。一张gif2.07m,解析完全 会有179帧,如果用png,100质量存储总有4.2m,在手机上只能解析128帧,其它因为内存不足无法解析,所以还是没有优化好。但是其它的图片到现在多数都解析成功了 ,有些不只是大小的问题,还关系到每帧高宽。3m+的gif图片解析也可以,但是那张2m的就不行。
废话不多了,现在介绍我的修改过程。
首先,原作者的地址为:http://code.google.com/p/gifview/
我只说我修改的部分:
先看decoder:
private Bitmap image; // current frame
private Bitmap lastImage; // previous frame
一个是存储当前的帧,一个存储上一帧.
为什么要存储上一帧呢?我也不大懂,猜是因为上一帧与当前的帧关联大,许多地方可以利用的.
从作者的代码来看,它提供了即时解析,解析一帧显示一帧.但是我考虑到,一张gif图片可能不是只从头到尾看一次,用户可能会想像pc的浏览器那样,动画完成后再从头开始,所以,我就把这些解析的帧存储在一个list里,解析完成后把list传到view中,然后就用线程处理画图的问题.
readContents()是读取内容的主要方法.其中的readImage就是读取图片了.而其它的方法读一些其它信息.我对gif了解不太多,也不能说出每个方法具体干什么的.
看原来的
private void readImage() {
Log.d(TAG, "readImage.");
ix=readShort(); // (sub)image position & size
iy=readShort();
iw=readShort();
ih=readShort();
int packed=read();
lctFlag=(packed&0x80)!=0; // 1 - local color table flag
interlace=(packed&0x40)!=0; // 2 - interlace flag
// 3 - sort flag
// 4-5 - reserved
lctSize=2<<(packed&7); // 6-8 - local color table size
if (lctFlag) {
lct=readColorTable(lctSize); // read table
act=lct; // make local table active
} else {
act=gct; // make global table active
if (bgIndex==transIndex) {
bgColor=0;
}
}
int save=0;
if (transparency) {
save=act[transIndex];
act[transIndex]=0; // set transparent color if specified
}
if (act==null) {
status=STATUS_FORMAT_ERROR; // no color table defined
}
if (err()) {
return;
}
decodeImageData(); // decode pixel data
skip();
if (err()) {
return;
}
frameCount++;
// create new image to receive frame data
image=Bitmap.createBitmap(width,height,Config.ARGB_4444);
// createImage(width, height);
setPixels(); // transfer pixel data to image
if (gifFrame==null) {
gifFrame=new GifFrame(image,delay);
currentFrame=gifFrame;
} else {
GifFrame f=gifFrame;
while (f.nextFrame!=null) {
f=f.nextFrame;
}
f.nextFrame=new GifFrame(image,delay);
}
// frames.addElement(new GifFrame(image, delay)); // add image to frame
// list
if (transparency) {
act[transIndex]=save;
}
resetFrame();
action.parseOk(true,frameCount);
}
可以看到image,这些建一个图,图片的生成是在setPixels里面的.
这个方法做的事比较多了.
private void setPixels() {
Log.d(TAG, "setPixels.");
int[] dest=new int[width*height];建一个颜色数组.然后把它填充,就可以画出一张图片了.
里面的if (lastImage!=null) {
lastImage.getPixels(dest,0,width,0,0,width,height);
// copy pixels
if (lastDispose==2) {
// fill last image rect area with background color
int c=0;
if (!transparency) {
c=lastBgColor;
}
for (int i=0;i<lrh;i++) {
int n1=(lry+i)*width+lrx;
int n2=n1+lrw;
for (int k=n1;k<n2;k++) {
dest[k]=c;
}
}
}
}
就是利用了上一帧的内容了.
然后再设置新的内容:
if (line<height) {
int k=line*width;
int dx=k+ix; // start of line in dest
int dlim=dx+iw; // end of dest line
if ((k+width)<dlim) {
dlim=k+width; // past dest edge
}
int sx=i*iw; // start of line in source
while (dx<dlim) {
// map color and insert in destination
int index=((int)pixels[sx++])&0xff;
int c=act[index];
if (c!=0) {
dest[dx]=c;
}
dx++;
}
}
最后创建 图片:
image=Bitmap.createBitmap(dest,width,height,Config.ARGB_4444);
然后它的帧是像c++那样,链接的,当前的帧有一个指针指向下一帧:
public GifFrame nextFrame;
这样,解析完成后可以从第一帧开始,第一帧指针第二帧,第二帧指向第三帧.这样一直下去.
我觉得还不如把这些帧存储在一个list里,按顺序加入,按顺序显示就可以了.
于是做了些小改动:
image=Bitmap.createBitmap(dest, width, height, Config.ARGB_4444);
frame2ArrayList.add(new GifFrame(image, delay));加入到列表中.
而GifFrame也只剩下了bitmap,delay.两个值 了.
建一个存放帧的列表:ArrayList<GifFrame> frame2ArrayList;//存放解析的帧。
如果文件不是太大,解析是没有问题的.
但是内存消耗较大的是setPixels方法.如果把这个方法再优化,就可以解析更大的文件了.
在解析完成后回调view的方法
:
@Override
public void parseOk(boolean parseStatus, int frameIndex) {
Log.d(TAG, "parseOk.frameIndex:"+frameIndex);
decodeFinish(parseStatus, frameIndex);
}
private void decodeFinish(boolean parseStatus, int frameIndex) {
if (!parseStatus) {
Log.d(TAG, "解析失败。");
if (null!=imageLoadCallback) {
imageLoadCallback.loadError();
}
return;
}
if (gifDecoder==null) {
/*if (null!=imageLoadCallback) {
imageLoadCallback.loadError();
}*/
return;
}
gifFrames=gifDecoder.getFrameArrayList();
currImageIdx=0;
frameLength=gifFrames.size();
//if (rect==null) {
mHandler.post(new Runnable() {
@Override
public void run() {
if (null!=imageLoadCallback) {
imageLoadCallback.loadFinish();
}
Bitmap bitmap=gifFrames.get(0).image;
Log.d(TAG, "gif帧间隔为:"+gifFrames.get(0).delay);
setShowDimension(bitmap.getWidth(), bitmap.getHeight());
}
});
//}
gifDecoder.free();
gifDecoder=null;
System.gc();
startAnimate();
}
private void startAnimate() {
Log.d(TAG, "startAnimate.animationType:"+animationType);
switch (animationType) {
case ANIMATION:
Log.d(TAG, "ANIMATION.");
if (frameLength>1) {
if (drawThread==null) {
drawThread=new DrawThread();
} else {
drawThread.interrupt();
drawThread=new DrawThread();
}
drawThread.start();
} else if (frameLength==1) {
reDraw();
}
break;
}
画图:
private class DrawThread extends Thread {
@Override
public void run() {
//Log.d(TAG, "DrawThread.run.");
if (gifFrames==null||frameLength<1) {
return;
}
while (isRun) {
GifFrame frame=gifFrames.get(currImageIdx++);
if (currImageIdx>=frameLength) {
currImageIdx=0;//重新播放。
//break;
}
currentImage=frame.image;
if (pause==false) {
long delay=frame.delay;
//Log.d(TAG, "run.currentImage:"+currentImage+" pause:"+pause+" isRun:"+isRun+" delay:"+delay);
Message msg=mHandler.obtainMessage();
mHandler.sendMessage(msg);
SystemClock.sleep(delay);
} else {
SystemClock.sleep(10);
break;
}
}
Log.d(TAG, "finish run.");
}
}
由于view在销毁时没有停止这个DrawThread线程,所以需要设置isRun与pause.
public void stopAnimate() {
Log.d(TAG, "stopAnimate.");
isRun=false;
pause=true;}
供外部调用.
这样看上去与原来的解析几乎没有差别,但是后来看到Android文档,说drawbitmap方法,中有一个参数为int[]类型的方法,说是会比较快,然后就修改了解析 的方法,返回的不再是Bitmap了,快了一些.因为生成Bitmap会消耗一些资源,慢一点.
于是setPixels方法修改为:
private void setPixels() {
try {
int[] dest=new int[width*height];//这里申请内存会溢出。
// fill in starting image contents based on last image's dispose code
if (lastDispose>0) {
if (lastDispose==3) {
// use image before last
int n=frameCount-2;
if (n>0) {
lastColors=getColorFrame(n-1);
} else {
lastColors = null;
}
}
//Log.d(TAG, "lastColors:"+lastColors+" lastDispose:"+lastDispose+" n:"+frameCount);
if (lastColors!=null) {
//dest=lastColors.colors;
System.arraycopy(lastColors.colors, 0, dest, 0, lastColors.colors.length);
// copy pixels
if (lastDispose==2) {
// fill last image rect area with background color
int c=0;
if (!transparency) {
c=lastBgColor;
}
for (int i=0; i<lrh; i++) {
int n1=(lry+i)*width+lrx;
int n2=n1+lrw;
for (int k=n1; k<n2; k++) {
dest[k]=c;
}
}
}
}
}
/*if (dest==null) {
dest=new int[width*height];
}*/
// copy each source line to the appropriate place in the destination
int pass=1;
int inc=8;
int iline=0;
for (int i=0; i<ih; i++) {
int line=i;
if (interlace) {
if (iline>=ih) {
pass++;
switch (pass) {
case 2:
iline=4;
break;
case 3:
iline=2;
inc=4;
break;
case 4:
iline=1;
inc=2;
}
}
line=iline;
iline+=inc;
}
line+=iy;
if (line<height) {
int k=line*width;
int dx=k+ix; // start of line in dest
int dlim=dx+iw; // end of dest line
if ((k+width)<dlim) {
dlim=k+width; // past dest edge
}
int sx=i*iw; // start of line in source
while (dx<dlim) {
// map color and insert in destination
int index=((int) pixels[sx++])&0xff;
int c=act[index];
if (c!=0) {
dest[dx]=c;
}
dx++;
}
}
}
lastColors=new ColorsFrame(dest, delay, width, height, transparency);
colorsArr.add(lastColors);
} catch (OutOfMemoryError e) {
Log.d(TAG, "colorsArr.size():"+colorsArr.size()+"-"+colorsArr.get(0).colors.length);
status=STATUS_FINISH;
e.printStackTrace();
}
}
class ColorsFrame {
private ColorsFrame(int[] colors, int delay, int width, int height,boolean transparency) {
this.colors=colors;
this.delay=delay;
this.width=width;
this.height=height;
this.transparency=transparency;
}
int[] colors;
int delay;
int width;
int height;
boolean transparency;
这些属性用于画图的.
}
返回的同样是一个列表,但这次不是Bitmap,而是int[] colors;
gif解析就是比较耗资源的过程.而且很恶心的是readNetscapeExt这个方法,
do {
readBlock();
if (block[0]==1) {
// loop count sub-block
int b1=((int) block[1])&0xff;
int b2=((int) block[2])&0xff;
loopCount=(b2<<8)|b1;
}
} while ((blockSize>0)&&!err());
有时会一直运行,因为while里的条件一直没有达到.
换了以后,gif的解析在内存没有溢出的情况下,会快一些.
然后就是view里的decodeFinish();
gifFrames=gifDecoder2.getFrameArrayList();
currImageIdx=0;
frameLength=gifFrames.size();
//if (rect==null) {
mHandler.post(new Runnable() {
@Override
public void run() {
if (null!=imageLoadCallback) {
imageLoadCallback.loadFinish();
}
//Bitmap bitmap=gifFrames.get(0).image;
int width=gifFrames.get(0).width;
int height=gifFrames.get(0).height;
Log.d(TAG, "gif帧间隔为:"+gifFrames.get(0).delay);
setShowDimension(width, height);
}
});
在onDraw里把它画出来:
canvas.drawBitmap(currentImage.colors, 0, currentImage.width, 0, 0, currentImage.width,
currentImage.height, currentImage.transparency, null);
=====================
由于Android里面的内存限制,大一些的文件不容易解析(解析的过程优化得不够,本人能力有限,只想到从生成Bitmap到直接使用int[]来画图).所以如果用c++解析再把结果传回来,性能上会好些.看作者的GifFrame,觉得他是从c++中转化来的代码.
完整的代码暂时不放上来了,可以参考原作者的代码,然后如果需要修改为int[]类型的值,传回去播放,最后的效果,可以从本人的微博程序中看到.
应网友要求把全部完整代码放上.但是上面说的进一步处理画图没有包含在内,我认为,如果你想得到一些什么,总是要付出的吧,像散架的汽车就等着组合 了,,code.google上已经上传了,在这里看看自己修改吧.性能影响不会太多
2012.4.17 gif.zip 完整的工程,在/sdcard/中放置test.gif就可以了,
最近又发现一个c写的lib,gif2png,解码部分可以拿出来,由于我对图像处理不熟悉,只能转换部分,虽然读取了整个gif文件,且解码了,但不知道如何将这些rgb值与一些char*等转为需要的像素值。
之前还有一个gifscile这是另外的解码方式,同样解码后不知道如何转为像素值。而且免费,随意使用。
现在把代码放上来,如果会转换的童鞋,可以帮忙弄下,工程里面已经相对完整了。Hello.zip里的jni目录gifread.c是读取gif的文件,/sdcard/test.gif必须