上一篇文章中Zxing项目介绍之移植篇我们介绍了如何将Zxing项目移植到自己的项目,并且简化了大量的代码只剩下基本的扫描功能,这篇文章将针对前一篇文章的简化后的Demo解析Zxing二维码解析的大致过程。
整个过程其实就在CaptureActivity里面,布局先简单介绍下,布局就是一个宽高都是fill_parent的SurfaceView,然后屏幕中间一个宽高258dp正方形的RelativeLayout用来做我们扫描的框框,并且这个CaptureActivity我们在Androidmainfiest设置的屏幕方向是portrait
<activity android:name="com.google.zxing.client.android.CaptureActivity"
android:screenOrientation="portrait"
/>
官网demo默认是横屏,我们的这个屏幕方向改动很重要,决定了后面我们需要对源码做的修改,这里先mark下。
接下来我们重点看CaptureActivity的onResume方法
@Override
protected void onResume() {
super.onResume();
// CameraManager must be initialized here, not in onCreate(). This is necessary because we don't
// want to open the camera driver and measure the screen size if we're going to show the help on
// first launch. That led to bugs where the scanning rectangle was the wrong size and partially
// off screen.
cameraManager = new CameraManager(getApplication());
handler = null;
beepManager.updatePrefs();
inactivityTimer.onResume();
SurfaceView surfaceView = (SurfaceView) findViewById(R.id.preview_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);
}
}
这个方法里核心在初始化了CameraManager,并且当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);
}
}
intiCamera是比较重要的方法,里面初始化摄像头数据之后将开始进行数据扫描
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, null, null, null, cameraManager);
}
initScanArea();
} 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();
}
刚开始先进行openDriver打开摄像头,然后new了CaptureActivityHandler,这个是实际的数据处理类,
initScanArea我们则是计算出我们ui上用来扫描二维码的框框在手机屏幕上的具体位置
/**
* 初始化扫描的矩形区域
*/
private void initScanArea() {
int cameraWidth = cameraManager.getCameraResolution().y;
int cameraHeight = cameraManager.getCameraResolution().x;
/** 获取布局中扫描框的位置信息 */
int[] location = new int[2];
mRlScanArea.getLocationInWindow(location);
int cropLeft = location[0];
int cropTop = location[1] - getStatusBarHeight();
int cropWidth = mRlScanArea.getWidth();
int cropHeight = mRlScanArea.getHeight();
/** 获取布局容器的宽高 */
int containerWidth = mRlScanContainer.getWidth();
int containerHeight = mRlScanContainer.getHeight();
/** 计算最终截取的矩形的左上角顶点x坐标 */
int x = cropLeft * cameraWidth / containerWidth;
/** 计算最终截取的矩形的左上角顶点y坐标 */
int y = cropTop * cameraHeight / containerHeight;
/** 计算最终截取的矩形的宽度 */
int width = cropWidth * cameraWidth / containerWidth;
/** 计算最终截取的矩形的高度 */
int height = cropHeight * cameraHeight / containerHeight;
/** 生成最终的截取的矩形 */
mScanRect = new Rect(x, y, width + x, height + y);
}
我们根据屏幕上框框的实际位置去根据摄像头的视觉宽高来转换我们屏幕上框框的位置对应摄像头视觉所看到的数据位置,最终生成一个矩形区域mScanRect。
再回到重点的CaptureActivityHandler,这个类继承Handler,用来处理各种扫描解析的消息,在构造函数里就开始了扫描的过程
CaptureActivityHandler(CaptureActivity activity,
Collection decodeFormats,
Map baseHints,
String characterSet,
CameraManager cameraManager) {
this.activity = activity;
decodeThread = new DecodeThread(activity, decodeFormats, baseHints, characterSet, null
);//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()方法
theCamera.getCamera().startPreview();
restartPreviewAndDecode方法就是不断发送解析消息给decodeThread进行解析
private void restartPreviewAndDecode() {
if (state == State.SUCCESS) {
state = State.PREVIEW;
cameraManager.requestPreviewFrame(decodeThread.getHandler(), R.id.decode);
}
}
/**
* 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是啥,它的类叫PreviewCallback,并实现了Camera.PreviewCallback的onPreviewFrame接口
@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的run方法很简单
@Override
public void run() {
Looper.prepare();
handler = new DecodeHandler(activity, hints);
handlerInitLatch.countDown();
Looper.loop();
}
开启这个线程的消息循环以及new了该线程对应的Handler,所以我们的解析过程就是在这个DecodeHandler里
@Override
public void handleMessage(Message message) {
if (!running) {
return;
}
int what = message.what;
if(what == R.id.decode){
decode((byte[]) message.obj, message.arg1, message.arg2);
}
else if(what == R.id.quit){
running = false;
Looper.myLooper().quit();
}
}
刚刚我们发送的是decode消息,所以看decode方法,这个方法代码比较多,比较乱,分段来看
Camera.Size size = activity.getCameraManager().getPreviewSize();
// 这里需要将获取的data翻转一下,因为相机默认拿的的横屏的数据
byte[] rotatedData = new byte[data.length];
for (int y = 0; y < size.height; y++) {
for (int x = 0; x < size.width; x++)
rotatedData[x * size.height + size.height - y - 1] = data[x + y * size.width];
}
// 宽高也要调整
int tmp = size.width;
size.width = size.height;
size.height = tmp;
先是对摄像头获取的byte[]数据做个翻转,因为相机默认拿横屏的数据,所以官方demo默认横屏扫描,我们在一开始说了改CaptureActivity改成竖屏,因此解析拿数据的时候也要相应颠倒下数据。
拿到数据之后就是数据的解析了
long start = System.currentTimeMillis();
Result rawResult = null;
PlanarYUVLuminanceSource source = buildLuminanceSource(rotatedData, size.width, size.height);
if (source != null) {
BinaryBitmap bitmap = new BinaryBitmap(new HybridBinarizer(source));
try {
rawResult = multiFormatReader.decodeWithState(bitmap);
} catch (ReaderException re) {
// continue
} finally {
multiFormatReader.reset();
}
}
上面不做深入分析,涉及一些二维码编解码的协议之类的,我也没有细看,但大致知道在讲什么内容,我们前面得到的byte数组数据是整个屏幕的,但我们扫描的框只有屏幕中间一小部分(前面说的258dp的正方形),因此我们根据这个扫描框的宽高单独抽出整个摄像头数据中对应扫码框位置的数据(buildLuminanceSource方法),
multiFormatReader就是具体的解码类,它的decodeWithState,会把扫描的数据根据添加的编解码方式遍历每种去解析数据,如果能够解析出数据也就是rawResult是不是为空
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();
}
}
并将结果发送给CaptureActivity的handler即CaptureActivityHandler
@Override
public void handleMessage(Message message) {
int what = message.what;
if(what == R.id.restart_preview){
restartPreviewAndDecode();
}
else if(what == R.id.decode_succeeded){
state = State.SUCCESS;
// Bundle bundle = message.getData();
// Bitmap barcode = null;
// float scaleFactor = 1.0f;
// if (bundle != null) {
// byte[] compressedBitmap = bundle.getByteArray(DecodeThread.BARCODE_BITMAP);
// if (compressedBitmap != null) {
// barcode = BitmapFactory.decodeByteArray(compressedBitmap, 0, compressedBitmap.length, null);
// // Mutable copy:
// barcode = barcode.copy(Bitmap.Config.ARGB_8888, true);
// }
// scaleFactor = bundle.getFloat(DecodeThread.BARCODE_SCALED_FACTOR);
// }
activity.handleDecode((Result) message.obj);
}
else if(what == R.id.decode_failed){
state = State.PREVIEW;
cameraManager.requestPreviewFrame(decodeThread.getHandler(), R.id.decode);
}
}
解析失败会继续扫描解析,解析成功就将解析结果回给Activity
public void handleDecode(Result rawResult) {
inactivityTimer.onActivity();
beepManager.playBeepSoundAndVibrate();
Intent resultIntent = new Intent();
resultIntent.putExtra("result", rawResult.getText());
this.setResult(RESULT_OK, resultIntent);
CaptureActivity.this.finish();
}
这个代码就很明朗了,解析成功震动下,关闭Activity并将数据传回调用处。