Android二维码扫描模块可简单集成(基于ZXIng实现)

时间:2022-11-17 07:44:19

Android二维码扫描模块可简单集成(基于ZXIng实现)

一、该模块实现代码地址:https://github.com/M075097/MoZXing

相对于官方Demo,有以下特点:

  • 1、修改横屏为竖屏,以及修改为竖屏后一维的条码识别依然需要横向扫描的问题
  • 2、修改扫描区域的视图效果,模仿支付宝的网格扫描效果,也可根据自己需求做简单的定制
  • 3、增加从相册选取图片识别的功能
  • 4、移除解码过程中的定位及可能结果的展示回调
  • 5、优化从相机调用到给出解码结果之间的调用,集成更简单
  • 6、自测有限,代码健壮性需要提升

二、从ZXing源码开始(版本3.3.1)

2.1 ZXing的github地址:https://github.com/zxing/zxing

可以直接clone其仓库中的代码到本地,也可以直接下载https://github.com/zxing/zxing/releases页面的最新的打包源码。

以3.3.1版本为例,工程构建使用AndroidStudio

该源码中实际工程中需要用的是目录.\android及.\android-core及目录core下的代码。

其中android下的即是一个.\android的实例工程,该目录下的代码主要是提供一个扫码界面,并捕捉实时图片数据供解码使用,具体解码处理在.\core下的代码完成

.\android-core下只有一个文件:CameraConfigurationUtils.java,该文件作用是相机的相关属性设置类。我们可以直接copy到.\android下的合适位置

.\core下是ZXing解码的核心处理代码,我们可以直接编译该文件夹下的代码为一个jar,进行依赖,也可以直接在项目build.gradle中添加依赖: compile ‘com.google.zxing:core:3.3.1’

2.2 运行源码

本例用的是AndroidStudio,过程如下:

  • 1.新建一个默认工程,把.\android 以module的形式导入该工程中,导入的时候直接使用默认转换,把原工程转化为一个AS工程
  • 2.copy .\andriod-core下的文件CameraConfigurationUtils.java 到.\android下,这里我们copy到原路径相同的camera下。
  • 在该Module下的build.gradle 中添加依赖:

    dependencies {
    compile fileTree(dir: 'libs', include: ['*.jar'])
    compile 'com.google.zxing:core:3.3.1'
    }
  • 运行该工程即可
2.3 该官方Demo中我们一般需要改的地方:
  • 1.官方Demo是横屏,我们需要更改为竖屏
  • 2.官方的扫码,一般需要修改为方形,且扫码效果需要可自定义
  • 3.官方Demo中再扫描过程中会回调并在扫描区域绘制出识别到的定位相关信息,实际中一般不需要,可以移除掉该功能
  • 4.官方Demo中没有从相册的图片中识别二维码的示例(底层解码是支持的)
  • 5.官方的Demo中从相机调用和图像展示以及解码三个部分界限不是很清楚,我们如果想集成到自己的工程中的时候略显繁琐

三、ZXing源码中主线的类图及时序图整理

3.1 ZXing Demo中的主要工作流程简单来说就是打开摄像头并在摄像头的预览回调中把数据传给解析核心,并把结果回传。解析内核的代码我们暂时不需要更改,但是可以拆开,以便更换解析内核,而我们的主要的工作是封装相机调用,并添加相册图片的识别和解析内核到一块以便我们自己简单的集成。
3.2 ZXing类图及说明

Android二维码扫描模块可简单集成(基于ZXIng实现)

3.3 ZXing时序图

Android二维码扫描模块可简单集成(基于ZXIng实现)

3.4 整个调用过程源码分析如下:
3.4.1 在打开CaptureActivity后,在onResume()中创建CameraManager类,并且在该类中创建Camera的PreviewCallback并创建Camera的参数设置管理类CameraConfigurationManager

CaptureActivity类代码如下:

<类CaptureActivity>
protected void onResume() {
super.onResume();
....
cameraManager = new CameraManager(getApplication());
....
SurfaceView surfaceView = (SurfaceView) findViewById(R.id.preview_view);
SurfaceHolder surfaceHolder = surfaceView.getHolder();
if (hasSurface) {//SurfaceView 已经被初始化并创建出来
// The activity was paused but not stopped, so the surface still exists. Therefore
// surfaceCreated() won't be called, so init the camera here.
initCamera(surfaceHolder);
} else {//SurfaceView还没有被初始化完毕,需要在其回调中打开相机
// Install the callback and wait for surfaceCreated() to init the camera.
surfaceHolder.addCallback(this);
}
}

