需求
打造一个干净纯粹的扫码模块;App基本都用到扫码功能(条码、二维码),网上资源一大把,搬过来就能用,确实能用,但应该还能更好用,经过几次与大厂对比,索性自己搞一个;
于是采用ZXing相机预览,ZBar解码整合路线;但是存在几个问题:
- ZBar中文乱码
- ZBar支持的条码类型有限,部分ZXing支持的格式,ZBar不支持;
- ZXing默认是横屏扫码
技术
- Linux下NDK编译
- Android jni
- Android Studio 配置CMakeLists
资料
源码
编译工具
Android Studio Electric Eel
Linux环境搭建
操作系统 centos7
Linux环境搭建
解压Linux NDK工具到/android-ndk-r17c/
解压叫餐编译工具到/gcc-arm-10.3-2021.07-x86_64-arm-none-linux-gnueabihf/
配置环境变量vi ~/.bash_profile
更新环境变量source ~/.bash_profile
PATH=$PATH:$HOME/bin
export PATH
export ANDROID_NDK=/android-ndk-r17c/
export PATH=$PATH:$ANDROID_NDK
export PATH=/gcc-arm-10.3-2021.07-x86_64-arm-none-linux-gnueabihf/bin:$PATH
进入/android-ndk-r17c/build/tools,运行命令
./make_standalone_toolchain.py --arch arm --api 28 --unified-headers --install-dir /root/my-android-toolchain/
1、编译libiconv
- 下载libiconv源码,上传到Linux,解压;
tar -xvf libiconv-1.17.tar.gz
因为libiconv不是针对Android的项目,所以我们得修改一下构建脚本,进行;在/androidSrc/libiconv-1.17/目录下创建脚本代码 android_build.sh
ANDROID_BUILD=/root/my-android-toolchain/
API_VERSION=28
PATH=$ANDROID_BUILD/bin:$PATH
SYSROOT=$ANDROID_BUILD/sysroot
HOST=arm-linux-androideabi
CFLAGS="-march=armv7-a -mfloat-abi=softfp -mfpu=vfpv3-d16 -D__ANDROID_API__=$API_VERSION"
CXXFLAGS="-std=c++11"
LIBDIRS="-L$ANDROID_BUILD/arm-linux-androideabi/lib"
LDFLAGS="-march=armv7-a -Wl,--fix-cortex-a8 $LIBDIRS"
CONFLAGS="--prefix=${SYSROOT}/usr --host=$HOST"
PKG_CONFIG_PATH="$SYSROOT/usr/lib/pkgconfig"
#configure
PKG_CONFIG_PATH=$PKG_CONFIG_PATH CFLAGS="$CFLAGS" CXXFLAGS="$CXXFLAGS" LDFLAGS="$LDFLAGS" ./configure $CONFLAGS &&
#make & install
运行脚本./android_build.sh,等会儿就好了;
2、编译ZBar
- 在android studio创建一个library模块,方便项目可以直接引入使用;
- 在src/main下创建jni文件夹用于存放C源码,如果创建module有jni目录可跳过;
- 复制ZBar源码到jni下
- 复制ZBar-master\android\jni\config.h到src/main/jni下;
- 复制ZBar-master\java\zbarjni.c到src/main/jni下;
- 复制刚刚Linux构建完的libiconv源码到jni中
- ZBar官方有提供了Androd.mk,也可以直接使用它,需要做一下修改,ndk-build;另一种是cmake,在ZBar模块路径下创建CmakeLists.txt用于构建jni;采用一种就可以,需要在build.gradle做配置android.defaultConfig.externalNativeBuild和android.externalNativeBuild
Androd.mk
CmakeLists.txt
Android.mk
MY_LOCAL_PATH := $(call my-dir)
# libiconv
include $(CLEAR_VARS)
LOCAL_PATH := $(MY_LOCAL_PATH)
LOCAL_MODULE := libiconv
LOCAL_CFLAGS := \
-Wno-multichar \
-D_ANDROID \
-DLIBDIR="c" \
-DBUILDING_LIBICONV \
-DBUILDING_LIBCHARSET \
-DIN_LIBRARY
LOCAL_SRC_FILES := \
libiconv-1.17/lib/iconv.c \
libiconv-1.17/libcharset/lib/localcharset.c \
libiconv-1.17/lib/relocatable.c
LOCAL_C_INCLUDES := \
$(LOCAL_PATH)/libiconv-1.17/include \
$(LOCAL_PATH)/libiconv-1.17/libcharset \
$(LOCAL_PATH)/libiconv-1.17/libcharset/include
include $(BUILD_SHARED_LIBRARY)
LOCAL_LDLIBS := -llog -lcharset
# -----------------------------------------------------
# libzbar
include $(CLEAR_VARS)
LOCAL_PATH := $(MY_LOCAL_PATH)
LOCAL_MODULE := zbar
LOCAL_SRC_FILES := \
zbarjni.c \
zbar/img_scanner.c \
zbar/decoder.c \
zbar/image.c \
zbar/symbol.c \
zbar/convert.c \
zbar/config.c \
zbar/scanner.c \
zbar/error.c \
zbar/refcnt.c \
zbar/video.c \
zbar/video/null.c \
zbar/decoder/code128.c \
zbar/decoder/code39.c \
zbar/decoder/code93.c \
zbar/decoder/codabar.c \
zbar/decoder/databar.c \
zbar/decoder/ean.c \
zbar/decoder/i25.c \
zbar/decoder/qr_finder.c \
zbar/qrcode/bch15_5.c \
zbar/qrcode/binarize.c \
zbar/qrcode/isaac.c \
zbar/qrcode/qrdec.c \
zbar/qrcode/qrdectxt.c \
zbar/qrcode/rs.c \
zbar/qrcode/util.c
LOCAL_C_INCLUDES := \
$(LOCAL_PATH)/include \
$(LOCAL_PATH)/zbar \
$(LOCAL_PATH)/libiconv-1.17/include
LOCAL_SHARED_LIBRARIES := libiconv
include $(BUILD_SHARED_LIBRARY)
CMakeLists.txt
# Set the minimum version of CMake required
cmake_minimum_required(VERSION 3.4.1)
# Set the project name and version
project(zbarjni VERSION 1.0)
# Enable C++11 support
set(CMAKE_CXX_STANDARD 11)
set(CMAKE_CXX_STANDARD_REQUIRED True)
set(JNI_SOURCE ${CMAKE_SOURCE_DIR}/src/main/jni)
# Include directories
include_directories(${JNI_SOURCE}
${JNI_SOURCE}/include
${JNI_SOURCE}/libiconv-1.17/libcharset
${JNI_SOURCE}/libiconv-1.17/include
${JNI_SOURCE}/libiconv-1.17/libcharset/include
${JNI_SOURCE}/zbar)
# libiconv library
add_library(libiconv SHARED
${JNI_SOURCE}/libiconv-1.17/lib/iconv.c
${JNI_SOURCE}/libiconv-1.17/libcharset/lib/localcharset.c
${JNI_SOURCE}/libiconv-1.17/lib/relocatable.c)
target_compile_options(libiconv PRIVATE
-Wno-multichar
-D_ANDROID
-DLIBDIR="c"
-DBUILDING_LIBICONV
-DBUILDING_LIBCHARSET
-DIN_LIBRARY)
# zbar library
add_library(zbarjni SHARED
${JNI_SOURCE}/zbarjni.c
${JNI_SOURCE}/zbar/img_scanner.c
${JNI_SOURCE}/zbar/decoder.c
${JNI_SOURCE}/zbar/image.c
${JNI_SOURCE}/zbar/symbol.c
${JNI_SOURCE}/zbar/convert.c
${JNI_SOURCE}/zbar/config.c
${JNI_SOURCE}/zbar/scanner.c
${JNI_SOURCE}/zbar/error.c
${JNI_SOURCE}/zbar/refcnt.c
${JNI_SOURCE}/zbar/video.c
${JNI_SOURCE}/zbar/video/null.c
${JNI_SOURCE}/zbar/decoder/code128.c
${JNI_SOURCE}/zbar/decoder/code39.c
${JNI_SOURCE}/zbar/decoder/code93.c
${JNI_SOURCE}/zbar/decoder/codabar.c
${JNI_SOURCE}/zbar/decoder/databar.c
${JNI_SOURCE}/zbar/decoder/ean.c
${JNI_SOURCE}/zbar/decoder/i25.c
${JNI_SOURCE}/zbar/decoder/qr_finder.c
${JNI_SOURCE}/zbar/qrcode/bch15_5.c
${JNI_SOURCE}/zbar/qrcode/binarize.c
${JNI_SOURCE}/zbar/qrcode/isaac.c
${JNI_SOURCE}/zbar/qrcode/qrdec.c
${JNI_SOURCE}/zbar/qrcode/qrdectxt.c
${JNI_SOURCE}/zbar/qrcode/rs.c
${JNI_SOURCE}/zbar/qrcode/util.c)
target_link_libraries(zbarjni PRIVATE libiconv log)
# Link against Android log and charset libraries
#target_link_libraries(zbar log charset)
# Enable verbose build output (optional)
set(CMAKE_VERBOSE_MAKEFILE ON)
# Set the default build type (optional)
set(CMAKE_BUILD_TYPE Release)
# If you have subdirectories with more CMakeLists.txt files, you can add them here
# add_subdirectory(subdirectory_name)
# Finish the CMakeLists.txt file
到这里配置结束,开始编译;理论上是没问题的;
导入java类,ZBar-master\java\net\sourceforge\zbar所有类都cp到module来,注意:包名不要改,这边和zbarjni.c对应着;
3、引入ZXing
Zxingdemo比较多,这边只做扫码,所以简化一下导入;
添加依赖
api 'com.google.zxing:core:3.3.3'
api 'com.google.zxing:android-core:3.3.0'
导入java类
导入资源文件
错误提示一个个处理,调用 PreferenceManager.getDefaultSharedPreferences,就全部改为取默认值;
注意:因为是library模块,所以switch(id)得改成用if判断;
完事就构建;
4、修复
ZXing默认横屏改为竖屏;
AndroidManifest.xml-activity -CaptureActivity, android:screenOrientation="portrait"
CameraManager里的 getFramingRectInpreview()方法,增加cameraResolution的判断
if(cameraResolution.x>cameraResolution.y){ //x大于y 改成竖屏数据,y和x互换 计算截图
rect.left = rect.left * cameraResolution.y / screenResolution.x;
rect.right = rect.right * cameraResolution.y / screenResolution.x;
rect.top = rect.top * cameraResolution.x / screenResolution.y;
rect.bottom = rect.bottom * cameraResolution.x / screenResolution.y;
}else{
rect.left = rect.left * cameraResolution.x / screenResolution.x;
rect.right = rect.right * cameraResolution.x / screenResolution.x;
rect.top = rect.top * cameraResolution.y / screenResolution.y;
rect.bottom = rect.bottom * cameraResolution.y / screenResolution.y;
}
解码部分,DecodeHandler 的decode数据进行矩阵替换,宽高互换;这边改用c进行转换;代码如下
JNIEXPORT jbyteArray JNICALL Java_net_sourceforge_zbar_DecodeManager_dataHandler
(JNIEnv *env, jobject thiz, jbyteArray array, jint length, jint width, jint height){
jbyte* jBuffer = (*env) -> GetByteArrayElements(env, array, 0);
unsigned char* pbuffer = (unsigned char*)jBuffer;
jbyteArray resultArray = (*env) -> NewByteArray(env, length);
jbyte *bytes = (*env) -> GetByteArrayElements(env,resultArray, 0);
for(int y=0; y<height; y++){
for(int x=0; x<width; x++){
bytes[x * height + height - y - 1] = pbuffer[x + y * width];
}
}
(*env) -> SetByteArrayRegion(env, resultArray, 0, length, bytes);
(*env) -> ReleaseByteArrayElements(env, array, jBuffer, JNI_ABORT); //释放
(*env) -> ReleaseByteArrayElements(env, resultArray, bytes, JNI_ABORT); //释放
return resultArray;
}
net.sourceforge.zbar.DecodeManager.java
public class DecodeManager extends ImageScanner {
public static native byte[] dataHandler(byte[] by, int length, int width, int height);
}
DecodeHandler 的decode方法,调用
data = decodeManager.dataHandler(data, data.length, width, height)
考虑Zbar部分条码类型不支持,解码部分先尝试Zbar,如果失败在用ZXing;
完整decode方法代码:
private void decode(byte[] data, int width, int height) {
String resultQRcode = null;
Result rawResult = null;
// 先用zbar解码 如果失败或者识别不了用zxing
Image barcode = new Image(width, height, "Y800");
barcode.setData(data);
Rect rect = activity.getCameraManager().getFramingRectInPreview();
if (rect != null) {
/*
zbar 解码库,不需要将数据进行旋转,因此设置裁剪区域是的x为 top, y为left
设置了裁剪区域,解码速度快了近5倍左右
*/
barcode.setCrop(rect.top, rect.left, rect.width(), rect.height()); // 设置截取区域,也就是你的扫描框在图片上的区域.
}
ImageScanner mImageScanner = new ImageScanner();
int result = mImageScanner.scanImage(barcode);
if (result != 0) {
SymbolSet symSet = mImageScanner.getResults();
for (Symbol sym : symSet) {
resultQRcode = sym.getData();
BarcodeFormat str = sym.getSymbolName();//条码格式,转化一下统一结果
rawResult = new Result(resultQRcode, data, null, str);
Log.d(getClass().getName(),resultQRcode);
}
}
if (TextUtils.isEmpty(resultQRcode) || rawResult == null) { //如果扫描模式是Zxing
/*
因为相机传感器捕获的数据是横向的, 所以需要将数据进行90度的旋转, 用java进行转换在红米三手机测试大概需要 600ms左右
因此换了C语言, 只需要 35ms左右 速度快了接近 20倍
*/
data = decodeManager.dataHandler(data, data.length, width, height);
//Log.d(TAG, "数组转换用时: " + (System.currentTimeMillis() - start));
int tmp = width;
width = height;
height = tmp;
PlanarYUVLuminanceSource source = activity.getCameraManager().buildLuminanceSource(data, width, height);
if (source != null) {
BinaryBitmap bitmap = new BinaryBitmap(new HybridBinarizer(source));
try {
rawResult = multiFormatReader.decodeWithState(bitmap);
} catch (ReaderException re) {
// continue
} finally {
multiFormatReader.reset();
}
}
if (rawResult != null) {
resultQRcode = rawResult.getText();
}
}
long end = System.currentTimeMillis();
Handler handler = activity.getHandler();
if (!TextUtils.isEmpty(resultQRcode)) { // 非空表示识别出结果了。
if (handler != null) {
Log.d(getClass().getName(), "解码成功: " + resultQRcode);
Message message = Message.obtain(handler, R.id.decode_succeeded, rawResult);
message.sendToTarget();
}
} else {
if (handler != null) {
Message message = Message.obtain(handler, R.id.decode_failed);
message.sendToTarget();
}
}
}
ZBar中文乱码,修改ZBar源码,src/main/jni/zbar/qrcode/qrdectxt.c在63行处,修改如下
// latin1_cd=iconv_open("UTF-8","ISO8859-1");
// sjis_cd=iconv_open("UTF-8","SJIS");
//中文乱码
latin1_cd=iconv_open("UTF-8","GB18030");
sjis_cd=iconv_open("UTF-8","GB2312");
到这里就结束了,可以依赖到项目中去愉快扫码了;
5、优化
界面优化,加入闪光灯,运行时权限参考:Android集成zxing扫码框架
编译参考:Linux下搭建Android交叉编译环境;