本篇文章已授权微信公众号 guolin_blog (郭霖)独家发布
引言
本篇博文是基于 Android 二维码的扫码功能实现(一) 文章写的,建议阅读这篇文章之前,先看看上篇文章。还有建议阅读本文的同学,结合zxing的源码理解。
上篇博客说明zxing的使用方式,并大致说了IntentIntegrator这个辅助类的作用,及内部的部分源码讲解。通过上篇博文的讲解,虽然我们成功使用了zxing 的扫码功能,但是我们发现它的界面是这样的:
这显然不是我们想要的效果。所以我们必须要对zxing库进行修改,变成我们项目所要的扫码库。
那现在我们打算实现一个样式类似于微信扫一扫样子的二维码。大多数项目的界面应该跟这个差不多。该怎么下手呢?我们看一下微信扫一扫的效果:
Zxing扫码流程分析
我们首先分析一波zxing扫码的整个流程。我们知道想实现上面的界面效果,主要的布局的变化,扫码的核心算法与思路应该是跟Zxing原来一样的。而且zxing的库是比较庞大的,我们只是实现扫码功能的话,zxing里面的很多东西,我们是用不到的,所以需要对其简化,去掉不用的东西。 首先我们看CaptureActivity这个类,上篇文章也有提到过这个类,这个Activity就是官方的扫码界面。我们看他的setContentView(R.layout.capture);这行语句,进入capture布局,可以看到,一下眼熟的控件。CaptureActivity里面有一个很重要的方法。如下:
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);
// Creating the handler starts the preview, which can also throw a RuntimeException.
if (handler == null) {
handler = new CaptureActivityHandler(this, decodeFormats, decodeHints, characterSet, cameraManager);
}
decodeOrStoreSavedBitmap(null, null);
} catch (IOException ioe) {
Log.w(TAG, ioe);
displayFrameworkBugMessageAndExit();
} 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);
displayFrameworkBugMessageAndExit();
}
}
这个initCamera方法涉及到相机的初始化配置,以及扫码配置与启动。CameraManager是相机管理类,里面有着很多很重要的方法,比如开始预览的方法,停止预览以及获取每一帧画面的数据信息等方法。我们先看cameraManager.openDriver(surfaceHolder);这行语句是,点击进去:
/**
* Opens the camera driver and initializes the hardware parameters.
*
* @param holder The surface object which the camera will draw preview frames into.
* @throws IOException Indicates the camera driver failed to open.
*/
public synchronized void openDriver(SurfaceHolder holder) throws IOException {
OpenCamera theCamera = camera;
if (theCamera == null) {
theCamera = OpenCameraInterface.open(requestedCameraId);
if (theCamera == null) {
throw new IOException("Camera.open() failed to return object from driver");
}
camera = theCamera;
}
if (!initialized) {
initialized = true;
configManager.initFromCameraParameters(theCamera);
if (requestedFramingRectWidth > 0 && requestedFramingRectHeight > 0) {
setManualFramingRect(requestedFramingRectWidth, requestedFramingRectHeight);
requestedFramingRectWidth = 0;
requestedFramingRectHeight = 0;
}
}
Camera cameraObject = theCamera.getCamera();
Camera.Parameters parameters = cameraObject.getParameters();
String parametersFlattened = parameters == null ? null : parameters.flatten(); // Save these, temporarily
try {
configManager.setDesiredCameraParameters(theCamera, false);
} catch (RuntimeException re) {
点看后我们看到描述的很清楚,这个方法的作用是打开相机设备,并且配置一些相机参数的。OpenCamera是Camera的包装类。CameraConfigurationManager是设置相机硬件参数的一个类。configManager.initFromCameraParameters(theCamera);这个方法主要是的内容是寻找最好的预览尺寸。寻找最佳预览尺寸的逻辑我就不说了,这块,可以看下这位兄弟写的
iluhcm.com/2016/01/08/… 里面说明了寻找最佳预览尺寸的逻辑,及优化。configManager.setDesiredCameraParameters(theCamera, false);这个方法主要就是设置我们想要的相机参数了。这里会把上面方法中找到的最佳预览大小bestPreviewSize设置给parameters.setPreviewSize(bestPreviewSize.x, bestPreviewSize.y);我们也可以在这个方法里面调用camera.setDisplayOrientation(90);来实现竖屏的效果。
以上是initCamera()方法里面的cameraManager.openDriver这一块分析,接着我们来看 handler = new CaptureActivityHandler(this, decodeFormats, decodeHints, characterSet, cameraManager);语句。进入进去代码如下:
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();
}
这个方法中我们看到decodeThread线程,我们进去看一下发现里面的代码主要是设置了Map
@Override
public void run() {
Looper.prepare();
handler = new DecodeHandler(activity, hints);
handlerInitLatch.countDown();
Looper.loop();
}
run方法里面主要是创建了一个decodeHandler对象,并把hints这个存储支持扫码类型的变量给传进去了。我们接着看decodeHandler是什么鬼?
DecodeHandler(CaptureActivity activity, Map<DecodeHintType,Object> hints) {
multiFormatReader = new MultiFormatReader();
multiFormatReader.setHints(hints);
this.activity = activity;
}
@Override
public void handleMessage(Message message) {
if (message == null || !running) {
return;
}
if (message.what == R.id.decode) {
decode((byte[]) message.obj, message.arg1, message.arg2);
} else if (message.what == R.id.quit) {
running = false;
Looper.myLooper().quit();
}
}
代码很好理解,首先创建了一个MultiFormatReader,并把支持扫码格式传给他,MultiFormatReader是专门解密的一个核心类。很重要。然后我们看到当该Handler收到R.id.decode改消息的时候,会调用decode((byte[]) message.obj, message.arg1, message.arg2);这个方法,我们看下:
private void decode(byte[] data, int width, int height) {
long start = System.currentTimeMillis();
Result rawResult = null;
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();
}
}
Handler handler = activity.getHandler();
if (rawResult != null) {
// Don't log the barcode contents for security.
long end = System.currentTimeMillis();
Log.d(TAG, "Found barcode in " + (end - start) + " ms");
if (handler != null) {
Message message = Message.obtain(handler, R.id.decode_succeeded, rawResult);
Bundle bundle = new Bundle();
bundleThumbnail(source, bundle);
message.setData(bundle);
message.sendToTarget();
}
} else {
if (handler != null) {
Message message = Message.obtain(handler, R.id.decode_failed);
message.sendToTarget();
}
}
}
O(∩_∩)O哈!找了半天终于找到了,这方法重要了,这就是我们扫码逻辑中最重要的解密的逻辑了。代码虽然多但是并不难。首先它构建了一个PlanarYUVLuminanceSource对象,接着根据source创建了二进制的BinaryBitmap。然后rawResult =
multiFormatReader.decodeWithState(bitmap);通过该语句,实现了解密,把解码的结果封装赋值给了Result类。
最后把结果传给了CaptureActivityHandler,在其handlemessage方法中实现对结果的处理。在这里要注意一个问题,就是需要把传进来的data数据中的数据旋转一下,这里的数据是横屏的画面数据。需要转化为竖屏画面数据。该方法传进来的width,height这两个参数的值也需要调换一下。具体的转化代码,可以看YZxing-lib库DecodeHandler类里的实现。
我们现在想一个问题,就是decode这个方法是在什么时候实现的呢?也就是说decodeHandler是在什么时候发送了R.id.decode这个消息?我们看这个方法:
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();
}
这个方法里面的
cameraManager.startPreview();
restartPreviewAndDecode();
这两行语句我们还没看呢。首先看第一行语句,很好理解,这是开始预览画面的执行语句。第二句是 restartPreviewAndDecode();,我们进去看一下:
if (state == State.SUCCESS) {
state = State.PREVIEW;
cameraManager.requestPreviewFrame(decodeThread.getHandler(), R.id.decode);
activity.drawViewfinder();
}
这里我们看到了R.id.decode这个消息的what值。我们看cameraManager的requestPreviewFrame方法:
public synchronized void requestPreviewFrame(Handler handler, int message) {
OpenCamera theCamera = camera;
if (theCamera != null && previewing) {
previewCallback.setHandler(handler, message);
theCamera.getCamera().setOneShotPreviewCallback(previewCallback);
}
}
这里是获取预览界面的一帧。我们看previewCallback里面的代码:
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");
}
}
挖了这么久终于找到了,onPreviewFrame方法里,在这decodeHandler发送了解码的消息,并把一帧的图像数据发送了过去。如果decodeHandler里面的decode 方法扫码失败的话,就发送一个R.id.decode_failed消息给CaptureActivityHandler,CaptureActivityHandler里会调用:
} else if (message.what == R.id.decode_failed) {// We're decoding as fast as possible, so when one decode fails, start another.
state = State.PREVIEW;
cameraManager.requestPreviewFrame(decodeThread.getHandler(), R.id.decode);
该方法,继续请求下一帧的画面数据,去解析。
分析到此,zxing的扫码流程,大致的脉络就是这个样子。这里总结一下吧,就是点击扫码,跳转到CaptureActivity,CaptureActivity里面调用了initCamera方法,该方法中一方面通过cameraManager.openDriver(surfaceHolder);对相机进行初始化,及硬件配置;一方面通过对CaptureActivityHandler的创建,实现解码类MultiFormatReader的配置,画面的预览实现,每一帧画面的数据请求,传递,解码逻辑实现。最后根据这一帧画面数据扫码结果 是成功还是失败发送,来决定是继续请求下一帧的画面信息还是处理扫码成功的结果。
在观察CaptureActivity的时候,我们发现了一个自定义控件,叫做ViewfinderVIew.通过阅读其代码,发现这就是绘制扫码框样式的地方。那我们在修改zxing库的时候就可以重写这个类,来实现对扫码框样式的修改。
YZxing-lib
YZxing-lib这个库,是我基于zxing库修改的扫码库,去除了原来ZXing库中多余的部分,并对扫码效率进行了优化。我们先来看一下YZxing库的实现效果:
(ps:演示效果图,弹窗逻辑已删除)
(扫码成功后,结果的回调)
微信的扫一扫,它聚焦框内有一条不断从上到下移动的绿线,我这边没做成他那样(比较懒),我这边实现的效果是跟zxing sample效果类似,是一条绿色的,一闪一闪的激光线。想实现微信它那种一条绿线从上到下不停移动的效果的话,让UI设计一张“绿线图片”(好拗口)设为ImageView的背景,通过Animation补间动画就可以实现了。
看过效果图之后这里就介绍一下YZxing-lib的结构,方便大家看源码。
callback包里面是请求每帧画面数据信息的回调。camera包是相机相关的类,具体类的介绍这里不再赘述,大家也可以进YZxing-lib源码看,有详细说明。decode包下主要是解码这块功能的类,以及扫码结果的处理。scannerView相当于zxing里面的viewfinderview,在这个类里实现了扫码界面的样式绘制。
使用方式
首先通过在build.gradle文件中添加如下编译语句将YZxing-lib库添加到项目中。
compile 'com.yangy:YZxing-lib:1.1'
或者在直接把GitHub上面的YZxing库下载下来,添加到项目中。 然后在点击跳转到扫码界面的点击事件中,调用如下方法:
Intent intent = new Intent(this, ScannerActivity.class);
//这里可以用intent传递一些参数,比如扫码聚焦框尺寸大小,支持的扫码类型。
// //设置扫码框的宽
// intent.putExtra(Constant.EXTRA_SCANNER_FRAME_WIDTH, 400);
// //设置扫码框的高
// intent.putExtra(Constant.EXTRA_SCANNER_FRAME_HEIGHT, 400);
// //设置扫码框距顶部的位置
// intent.putExtra(Constant.EXTRA_SCANNER_FRAME_TOP_PADDING, 100);
// Bundle bundle = new Bundle();
// //设置支持的扫码类型
// bundle.putSerializable(Constant.EXTRA_SCAN_CODE_TYPE, mHashMap);
// intent.putExtras(bundle);
startActivityForResult(intent, RESULT_REQUEST_CODE);
这里可以使用intent传递一些配置参数。支持有设置扫码框的大小,及位置;设置支持的扫码类型。目前支持的自定义配置不多,后续有机会再扩充。 跳转的时候要有startActivityForResult来跳转,这样在扫码成功之后,返回的结果可以在onActivityResult方法中处理代码如下:
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
if (resultCode == RESULT_OK) {
switch (requestCode) {
case RESULT_REQUEST_CODE:
if (data == null) return;
String type = data.getStringExtra(Constant.EXTRA_RESULT_CODE_TYPE);
String content = data.getStringExtra(Constant.EXTRA_RESULT_CONTENT);
Toast.makeText(MainActivity.this,"codeType:" + type
+ "-----content:" + content,Toast.LENGTH_SHORT).show();
break;
default:
break;
}
}
super.onActivityResult(requestCode, resultCode, data);
}
优化问题
基于zxing的二维码扫码可能会出现扫码速率比较低的问题。这里我所用的几点解决方法。 1.zxing源码是截取的扫码聚焦框里面的图像数据信息来解码,这里可以改成获取全屏的图像信息。实现代码如下:
public PlanarYUVLuminanceSource buildLuminanceSource(byte[] data, int width, int height) {
return new PlanarYUVLuminanceSource(data, width, height, 0, 0,
width, height, false);
}
2.尽量减少支持的扫码类型。zxing源码默认是支持所有的扫码类型。我们项目中使用的话,一般不需要支持这么多。仅支持BarcodeFormat.QR_CODE(二维码)、BarcodeFormat.CODE_128(一维码)就可以应对很多场景了。
3.添加 hints.put(DecodeHintType.TRY_HARDER, true);语句,能够提高扫码精确度,准确率。
这三点是我在使用的,并且取得很大的效果的方法。还有一些提高的扫码速率的方法我就不细说了,这里推荐一篇文章写的蛮好的。
扫码优化策略
总结
在看源码的过程中,别想着一下能看明白,得慢慢看慢慢琢磨,实在想不明白的地方,就别去纠结了,过段时间再去看你当时迷惑的地方,可能就会想明白了。最后附上项目的地址,觉得还不错就start下吧(^__^) 。
YZxing项目地址