CameraManager类的构造函数如下

 public CameraManager(Context context) {
this.context = context;
this.configManager = new CameraConfigurationManager(context);//相机的参数设置管理类
previewCallback = new PreviewCallback(configManager);//相机的预览回掉系统级
}
3.4.2 然后再CaptureActivity中会调用initCamera(surfaceHolder),在此处需要初始化一个SurfaceView,并取得其SurfaceHolder,并需要保证该SurfaceView已经被创建,代码及注释如下

initCamera(SurfaceHolder)

initCamera(SurfaceHodler surfaceHolder){
cameraManager.openDriver(surfaceHolder);//此处打开相机,并设置相机的相关属性,然后把相机预览图像通过SurfaceHodler显示在SurfaceView,(cameraObject.setPreviewDisplay(holder))
// Creating the handler starts the preview, which can also throw a RuntimeException.
if (handler == null) {
handler = new CaptureActivityHandler(this, decodeFormats, decodeHints, characterSet, cameraManager);//新建处理事件调度中心,在初始化时会创建一个,子线程,并在该线程中解析条码,解析完毕后,在回传到该CaptureActivityHandler中处理
}
}
3.4.3 我们看下CaptureActivityHandler的构造函数,在该构造函数中,会初始化一个子线程DecodeThread用来解析条形码并开启,同时开启相机的预览。需要注意的是此时的相机已经初始化完毕

构造函数如下:

 CaptureActivityHandler(CaptureActivity activity,
Collection<BarcodeFormat> decodeFormats,
Map<DecodeHintType,?> baseHints,
String characterSet,
CameraManager cameraManager) {
this.activity = activity;
decodeThread = new DecodeThread(activity, decodeFormats, baseHints, characterSet,
new ViewfinderResultPointCallback(activity.getViewfinderView()));
decodeThread.start();
state = State.SUCCESS;

// Start ourselves capturing previews and decoding.
this.cameraManager = cameraManager;
cameraManager.startPreview();
restartPreviewAndDecode();
}
3.4.4 DecodeThread继承自Thread,在该Thread的run函数中会新见一个消息队列,并用于解析条形码

其run函数如下

  @Override
public void run() {
Looper.prepare();
handler = new DecodeHandler(activity, hints);//在该子线程中新建一个消息队列,以接收数据并解析条形码的信息
handlerInitLatch.countDown();
Looper.loop();
}
3.4.5 在DecodeHandler中的处理信息的主要代码如下:

handleMessage()代码如下,该信息的发送正是在CameraManager初始化时初始化的相机的PreviewCallBack中调用的

@Override
public void handleMessage(Message message) {
if (message == null || !running) {
return;
}
switch (message.what) {
case R.id.decode:
decode((byte[]) message.obj, message.arg1, message.arg2);
break;
case R.id.quit:
running = false;
Looper.myLooper().quit();
break;
}
}
3.4.6 PreViewCallback的代码如下:

在相机预览到图片之后会调用previewHandler处理消息,该previewHandler的设置是通过CameraManager的requestPreviewFrame(Handler handler, int message)设置,而该方法的调用是在CaptureActivityHandler中调用的其中的handler传入的正是decodeThread.getHandler()即DecodeHandler

final class PreviewCallback implements Camera.PreviewCallback {

private static final String TAG = PreviewCallback.class.getSimpleName();

private final CameraConfigurationManager configManager;
private Handler previewHandler;
private int previewMessage;

PreviewCallback(CameraConfigurationManager configManager) {
this.configManager = configManager;
}

void setHandler(Handler previewHandler, int previewMessage) {
this.previewHandler = previewHandler;
this.previewMessage = previewMessage;
}

@Override
public void onPreviewFrame(byte[] data, Camera camera) {
Point cameraResolution = configManager.getCameraResolution();
Handler thePreviewHandler = previewHandler;
if (cameraResolution != null && thePreviewHandler != null) {
Message message = thePreviewHandler.obtainMessage(previewMessage, cameraResolution.x,
cameraResolution.y, data);
message.sendToTarget();
previewHandler = null;
} else {
Log.d(TAG, "Got preview callback, but no handler or resolution available");
}
}

}
3.4.7 中间还有重要的一个过程即为设置需要解码的格式都哪些,Demo中有两种设置,在DecodeThread的构造函数中可以看出,最终支持哪些解码格式由此处决定

