前言
上一篇我们讲完了gif动图格式,这篇文章我们将以代码的形式实现gif图片在手机屏幕上加载。
新建一个NDK项目,配置相关库、CMakeLists。
添加如下几个库到cpp文件夹中。
我们在代码中新建一个GifNdkDecoder类,并且添加如下方法:
public class GifNdkDecoder {
private long gifPointer;
static {
System.loadLibrary("native-lib");
}
public static native int getWidth(long gifPointer);
public static native int getHeight(long gifPointer);
public static native long loadGif(String path);
public static native int updateFrame(Bitmap bitmap, long gifPointer);
public GifNdkDecoder(long gifPoint) {
this.gifPointer = gifPoint;
}
public static GifNdkDecoder load(String path) {
long gifHander = loadGif(path);
GifNdkDecoder gifNdkDecoder= new GifNdkDecoder(gifHander);
return gifNdkDecoder;
}
public long getGifPointer() {
return gifPointer;
}
}
在这个类中,我们新增了四个native方法,分别对应获取宽高,加载gif图进内存,更新gif帧的方法。
然后我们看在native-lib.cpp的实现:
#include <jni.h>
#include <string>
#include "gif_lib.h"
//#include <android/log.h>
#define LOG_TAG "gif"
//#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR,LOG_TAG,__VA_ARGS__)
#define argb(a, r, g, b) ( ((a) & 0xff) << 24 ) | ( ((b) & 0xff) << 16 ) | ( ((g) & 0xff) << 8 ) | ((r) & 0xff)
typedef struct GifBean {
int current_frame;
int total_frames;
int *delays; // 延迟数组
};
void drawFrame(GifFileType *pType, GifBean *pBean, AndroidBitmapInfo info, void *pVoid);
extern "C"
JNIEXPORT jlong JNICALL
Java_com_gif_GifNdkDecoder_loadGif(JNIEnv *env, jclass clazz, jstring path_) {
const char *path = env->GetStringUTFChars(path_, 0);
int error;
// 保存GIF信息结构体
GifFileType *gifFileType = DGifOpenFileName(path, &error);
// gif初始化
DGifSlurp(gifFileType);
// 分配内存
GifBean *gifBean = static_cast<GifBean *>(malloc(sizeof(GifBean)));
memset(gifBean, 0, sizeof(GifBean));
gifBean->delays = static_cast<int *>(malloc(sizeof(int) * gifFileType->ImageCount));
memset(gifBean->delays, 0, sizeof(int) * gifFileType->ImageCount);
ExtensionBlock *ext;
// 复制给GifBean
for (int i = 0; i < gifFileType->ImageCount; ++i) {
SavedImage frame = gifFileType->SavedImages[i];
for (int j = 0; i < frame.ExtensionBlockCount; ++j) {
if (frame.ExtensionBlocks[j].Function == GRAPHICS_EXT_FUNC_CODE) {
ext = &frame.ExtensionBlocks[j];
break;
}
}
if (ext) {
// 拿到延迟时间
// 小端模式存储,舍弃头部两个固定数据
int frame_delay = (ext->Bytes[2] << 8 | ext->Bytes[1]) * 10;
gifBean->delays[i] = frame_delay;
}
}
gifBean->total_frames = gifFileType->ImageCount;
// 方便后面取GifBean里面的信息
gifFileType->UserData = gifBean;
env->ReleaseStringUTFChars(path_, path);
return (jlong) gifFileType;
}
void drawFrame(GifFileType *gifFileType, GifBean *gifBean, AndroidBitmapInfo info, void *pixels) {
SavedImage savedImage = gifFileType->SavedImages[gifBean->current_frame];
// 当前帧的图像信息
GifImageDesc imageInfo = savedImage.ImageDesc;
int *px = (int *) pixels;// 图像首地址
ColorMapObject *colorMapObject = imageInfo.ColorMap;
if (colorMapObject == NULL) {
colorMapObject = gifFileType->SColorMap;
}
// 方向心偏移量
px = reinterpret_cast<int *>((char *) px + info.stride * imageInfo.Top);
int pointPixel;// 记录像素位置
GifByteType gifByteType;
GifColorType gifColorType;
int *line;// 每一行的首地址
for (int y = imageInfo.Top; y < imageInfo.Top + imageInfo.Height; ++y) {
line = px;
for (int x = imageInfo.Width; x < imageInfo.Width + imageInfo.Width; ++x) {
pointPixel = (y - imageInfo.Top) * imageInfo.Width + (x - imageInfo.Left);
// 拿到像素数据
gifByteType = savedImage.RasterBits[pointPixel];// 实际上就是LZW算法中的索引
// 给像素赋予颜色
if (colorMapObject != NULL) {
gifColorType = colorMapObject->Colors[gifByteType];
line[x] = argb(255, gifColorType.Red, gifColorType.Green, gifColorType.Blue);
}
}
px = reinterpret_cast<int *>((char *) px + info.stride);
}
}
extern "C"
JNIEXPORT jint JNICALL
Java_com_gif_GifNdkDecoder_getWidth(JNIEnv *env, jclass clazz, jlong gif_pointer) {
GifFileType *gifFileType = (GifFileType *) (gif_pointer);
return gifFileType->SWidth;
}
extern "C"
JNIEXPORT jint JNICALL
Java_com_gif_GifNdkDecoder_getHeight(JNIEnv *env, jclass clazz, jlong gif_pointer) {
GifFileType *gifFileType = (GifFileType *) (gif_pointer);
return gifFileType->SHeight;
}
extern "C"
JNIEXPORT jint JNICALL
Java_com_gif_GifNdkDecoder_updateFrame(JNIEnv *env, jclass clazz, jobject bitmap,
jlong gif_pointer) {
GifFileType *gifFileType = (GifFileType *) gif_pointer;
GifBean *gifBean = (GifBean *) gifFileType->UserData;
AndroidBitmapInfo info;
AndroidBitmap_getInfo(env, bitmap, &info);
void *pixels;// 像素数组
// 锁定bitmap
AndroidBitmap_lockPixels(env, bitmap, &pixels);
// 绘制一帧图像
drawFrame(gifFileType, gifBean, info, pixels);
gifBean->current_frame += 1;
if (gifBean->current_frame >= gifBean->total_frames) {
gifBean->current_frame = 0;
}
AndroidBitmap_unlockPixels(env, bitmap);
return gifBean->delays[gifBean->current_frame];
}
我们先分析loadGif(),这个方法的作用是将gif图加载进内存,然后根据gif图的格式取到延迟时间,并计算有多少帧。然后保存在GifBean这个结构体中。
获取宽高的方法getWidth(),getHeight()就是根据当前GifFileType结构体的指针可以直接取到,对应的还能取到的数据如下:
typedef struct GifFileType {
GifWord SWidth, SHeight; /* Size of virtual canvas */
GifWord SColorResolution; /* How many colors can we generate? */
GifWord SBackGroundColor; /* Background color for virtual canvas */
GifByteType AspectByte; /* Used to compute pixel aspect ratio */
ColorMapObject *SColorMap; /* Global colormap, NULL if nonexistent. */
int ImageCount; /* Number of current image (both APIs) */
GifImageDesc Image; /* Current image (low-level API) */
SavedImage *SavedImages; /* Image sequence (high-level API) */
int ExtensionBlockCount; /* Count extensions past last image */
ExtensionBlock *ExtensionBlocks; /* Extensions past last image */
int Error; /* Last error condition reported */
void *UserData; /* hook to attach user data (TVT) */
void *Private; /* Don't mess with this! */
} GifFileType;
我们看updateFrame()方法。
这里我们通过AndroidBitmapInfo这个结构体获取需要绘制的信息,然后在drawFrame()方法中绘制一帧的图像。
然后我们看MainActivity中的调用。
public class MainActivity extends AppCompatActivity {
// Used to load the 'native-lib' library on application startup.
private static final String TAG = "MainActivity";
private ImageView image;
private Bitmap bitmap;
private GifNdkDecoder gifNdkDecoder;
private Handler handler = new Handler(new Handler.Callback() {
@Override
public boolean handleMessage(@NonNull Message msg) {
long mNextFrameRenderTime =
gifNdkDecoder.updateFrame(bitmap, gifNdkDecoder.getGifPointer());
handler.sendEmptyMessageDelayed(1, mNextFrameRenderTime);
image.setImageBitmap(bitmap);
return true;
}
});
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
image = findViewById(R.id.image);
}
public void ndkLoadGif(View view) {
new MyAsyncTask().execute();
}
class MyAsyncTask extends AsyncTask<Void, Void, Void> {
private ProgressDialog progressDialog;
@Override
protected void onPreExecute() {
progressDialog = new ProgressDialog(MainActivity.this);
progressDialog.setProgressStyle(ProgressDialog.STYLE_SPINNER);
progressDialog.setTitle("正在加载Gif图片...");
progressDialog.setCancelable(false);
progressDialog.show();
}
@Override
protected Void doInBackground(Void... voids) {
File file = new File(Environment.getExternalStorageDirectory(), "demo.gif");
long start = System.currentTimeMillis();
gifNdkDecoder = GifNdkDecoder.load(file.getAbsolutePath());
Log.e(TAG, "load gif 耗时" + (System.currentTimeMillis() - start));
int width = gifNdkDecoder.getWidth(gifNdkDecoder.getGifPointer());
int height = gifNdkDecoder.getHeight(gifNdkDecoder.getGifPointer());
bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
return null;
}
@Override
protected void onPostExecute(Void aVoid) {
progressDialog.dismiss();
long mNextFrameRenderTime =
gifNdkDecoder.updateFrame(bitmap, gifNdkDecoder.getGifPointer());
handler.sendEmptyMessageDelayed(1, mNextFrameRenderTime);
}
}
public void glideLoadGif(View view) {
Glide.with(this).load(new File(Environment.getExternalStorageDirectory(), "demo.gif")).into(image);
}
}
调用很简单,原理就是通过gif图片中的延迟时间不断的更新ImageView。
这里,我们还将NDK加载gif和Glide加载gif图的性能做了一个对比,发现NDK的加载时间要远远小于Glide加载,具体数据我就不展示了。感兴趣的小伙伴可以通过大的gif图做对比,结果很明显。