前言
该篇只是记录一下利用giflib来加载gif图片的使用,主要涉及知识:C/C++基础,CMake编译、gif格式,本文不做详细介绍。
Gif格式可以参数:https://blog.csdn.net/wzy198852/article/details/17266507
也可以参考国外一个比较厉害的开源库:https://github.com/koral--/android-gif-drawable
原理
使用giflib(Android源码工程里有)加载gif文件,并解压每帧对应的图像信息,通过字节拷贝的方式替换Java层的Bitmap实例对应的内存里的字节,以达到在同一块内存空间渲染不同图片信息的效果,由于整个过程主要只在同一个Bitmap里进行,所以避免了多张图片切换情况下内存的重复申请和释放(Bitmap的创建和释放),所以比在Java层实现来得高效很多!
通过giflib解析出来的图像信息里,包含了每张图片的显示间隔时间(每帧间隔时间通常都是固定的),我们可以利用它来实现动画播放效果。
功能
- 加载并播放gif图片(89a版本)
- 支持gif暂停、重放、停止播放操作
效果展示
不足之处
- gif有两个版本分别是:89a和87a,本项目没对87a版本进行兼容,所以请下载89a版本的gif图片来进行测试使用
- giflib支持多种方式打开gif文件,本文只实现了通过gif文件路径和文件流两种打开方式,打开网络gif资源本文只提供了调用架子,C++层没真正实现
- 其它未知的兼容性问题
项目结构
src/main/cpp/gif:giflib的源码,从Android源码工程里拷贝过来的。从文件名可以看出它包含了gif解码、编码以及添加文件等功能。主要用到的是git_lib.h定义的功能
androidLog.h:简化在Native层打印Log的宏定义文件
native-lib.cpp:主要的gif播放实现代码
GifHandler:继承android.os.Handler,主要为了更新图片,以及发送延迟更新下一帧图片通知
GifImageView:继承ImageView,控制和显示gif的控件
GifManager:提供对gif文件进行操作的native层接口
核心代码
native-lib.cpp:
#include <jni.h>
#include <string>
#include "gif/gif_lib.h"
#include "androidLog.h"
#include <android/bitmap.h>
// a: 0000 0001
// r: 0000 0010
// g: 0000 0100
// b: 0000 1000
// 合并后变成:0000 0001 0000 0010 0000 0100 0000 1000
// 把4个char类型的数据合并成一个32位的int数据
#define argb(a, r, g, b) (((a)&0xFF) << 24) | (((b)&0xFF) << 16) | (((g)&0xFF) << 8) | ((r)&0xFF)
// Gif文件类型:文件路径或文件描述符
enum FileType {
PATH, FD, NET
};
typedef struct GifBean {
int current_frame; // 当前帧
int total_frame; // 总共帧数
int *delays; // 延迟时间数组
} GifBean;
// 方法声明
void drawFrame(GifFileType *gifFileType, GifBean *gifBean, AndroidBitmapInfo bitmapInfo,
void *pixelArray);
GifFileType *initGifFile(FileType type, void *param);
// 是否正在绘制,如果当前正在绘制,那么如果这时停止了播放那么需要等当前绘制帧绘制完成后才会停止
bool isDrawing = false;
// 是否已停止
bool isStop = false;
// 记录当前帧,重新播放时直接从该帧开始
int current_frame = 0;
/**
* 通过文件路径加载gif文件
*/
extern "C"
JNIEXPORT jlong JNICALL
Java_com_log_loggif_GifManager_loadPath0(JNIEnv *env, jobject thiz, jstring gif_file) {
jlong pointer = 0;
const char *file = env->GetStringUTFChars(gif_file, 0);
// 根据文件路径打开gif文件
GifFileType *gifFileType = initGifFile(PATH, (void *) file);
if (gifFileType) {
// 把gif文件的地址返回给Java层
pointer = reinterpret_cast<jlong>(gifFileType);
}
env->ReleaseStringUTFChars(gif_file, file);
return pointer;
}
/**
* 通过文件描述符加载gif文件
*/
extern "C"
JNIEXPORT jlong JNICALL
Java_com_log_loggif_GifManager_loadFromFD0(JNIEnv *env, jobject thiz, jint gifFD) {
// 根据文件路径打开gif文件
GifFileType *gifFileType = initGifFile(FD, &gifFD);
if (gifFileType) {
// 把gif文件的地址返回给Java层
return reinterpret_cast<jlong>(gifFileType);
}
return 0;
}
/**
* 加载网络gif图片
*/
extern "C"
JNIEXPORT jlong JNICALL
Java_com_log_loggif_GifManager_loadFromNet0(JNIEnv *env, jobject thiz, jstring url) {
jlong pointer = 0;
const char *gif = env->GetStringUTFChars(url, 0);
// 根据文件路径打开gif文件
GifFileType *gifFileType = initGifFile(NET, (void *) gif);
if (gifFileType) {
// 把gif文件的地址返回给Java层
pointer = reinterpret_cast<jlong>(gifFileType);
}
env->ReleaseStringUTFChars(url, gif);
return pointer;
}
int gif_input_callback(GifFileType *gifFileType, GifByteType *gifByteType, int) {
const char *url = static_cast<const char *>(gifFileType->UserData);
// TODO 在这里访问网络gif资源
return GIF_OK;
}
/**
* 初始化Gif文件结构体
* @param gifFileType
* @return
*/
GifFileType *initGifFile(FileType type, void *param) {
int stateCode;
GifFileType *gifFileType = NULL;
if (type == PATH) {
gifFileType = DGifOpenFileName((const char *) param, &stateCode);
} else if (type == FD) {
gifFileType = DGifOpenFileHandle(*((jint *) param), &stateCode);
} else if (type == NET) {
gifFileType = DGifOpen(param, gif_input_callback, &stateCode);
} else {
LOGE("不合法的文件类型:%d", type);
return NULL;
}
if (gifFileType == NULL) {
LOGE("打开Gif文件失败:%d", stateCode);
return NULL;
}
// 初始化GifFileType结构体
stateCode = DGifSlurp(gifFileType);
if (stateCode == GIF_ERROR) {
LOGE("初始化Gif文件失败:%d", stateCode);
return NULL;
}
GifBean *gifBean = static_cast<GifBean *>(calloc(1, sizeof(GifBean)));
gifBean->current_frame = current_frame;
gifBean->total_frame = gifFileType->ImageCount;
LOGE("总共帧数:%d", gifBean->total_frame);
// 初始化延迟时间
gifBean->delays = static_cast<int *>(calloc(gifFileType->ImageCount, sizeof(int)));
// 延迟时间是在图形控制扩展块里,这里需要遍历所有帧里的扩展块,找到图形扩展块
for (int i = 0; i < gifFileType->ImageCount; i++) {
SavedImage savedImage = gifFileType->SavedImages[i];
// 遍历帧里的扩展块
for (int j = 0; j < savedImage.ExtensionBlockCount; j++) {
ExtensionBlock extensionBlock = savedImage.ExtensionBlocks[j];
// gif的89a版本的每一帧都可能有四个扩展块:图形扩展块、注释扩展块、图形文本扩展块、应用程序扩展块
// 如果是图形扩展块(89a版本才有)
if (extensionBlock.Function == GRAPHICS_EXT_FUNC_CODE) {
// 根据gif图形控制扩展块定义,第5和第6位表示延迟时间
// Bytes[1]: 0000 0000 1100 0000
// |
// Bytes[2]: 0000 0000 0011 0000 << 8 = 0011 0000 0000 0000
// =
// 0011 0000 1100 0000
// extensionBlock.Bytes的第二元素和第三元素分别放的是第5和第6位的延迟时间字节数据,第一个元素是保留的数据
// 延迟时间单位为1/100秒,如果值不为1,表示暂停规定的时间后再继续往下处理数据流
// 1秒=1000毫秒,那么有1000 / 100 = 10毫秒,也就是10毫秒为一个单位
int frame_delay = extensionBlock.Bytes[1] | (extensionBlock.Bytes[2] << 8);
gifBean->delays[i] = frame_delay * 10;
// LOGE("第%d帧的延迟时间为:%d毫秒", i, frame_delay);
break;
}
}
}
isStop = false;
// 保存GifBean信息,类似于View.setTag()
gifFileType->UserData = gifBean;
return gifFileType;
}
extern "C"
JNIEXPORT jint JNICALL
Java_com_log_loggif_GifManager_getWidth0(JNIEnv *env, jobject thiz, jlong git_pointer) {
return ((GifFileType *) git_pointer)->SWidth;
}
extern "C"
JNIEXPORT jint JNICALL
Java_com_log_loggif_GifManager_getHeight0(JNIEnv *env, jobject thiz, jlong git_pointer) {
return ((GifFileType *) git_pointer)->SHeight;
}
/**
* 把不同的gif图片数据通过修改同一个Bitmap对象的像素数组来达到切换图片的效果
* 这里操作的都是同一块内存,不用重复创建和销毁Bitmap对象,所以很高效!
*/
extern "C"
JNIEXPORT jint JNICALL
Java_com_log_loggif_GifManager_renderFrame0(JNIEnv *env, jobject thiz, jlong git_pointer,
jobject bm) {
// 强转成GifFileType*指针类型
GifFileType *gifFileType = reinterpret_cast<GifFileType *>(git_pointer);
// 获取GifBean
GifBean *gifBean = static_cast<GifBean *>(gifFileType->UserData);
// 获取Bitmap信息
AndroidBitmapInfo bitmapInfo;
if (AndroidBitmap_getInfo(env, bm, &bitmapInfo) != ANDROID_BITMAP_RESULT_SUCCESS) {
throw "获取图像失败";
}
// 1.先锁住Bitmap
// 存储Bitmap的像素数组。它每个元素是一个指向一维数组的首地址指针
void *pixelArray;
// 如果锁定Bitmap成功,那么会把Bitmap的像素数组赋值给pixelArray
if (AndroidBitmap_lockPixels(env, bm, &pixelArray) != ANDROID_BITMAP_RESULT_SUCCESS) {
throw "锁定图像失败";
}
// 标记当前正在绘制中
isDrawing = true;
drawFrame(gifFileType, gifBean, bitmapInfo, pixelArray);
current_frame++;
// 如果大于总帧数,那么就重新播放
if (current_frame >= gifBean->total_frame - 1) {
current_frame = 0;
LOGE("重新播放...");
}
gifBean->current_frame = current_frame;
// 解锁
AndroidBitmap_unlockPixels(env, bm);
// 绘制完成后需要修改一下
isDrawing = false;
return gifBean->delays[gifBean->current_frame];
}
void drawFrame(GifFileType *gifFileType, GifBean *gifBean, AndroidBitmapInfo bitmapInfo,
void *pixelArray) {
// 拿到当前帧
SavedImage savedImage = gifFileType->SavedImages[gifBean->current_frame];
// 由于真正的图像像素会有偏移,所以不一定是从第0个位置开始绘制像素,这里需要从偏移量的位置开始
GifImageDesc imageDesc = savedImage.ImageDesc;
ColorMapObject *colorMapObject = imageDesc.ColorMap;
LOGE("imageDesc的信息:左:%d,右:%d,宽:%d, 高:%d", imageDesc.Left, imageDesc.Top, imageDesc.Width,
imageDesc.Height);
if (NULL == colorMapObject) {
return;
}
// 整张图片首地址
int *lines = (int *) pixelArray;
// 把Bitmap里的pixelArray开始绘制位置也需要进行偏移
// bitmapInfo.stride表示是每行的字节长度。这里注意要先转成char*再进行加运算,因为如果按照int*进行加运算,那么它的步长是4而不是1,最后会导致偏移不对。
// 这里进行偏移后就是第一行需要渲染的首地址
lines = reinterpret_cast<int *>((char *) lines + bitmapInfo.stride * imageDesc.Top);
// Gif图片的数据都是经过LZW算法进行压缩的,这里先遍历每个压缩数据的位置,然后利用ColorMapObject来进行解压得到真实的像素数据
for (int y = imageDesc.Top; y < imageDesc.Top + imageDesc.Height; ++y) {
// 先计算当前是第几行
for (int x = imageDesc.Left; x < imageDesc.Left + imageDesc.Width; ++x) {
// pixelArray里的数据已经除掉偏移后的像素,所以pixelArray数组(pixelArray是二维数组)的第一个像素坐标是:(0,0)
// 然后再确定具体像素数据坐标。注意:这里的坐标指的是在经过压缩后的数据的坐标
int pixelPoint = (y - imageDesc.Top) * imageDesc.Width + (x - imageDesc.Left);
GifByteType byteType = savedImage.RasterBits[pixelPoint];
// 对压缩数据进行解压。colorMapObject->Colors里就是一个字典,其实就是个颜色表,通过它可以找到映射的真实像素值,也就是得到LZW算法压缩前的数据等于是解压。
GifColorType colorType = colorMapObject->Colors[byteType];
// GifColorType里包括三个char类型的字段:Red, Green, Blue,需要把它们合并成一个4字节的int像素。这个就是真实的一个像素数据
int realPixel = argb(255, colorType.Red, colorType.Green, colorType.Blue);
// 替换Bitmap数组里的每个像素,从而达到替换图片的效果
lines[x] = realPixel;
}
// 下移一行
lines = (int *) ((char *) lines + bitmapInfo.stride);
}
}
extern "C"
JNIEXPORT void JNICALL
Java_com_log_loggif_GifManager_close0(JNIEnv *env, jobject thiz, jlong git_pointer, jint isPause) {
if (!isPause) {
// 重置当前帧索引
current_frame = 0;
}
if (isStop) {
return;
}
while (isDrawing) {
// 等待当前帧绘制完成
}
int stateCode = 0;
GifFileType *gifFileType = reinterpret_cast<GifFileType *>(git_pointer);
if (NULL == gifFileType) {
return;
}
if (gifFileType->UserData != NULL) {
free(gifFileType->UserData);
gifFileType->UserData = NULL;
}
DGifCloseFile(gifFileType, &stateCode);
gifFileType = NULL;
if (D_GIF_SUCCEEDED != stateCode) {
LOGE("关闭文件失败:%d", stateCode);
}
isStop = true;
}
GifManager.java
package com.log.loggif;
import android.graphics.Bitmap;
import android.util.Log;
import java.io.FileDescriptor;
import java.io.FileInputStream;
import java.io.InputStream;
import java.lang.reflect.Field;
public class GifManager {
private static final GifManager instance;
static {
System.loadLibrary("gifplayer");
instance = new GifManager();
}
private GifManager() {
}
public static long load(InputStream inputStream) {
Log.e("GifManager", inputStream.getClass().getCanonicalName());
if (!(inputStream instanceof FileInputStream)) {
throw new RuntimeException("The inputStream is not the type of FileInputStream!");
}
try {
FileDescriptor fileDescriptor = ((FileInputStream) inputStream).getFD();
// 通过反射获得对应的文件操作符
Field fdField = FileDescriptor.class.getDeclaredField("descriptor");
fdField.setAccessible(true);
int fd = (int) fdField.get(fileDescriptor);
return instance.loadFromFD0(fd);
} catch (Exception e) {
e.printStackTrace();
}
return 0;
}
public static long load(String gifFile) {
return instance.loadPath0(gifFile);
}
public static long loadFromNet(String url) {
return instance.loadFromNet0(url);
}
public static int renderFrame(long gitPointer, Bitmap bm) {
return instance.renderFrame0(gitPointer, bm);
}
public static int getWidth(long gitPointer) {
return instance.getWidth0(gitPointer);
}
public static int getHeight(long gitPointer) {
return instance.getHeight0(gitPointer);
}
public static void pause(long gitPointer) {
instance.close0(gitPointer, 1);
}
public static void stop(long gitPointer) {
instance.close0(gitPointer, 0);
}
/**
* 在C/C++层加载指定的Gif图片
*
* @param gifFile gif文件路径
* @return 如果打开文件失败返回0,如果成功返回文件结构体的指针
*/
private native long loadPath0(String gifFile);
private native long loadFromFD0(int gifFD);
/**
* 加载网络gif图片
* @param url
* @return
*/
private native long loadFromNet0(String url);
/**
* 停止播放并释放gif资源
* @param gitPointer
* @param isPause 0表示是停止,1表示是暂停(不会重置当前帧索引)
*/
private native void close0(long gitPointer, int isPause);
/**
* 渲染图片
*
* @param gitPointer
* @param bm
* @return 下一帧图片开始渲染的等待时间
*/
private native int renderFrame0(long gitPointer, Bitmap bm);
/**
* 获取gif图片宽度
*
* @param gitPointer gif的指针,调用loadPath()方法会返回
* @return gif图片宽度
*/
private native int getWidth0(long gitPointer);
/**
* 获取gif图片高度
*
* @param gitPointer gif的指针,调用loadPath()方法会返回
* @return gif图片高度
*/
public native int getHeight0(long gitPointer);
}
GifImageView.java
package com.log.loggif;
import android.content.Context;
import android.graphics.Bitmap;
import android.os.Message;
import android.util.AttributeSet;
import androidx.annotation.Nullable;
import androidx.appcompat.widget.AppCompatImageView;
import java.io.InputStream;
public class GifImageView extends AppCompatImageView {
private GifHandler handler;
// Bitmap的指针
private long gifPointer;
private Bitmap bitmap;
private boolean isStop;
public GifImageView(Context context) {
super(context);
init();
}
public GifImageView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
init();
}
public GifImageView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init();
}
private void init() {
handler = new GifHandler();
}
private void initBitmap() {
if (gifPointer == 0) {
throw new RuntimeException("加载文件失败");
}
int width = GifManager.getWidth(gifPointer);
int height = GifManager.getHeight(gifPointer);
bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
setImageBitmap(bitmap);
}
public void load(String gifFile) {
gifPointer = GifManager.load(gifFile);
initBitmap();
}
public void load(InputStream inputStream) {
gifPointer = GifManager.load(inputStream);
initBitmap();
}
public void loadFromNet(String url) {
gifPointer = GifManager.loadFromNet(url);
initBitmap();
}
public void play() {
isStop = false;
Message msg = handler.obtainMessage();
msg.what = GifHandler.GIF_MSG_UPDATE;
msg.obj = this;
handler.sendMessage(msg);
}
public void pause() {
handler.removeCallbacksAndMessages(null);
GifManager.pause(gifPointer);
}
public void stop() {
handler.removeCallbacksAndMessages(null);
GifManager.stop(gifPointer);
if (bitmap != null) {
if (!bitmap.isRecycled()) {
bitmap.recycle();
}
bitmap = null;
}
setImageBitmap(null);
isStop = true;
}
public int updateFrame() {
if (isStop) {
return -1;
}
int delay = GifManager.renderFrame(gifPointer, bitmap);
this.setImageBitmap(bitmap);
return delay;
}
}
GifHandler.java
package com.log.loggif;
import android.os.Handler;
import android.os.Message;
import androidx.annotation.NonNull;
public class GifHandler extends Handler {
public static final int GIF_MSG_UPDATE = 0x110;
@Override
public void handleMessage(@NonNull Message msg) {
switch (msg.what) {
case GIF_MSG_UPDATE:
GifImageView gifImageView = (GifImageView) msg.obj;
int delay = gifImageView.updateFrame();
// 如果这时停止播放,那么就不继续发送刷新消息
if (delay < 0) {
break;
}
Message nextMsg = obtainMessage();
nextMsg.what = GIF_MSG_UPDATE;
nextMsg.obj = gifImageView;
sendMessageDelayed(nextMsg, delay);
break;
}
}
}
Util.java
package com.log.loggif;
import android.content.Context;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
public class Util {
/**
* 模拟从服务器下载资源apk包
*
* @param context
* @param is
*/
public static String download(Context context, InputStream is, String fileName) {
File file = context.getDir("resources", Context.MODE_PRIVATE);
// File file = Environment.getExternalStorageDirectory();
String resourceFilePath = file.getAbsolutePath() + "/" + fileName;
FileOutputStream fos = null;
try {
File resourceFile = new File(resourceFilePath);
if (resourceFile.exists()) {
resourceFile.delete();
}
fos = new FileOutputStream(resourceFile.getAbsolutePath());
byte[] buf = new byte[2048];
int len;
while ((len = is.read(buf)) != -1) {
fos.write(buf, 0, len);
}
} catch (Exception e) {
e.printStackTrace();
} finally {
try {
if (fos != null) {
fos.close();
}
if (is != null) {
is.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
return resourceFilePath;
}
}
测试入口:MainActivity.java
package com.log.loggif;
import android.os.Bundle;
import android.util.Log;
import android.view.View;
import androidx.appcompat.app.AppCompatActivity;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
public class MainActivity extends AppCompatActivity {
private GifImageView gifImageView;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
gifImageView = findViewById(R.id.gifView);
}
public void onPlay(View view) {
try {
String fileName = "11.gif";
String gifPath = Util.download(this, getAssets().open(fileName), fileName);
openByStream(gifPath);
} catch (IOException e) {
e.printStackTrace();
}
// String url = "https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1605683846537&di=72d14a8250dfd49d47826a418c65b9bf&imgtype=0&src=http%3A%2F%2Fc-ssl.duitang.com%2Fuploads%2Fitem%2F201211%2F01%2F20121101150323_sveRT.thumb.400_0.gif";
// openByUrl(url);
gifImageView.play();
}
/**
* 通过文件路径打开gif文件
* @param path
*/
public void openByPath(String path) {
gifImageView.load(path);
}
/**
* 通过文件流打开gif文件
* @param path
*/
public void openByStream(String path) {
try {
FileInputStream fis = new FileInputStream(path);
gifImageView.load(fis);
} catch (FileNotFoundException e) {
e.printStackTrace();
}
}
/**
* TODO C++层还没实现
* 访问网络gif资源
* @param url
*/
public void openByUrl(String url) {
gifImageView.loadFromNet(url);
}
public void onPause(View view) {
gifImageView.pause();
}
public void onStop(View view) {
gifImageView.stop();
}
}