DecodeThread构造函数如下

 DecodeThread(CaptureActivity activity,
Collection<BarcodeFormat> decodeFormats,
Map<DecodeHintType,?> baseHints,
String characterSet,
ResultPointCallback resultPointCallback) {

this.activity = activity;
handlerInitLatch = new CountDownLatch(1);

hints = new EnumMap<>(DecodeHintType.class);
if (baseHints != null) {
hints.putAll(baseHints);
}

// The prefs can't change while the thread is running, so pick them up once here.
if (decodeFormats == null || decodeFormats.isEmpty()) {//没有设置,则根据设置要解码的格式重新进行添加
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(activity);
decodeFormats = EnumSet.noneOf(BarcodeFormat.class);
if (prefs.getBoolean(PreferencesActivity.KEY_DECODE_1D_PRODUCT, true)) {
decodeFormats.addAll(DecodeFormatManager.PRODUCT_FORMATS);
}
if (prefs.getBoolean(PreferencesActivity.KEY_DECODE_1D_INDUSTRIAL, true)) {
decodeFormats.addAll(DecodeFormatManager.INDUSTRIAL_FORMATS);
}
if (prefs.getBoolean(PreferencesActivity.KEY_DECODE_QR, true)) {
decodeFormats.addAll(DecodeFormatManager.QR_CODE_FORMATS);
}
if (prefs.getBoolean(PreferencesActivity.KEY_DECODE_DATA_MATRIX, true)) {
decodeFormats.addAll(DecodeFormatManager.DATA_MATRIX_FORMATS);
}
if (prefs.getBoolean(PreferencesActivity.KEY_DECODE_AZTEC, false)) {
decodeFormats.addAll(DecodeFormatManager.AZTEC_FORMATS);
}
if (prefs.getBoolean(PreferencesActivity.KEY_DECODE_PDF417, false)) {
decodeFormats.addAll(DecodeFormatManager.PDF417_FORMATS);
}
}
hints.put(DecodeHintType.POSSIBLE_FORMATS, decodeFormats);

if (characterSet != null) {
hints.put(DecodeHintType.CHARACTER_SET, characterSet);
}
hints.put(DecodeHintType.NEED_RESULT_POINT_CALLBACK, resultPointCallback);
Log.i("DecodeThread", "Hints: " + hints);

}

四、根据需求我们需要做如下优化

4.1.我们设置扫码界面为竖屏,在Manifest文件的相关Activity标签中添加以下属性
android:configChanges="keyboard|orientation"
android:screenOrientation="portrait"

同是,我们在onresum()不再根据当前手机的物理方向去修改activity的方向,而是直接设置,该方法为Activity的方法

activity.setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_SENSOR_PORTRAIT);
4.2.解决修改为竖屏后,一维条形码仍需要横置手机才能扫描出结果的问题,该问题原因是相机的预览的图像是竖直方向的,但是实际在相机的PreviewCallback中的数据依然是横向的格式,此时解码器在解码一维的条形码时等于是纵向解析条形码,而对于一维码纵向识别不到,当横置手机时才是以横向解析条形码。因此我们可以直接转置在相机的PreviewCallback中拿到的数据,转置后即相当于数据为纵向格式,具体操作如下:
@Override
public void onPreviewFrame(byte[] data, Camera camera) {
Point cameraResolution = configManager.getCameraResolution();//拿取相机的分辨率
int previewHeight = cameraResolution.y;
int previewWidth = cameraResolution.x;
byte[] rotatedData = new byte[data.length];//新建转置byte数组
for (int y = 0; y < previewHeight; y++) {//转置原数组,纵向横向数据互换
for (int x = 0; x < previewWidth; x++)
rotatedData[x * previewHeight + previewHeight - y - 1] = data[x + y * previewWidth];
}
int tmp = previewWidth;
previewWidth = previewHeight;
previewHeight = tmp;//更改记录数据的宽高
data = rotatedData;
....
//发送数据到解码子线程中进行解码
}

另外,同时需要修改CameraManaget中的有效数据区域的生成方法,

