扫一扫的功能比较常见,其中最常用的是zxing和zbar。zxing项目是谷歌推出的用来识别多种格式条形码的开源项目。ZBar基于C语言编写,解码效率高于Zxing项目。
Zxing的github地址
Zbar的官网
集成Zxing
下载
下载jar包: http://repo1.maven.org/maven2/com/google/zxing/core/3.3.3/
下载Zxing库
接入项目
- 在你项目中,File -> New -> Import Module 把刚下载的android包添加进入
- 然后在ZXing的build.gradle下第一行改成如下,还有把下面的 applicationId那行删掉。
修改前
apply plugin: 'com.android.application'
修改后
apply plugin: 'com.android.library'
- 修改清单文件。运行时就会报Execution failed for task ‘app:processDebugManifest’,只要自己项目的AndroidManifest.xml文件 application标签加上 tools:replace=”icon,theme”。同时module的清单文件里会有CaptureActivity的默认启动项,去除掉
- 把Core Jar包导入ZXing,在ZXing创建一个libs文件夹,把Core Jar放进去,然后右键 As Add Library
- 修改错误。build后会报错:Resource IDs cannot be used in a switch statement in Android library modules。在android项目的library module里不能使用资源ID作为switch语句的case值。为什么呢?因为switch里的case值必须是常数,而在library module的R文件里ID的值不是final类型的,但是主module的R文件里的ID值是final类型的,所以主module里可以用资源ID作为case值而library module却不能。那问题怎么解决呢?把switch-case转成if-else呗。接下来发现会少一个CameraConfigurationUtils类,这个就是刚才在android-core下的那一个类,把它拖到camera包下就好了。
运行
直接Intent启动CaptureActivity(可能需要先去修改相机权限),得到大概如下的页面
google的原项目集成了很多的功能。所以还要对它进行精简
其他开源库
https://github.com/journeyapps/zxing-android-embedded
代码分析
简化后 最主要的几个文件
最主要的步骤如图
首先CaptureActivity 是主要的扫码界面,在onResume里面 1.初始化了CameraManager 初始化ViewfinderView 初始化SurfaceView
@Override
protected void onResume() {
super.onResume();
cameraManager = new CameraManager(getApplication()); //1
viewfinderView = (ViewfinderView) findViewById(R.id.viewfinder_content);
viewfinderView.setCameraManager(cameraManager);
SurfaceView surfaceView = (SurfaceView) findViewById(R.id.scanner_view);
SurfaceHolder surfaceHolder = surfaceView.getHolder();
if (hasSurface) {
// 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 {
// Install the callback and wait for surfaceCreated() to init the camera.
surfaceHolder.addCallback(this);
}
}
2.当SurfaceView creat成功后会去initCamera
@Override
public void surfaceCreated(SurfaceHolder holder) {
if (holder == null) {
Log.e(TAG, "*** WARNING *** surfaceCreated() gave us a null surface!");
}
if (!hasSurface) {
hasSurface = true;
initCamera(holder);
}
}
3.在initCamera时,他又会创建了一个CaptureActivityHandler
private void initCamera(SurfaceHolder surfaceHolder) {
if (surfaceHolder == null) {
throw new IllegalStateException("No SurfaceHolder provided");
}
if (cameraManager.isOpen()) {
Log.w(TAG, "initCamera() while already open -- late SurfaceView callback?");
return;
}
try {
cameraManager.openDriver(surfaceHolder);//先进行openDriver打开摄像头
// Creating the handler starts the preview, which can also throw a RuntimeException.
if (handler == null) {
handler = new CaptureActivityHandler(this, decodeFormats, decodeHints, characterSet, cameraManager);
}
} catch (IOException ioe) {
Log.w(TAG, ioe);
} catch (RuntimeException e) {
// Barcode Scanner has seen crashes in the wild of this variety:
// java.?lang.?RuntimeException: Fail to connect to camera service
Log.w(TAG, "Unexpected error initializing camera", e);
}
}
4.看一下CaptureActivityHandler的构造方法,这个类继承Handler,用来处理各种扫描解析的消息,在构造函数里就开始了扫描的过程
public CaptureActivityHandler(CaptureActivity activity,
Collection decodeFormats,
Map 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();
}
先开启一个解码线程DecodeThread,然后cameraManager.startPreview()实际是里面是调用了系统Camera类的startPreview()方法
public synchronized void startPreview() {
OpenCamera theCamera = camera;
if (theCamera != null && !previewing) {
theCamera.getCamera().startPreview();
...
}
}
restartPreviewAndDecode方法就是不断发送解析消息给decodeThread进行解析
private void restartPreviewAndDecode() {
if (state == State.SUCCESS) {
state = State.PREVIEW;
cameraManager.requestPreviewFrame(decodeThread.getHandler(), R.id.decode);
activity.drawViewfinder();
}
}
/**
* A single preview frame will be returned to the handler supplied. The data will arrive as byte[]
* in the message.obj field, with width and height encoded as message.arg1 and message.arg2,
* respectively.
*
* @param handler The handler to send the message to.
* @param message The what field of the message to be sent.
*/
public synchronized void requestPreviewFrame(Handler handler, int message) {
OpenCamera theCamera = camera;
if (theCamera != null && previewing) {
previewCallback.setHandler(handler, message);
theCamera.getCamera().setOneShotPreviewCallback(previewCallback);
}
}
demo的注释写得蛮清楚,将我们刚刚传入的handler设置给摄像头数据回调previewCallback,数据会以byte数组的方式返回
PreviewCallback,并实现了Camera.PreviewCallback的onPreviewFrame接口
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");
}
}
}
这个方法里面的previewHandler和previewMessage,就是我们在requestPreviewFrame中setHandler方法传入的两个参数,可以看到在onPreviewFrame中会将摄像头获取的数据,还有摄像头视觉的宽高封装到Message中发送给刚刚的DecodeThread去解析.
DecodeThread继承自Thread,在该Thread的run函数中会新见一个消息队列,并用于解析条形码
其run函数如下
@Override
public void run() {
Looper.prepare();
handler = new DecodeHandler(activity, hints);//在该子线程中新建一个消息队列,以接收数据并解析条形码的信息
handlerInitLatch.countDown();
Looper.loop();
}
所以我们的解析过程就是在这个DecodeHandler里
@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;
}
}
还有重要的一个过程即为设置需要解码的格式都哪些,Demo中有两种设置,在DecodeThread的构造函数中可以看出(第二个参数Collection< BarcodeFormat > decodeFormats),最终支持哪些解码格式由此处决定。在枚举类BarcodeFormat中已经定义17种码类型,需要支持二维码
decodeFormats.addAll(DecodeFormatManager.QR_CODE_FORMATS);
其他类
- BeepManager 扫描成功后的手机震动和提示音管理器
- InactivityTimer 一段时间不操作(5分钟 ),关闭该应用(工具类)
- ViewfinderView SurferView相机预览界面(取景器矩形和部分透明,激光扫描仪点动画和结果)
- ViewfinderResultPointCallback SurferView相机预览回调
- CameraConfigurationManager 设置相机的参数信息类
主要方法:
- initFromCameraParameters(OpenCamera camera) 计算了屏幕分辨率和当前最适合的相机像素
- setDesiredCameraParameters(OpenCamera camera, boolean safeMode) 读取配置设置相机的对焦模式、闪光灯模式等等
- getPreviewSizeOnScreen()
- getCameraResolution() //相机分辨率
- getScreenResolution() //屏幕分辨率
- getCWNeededRotation()
- getTorchState(Camera camera)
- setTorch(Camera camera, boolean newSetting)
- CameraManager 摄像头管理类(打开,关闭)
- 闪光灯开关,CameraManager.setTorch(true),方法父类是CameraConfigurationManager ,闪光灯开启同时启动自动对焦
- 打开摄像头驱动 CameraManager.openDriver(SurfaceHolder)
- 设置扫码框显示位置 getFramingRectInPreview()
- 设置扫码框矩形的大小(根据屏幕分辨率) getFramingRect(),
- 设置扫码框矩形的大小(自定义 setManualFramingRect(int width, int height))
- CameraFacing 打开前置或后置摄像头 枚举(BACK,FRONT)
- OpenCamera bean类(获取摄像头部分参数index,camera,facing,orientation )
- OpenCameraInterface 打开摄像头的接口类
参考ZXing 源码分析(简阅)
修改扫码框大小
默认的扫描界面太丑了,是长方形的,而且中间一根红线也不动,就是附近有几个点在闪烁。改聚焦框的大小,代码在CameraManager中,修改getFramingRect里的width height
自定义扫码样式
也就是修改ViewfinderView代码
参考示例:
Android-自定义View实现二维码网格扫描+纵向雷达的扫描效果
更新
今日头条的二维码扫描优化,等它开源