一、思路:
Java层:用ImageView加载一张bitmap,并定时调用jni方法来刷新bitmap的内容;
Jni层:接收到从java层传递的bitmap,找到指定帧的数据,把像素点copy到bitmap中,完成刷新。
二、上代码:
Java层:
/**
* 加载本地gif文件
* @param view
*/
public void ndkLoadGif(View view) {
Log.i(TAG, "ndkLoadGif");
isPlaying = true;
File gifFile = new File(Environment.getExternalStorageDirectory(), "demo.gif");
// 第一步:通过JNI加载本地gif图,拿到对应的信息,并返回解析后的gif图结构体的指针
gifHandler = new GifHandler(gifFile.getAbsolutePath());
// 第二步:得到gif的width , height, 生成bitmap
int width = gifHandler.getWidth();
int height = gifHandler.getHeight();
gifBitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
gifBitmap.eraseColor(0xff000000);
ivGif.setImageBitmap(gifBitmap);
DisplayMetrics outMetrics = new DisplayMetrics();
getWindowManager().getDefaultDisplay().getMetrics(outMetrics);
int widthPixels = outMetrics.widthPixels;
int heightPixels = outMetrics.heightPixels;
Log.i(TAG, "widthPixels = " + widthPixels + ",heightPixels = " + heightPixels);
// 更新imageView的大小
ViewGroup.LayoutParams params = ivGif.getLayoutParams();
params.width = ViewGroup.LayoutParams.MATCH_PARENT;
params.height = (int) Math.round(widthPixels * height * 1.0 / width);
ivGif.setLayoutParams(params);
// 第三步: 更新gif图帧数据到bitmap中
updateGifFrame();
}
/**
* 更新帧画面
*/
private void updateGifFrame() {
if(!isPlaying){
return;
}
long cost = System.currentTimeMillis();
// 第四步:获取下一帧的刷新时间,利用handler循环刷新
int delayShowTime = gifHandler.updateFrame(gifBitmap);
cost = System.currentTimeMillis() - cost;
int realDelay = (int) (delayShowTime - cost);
// 真正的延时,需要减去更新一帧所消耗的时间
realDelay = realDelay < 0 ? 0 : realDelay;
uiHandler.sendEmptyMessageDelayed(1, realDelay); // 不停地更新
Log.v(TAG, "update frame cost : " + cost);
Log.v(TAG, "setImageBitmap first gifBitmap = " + gifBitmap + ", delayShowTime = " + delayShowTime
+ ", realDelay = " + realDelay);
// 最后一步:设置bitmap,刷新显示
ivGif.setImageBitmap(gifBitmap);
}
Jni层
extern "C"
JNIEXPORT jlong JNICALL
Java_com_hele_zandroid_gif_GifHandler_loadPath(JNIEnv *env, jclass clazz, jstring path_) {
const char *path = env->GetStringUTFChars(path_, 0);
int err;
// 第一步:android系统源码,打开gif文件,并返回相应的信息
GifFileType *gifFileType = DGifOpenFileName(path, &err);
// 初始化 --- 在这一步以后,gifFileType才有相应的信息
DGifSlurp(gifFileType);
// 第二步:自定义一个bean类,来存储当前播放帧,总帧数,播放延迟的数组,并把bean对象与gifFileType绑定
GifBean *gifbean = (GifBean *) malloc(sizeof(GifBean));
// 清空内存地址,相当于初始化
memset(gifbean, 0, sizeof(GifBean));
// 把gifbean绑定到gifFileType中
gifFileType->UserData = gifbean;
// 初始化gifBean中的delays数组大小
gifbean->delays = (int *) (malloc(sizeof(int) * gifFileType->ImageCount));
memset(gifbean->delays, 0, sizeof(int) * gifFileType->ImageCount);
// 初始化gifBean的初始数据
gifbean->current_frame = 0;
gifbean->total_frame = gifFileType->ImageCount;
// 初始化显示时长列表
ExtensionBlock *extensionBlock;
for (int i = 0; i < gifFileType->ImageCount; ++i) {
SavedImage savedImage = gifFileType->SavedImages[i];
for (int j = 0; j < savedImage.ExtensionBlockCount; j++) {
// 拿到图形控制扩展块,里面有延时时间
if (savedImage.ExtensionBlocks[j].Function == GRAPHICS_EXT_FUNC_CODE) {
extensionBlock = &savedImage.ExtensionBlocks[j];
break;
}
}
// 如果不为空
if (extensionBlock) {
// 2个字节表示时间, 1/100秒为单位
int delay_time =
10 * (extensionBlock->Bytes[1] | (extensionBlock->Bytes[2] << 8)); // 转成ms
// 存储延迟时间
gifbean->delays[i] = delay_time;
}
}
env->ReleaseStringUTFChars(path_, path);
// 返回gifFileType的指针,方便updateFrame的时候调用
return reinterpret_cast(gifFileType);
}
extern "C"
JNIEXPORT jint JNICALL
Java_com_hele_zandroid_gif_GifHandler_updateFrame(JNIEnv *env, jclass clazz, jlong ndkGif,
jobject bitmap) {
GifFileType *gifFileType = (GifFileType *) ndkGif;
GifBean *gifBean = (GifBean *) gifFileType->UserData;
AndroidBitmapInfo androidBitmapInfo;
// 获取图片信息
AndroidBitmap_getInfo(env, bitmap, &androidBitmapInfo);
void *pixels;
// 锁定bitmap中的像素,防止其它线程更改
AndroidBitmap_lockPixels(env, bitmap, &pixels);
// // 第三步:从gif图中取出对应的帧数据,并更新到bitmap中
drawFrame(gifFileType, gifBean, androidBitmapInfo, pixels);
// 更新对应的帧信息
gifBean->current_frame++;
// 如果到最后了,循环播放
if (gifBean->current_frame >= gifBean->total_frame - 1) {
gifBean->current_frame = 0;
// 重新播放
// __android_log_print(ANDROID_LOG_INFO, "gif", "play again");
}
// 释放锁
AndroidBitmap_unlockPixels(env, bitmap);
// 把下一帧的延迟时间返回上去
return gifBean->delays[gifBean->current_frame];
}
/**
* 渲染
*/
void drawFrame(GifFileType *gifFileType, GifBean *gifBean, AndroidBitmapInfo info, void *pixcels) {
LOGD("drawFrame");
// 先拿到当前帧
SavedImage savedImage = gifFileType->SavedImages[gifBean->current_frame];
// 获取color map
GifImageDesc imageDesc = savedImage.ImageDesc;
ColorMapObject *colorMapObject = imageDesc.ColorMap;
// 这里做一个容错,如果imageDesc.ColorMap为空;从gifFileType->SColorMap中获取
if (NULL == colorMapObject) {
colorMapObject = gifFileType->SColorMap;
}
// bitmap点
int *px = (int *) pixcels; // 先做一次强转
// 真实存储是 A B G R
int bitmapLineStart; // bitmap每一行的首地址
int gifPointIndex;
// 一个像素一个像素的遍历,把colorMap中的值转成rgba,并设置给bitmap; 所以如果是高帧率的gif图,加载就有可能会不流畅了。
// gif中的图像可能有偏移
for (int y = imageDesc.Top; y < imageDesc.Top + imageDesc.Height; y++) {
// 更新行的首地址
bitmapLineStart = y * info.width;
for (int x = imageDesc.Left; x < imageDesc.Left + imageDesc.Width; x++) {
// 因为gif图有偏移,所以从gif 帧中取出rgb数据时,index也要做相应的偏移
// 拿到一个坐标的位置索引 --> 数据
gifPointIndex = (y - imageDesc.Top) * imageDesc.Width + x - imageDesc.Left;
// 通过index拿到的是一个压缩数据
GifByteType gifByteType = savedImage.RasterBits[gifPointIndex];
// 拿到真正的rgb
GifColorType gifColorType = colorMapObject->Colors[gifByteType];
// 转成一个int值,并赋值给对应的像素点
px[bitmapLineStart + x] =
abgr(255, gifColorType.Blue, gifColorType.Green, gifColorType.Red);
}
}
// 至此,bitmap内容刷新成功
}
三、android gif 源码
https://android.googlesource.com/platform/external/giflib/+/refs/heads/master
四、gif图片格式可参考
https://blog.csdn.net/poisx/article/details/79122506