public synchronized Rect getFramingRectInPreview() {
if (framingRectInPreview == null) {
Rect framingRect = getFramingRect();
if (framingRect == null) {
return null;
}
Rect rect = new Rect(framingRect);
Point cameraResolution = configManager.getCameraResolution();
Point screenResolution = configManager.getScreenResolution();
if (cameraResolution == null || screenResolution == null) {
// Called early, before wakeup even finished
return null;
}
/* 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;*/

//FIX 竖屏一维码扫不出,在转置相机PreviewCallback中的数据后,该有效裁剪区域不不修改会导致,裁剪时裁剪范围超出数据范围,此时相机preview图像为纵向,cameraResolution.y相当于图像的宽,cameraResolution.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;
framingRectInPreview = rect;
}
Log.d(TAG, "getFramingRectInPreview: framingRectInPreview" + framingRectInPreview.toString()+"--framingRect="+framingRect.toString());
return framingRectInPreview;
}
4.3 添加解析相册中的图片的方法
4.3.1 在ZXing解码中可以使用 RGBLuminanceSource 构建解码所需的BinaryBitmap,而RGBLuminanceSource需要的构造参数是图像的像素数据,类型为int[],我们可以通过以下方法从有效的相册图片路径path生成所需的数据:
//从有效的绝对路径地址中生成构建BinaryBitmap所需要的图片像素数据 int[]
public static PicDataInfo getPicInfo(String picPath) {
if(TextUtils.isEmpty(picPath)){
return null;
}
PicDataInfo picDataInfo = new PicDataInfo();//自定义的类用来保存生成的数据
try {
File picFile = new File(picPath);
if (!picFile.exists()) {
return null;
}

FileInputStream fis = new FileInputStream(picFile);
ByteArrayOutputStream baos = new ByteArrayOutputStream();
byte[] buffer = new byte[1024 * 10];
int len = 0;
while ((len = fis.read(buffer)) != -1) {
baos.write(buffer, 0, len);
}
baos.flush();
picDataInfo.data = baos.toByteArray();
baos.close();
BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true; // 先获取原大小
Bitmap bitmap = BitmapFactory.decodeByteArray(picDataInfo.data, 0, picDataInfo.data.length);
picDataInfo.height = bitmap.getHeight();
picDataInfo.width = bitmap.getWidth();
int[] pixData = new int[picDataInfo.width * picDataInfo.height];
bitmap.getPixels(pixData, 0, picDataInfo.width, 0, 0, picDataInfo.width, picDataInfo.height);//生成pixData
picDataInfo.pixData = pixData;
} catch (Exception e) {
e.printStackTrace();
} finally {
return picDataInfo;
}
}
4.3.2 通过上一步生成的pixData,对其进行解码,代码如下
//解码相册中的图片数据
public String decodeCode(int[] pixels, Rect validArea) {
Result rawResult = null;
RGBLuminanceSource source = new RGBLuminanceSource(validArea.width(), validArea.height(), pixels);
if (source != null) {
BinaryBitmap bitmap = new BinaryBitmap(new HybridBinarizer(source));
try {
rawResult = multiFormatReader.decodeWithState(bitmap);
Log.d(TAG, "decode result:" + rawResult.toString());
} catch (ReaderException re) {
// continue
} finally {
multiFormatReader.reset();
}
}
if (rawResult != null) {
return rawResult.getText();
}
return null;
}

4.4.自定义扫描窗口的动画及样式,我们不需要在解析过程中回显可能的的定位位置,所以官方Demo中原有的ViewFinderView,大部分我们不需使用,并且在构建解码器所需的解码规则标志集合(Map ”DecodeHintType,Object>)中不需要设置相关回调方法。我们唯一需要的是CameraManager中根据相机参数生成的一个有效扫描区域,我们需要在该区域上绘制相关的效果即可

扫描动画效果的实现可以参考该链接:http://blog.csdn.net/M075097/article/details/78533141

五、根据以上封装的一个方便集成和使用简单的版本,如有需要可以进行参考,使用方式见工程目录下的README,源码地址为:https://github.com/M075097/MoZXing

该工程特点如下:

  • 1.依照常用的主线裁掉不需要的代码
  • 2.修改扫面页面为纵向
  • 3.添加了从相册选取图片识别的功能
  • 4.修改了扫码窗口的样式,并可部分定制扫描窗口
  • 5.给予了简单的封装,集成进现有工程更为简单和清晰
  • 6.底层的ZXing解码核心没有改变,只是上层的相机调用/预览,及相关逻辑处理进行了调整