0.
最近项目里需要实现二维码的扫描功能,扫描两个二维码然后得到数据进行绑定。目前比较常见的二维码扫描库就是zxing和zbar了,zxing是google官方的开源项目,有专门的维护,java编写。zbar使用C语言写的,而且github上多年没有代码提交了,所以我决定选用zxing。
1.
附上zxing的项目地址:
zxing
打开zxing的github地址,发现似乎没有如何接入的文档。没关心,没有文档,但是有demo,我们要做的就是修改demo,移除无用的功能,只保留二维码的扫描和识别。
2.
下载项目后,里面很多东西我们是不需要的,我们需要的就是这个,如图所示
screenshot.png
这个就是刚才所说的android的demo,新建一个android项目,将这个module导入工程并命名为zxinglib,在这个module里的gradle文件里添加依赖。
dependencies{
api 'com.google.zxing:android-core:3.3.0'
api 'com.google.zxing:core:3.3.2'
}
运行这个module,你会发现这就是一个已经集成好zxing二维码扫描的app,同时还有一些不需要的功能,比如创建二维码,历史记录等等,而且还是相机预览还是横屏。下面我们就分析一下这个demo的二维码识别流程
3.
首先打开AndroidManifest文件,找到含有
的Activity,发现是CaptureActivity,发现他还有好多其他intent-filter,CaptureActivity是可以被其他应用打开的,既然找到了入口,那就进去分析吧。
先看onCreate方法:
@Override
public void onCreate(Bundle icicle) {
super.onCreate(icicle);
Window window = getWindow(); window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
setContentView(R.layout.capture);
hasSurface = false;
inactivityTimer = new InactivityTimer(this);
beepManager = new BeepManager(this);
ambientLightManager = new AmbientLightManager(this);
PreferenceManager.setDefaultValues(this, R.xml.preferences, false);}
window设置标志位,保证屏幕常亮不会黑屏,inactivityTimer保证在电量较低的时候且一段时间没有激活的时候,关闭CaptureActivity,
beepManager是用来扫码时发出声音和震动的,ambientLightManager是用来控制感光的,以此来控制闪光灯的开闭。然后我们再来看布局文件:
布局文件里比较重要的就是这两个,一个surfaceview和一个viewfinderView,一个是照相机用来预览的界面,一个是取景框的界面,剩下的控价都是用来展示扫描结果的,和我们的需求没有太大关系,这里就不说了。
4.
扫描二维码自然不能少的就是相机的调用了,在AndroidManifest文件里,我们也看到了相关权限的声明
有相机,震动和闪光灯的权限。
接着看一下是在哪里初始化相机并调用预览的,在onReumse里
有一段代码
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);
}
这里初始化了surfaceview,同时判断surface是否为true,true就调用initcamera,否在就为surfaceholder添加回调。刚才已经看到了,hasSurface在onCreate的时候赋值为false,之后在onResume也没有进行true的赋值,所以这里在第一次打开的时候,hasSurface=false。
那我们就要关注surfaceHolder的回调了
@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);
}
}
在这里我们发现在surface创建后还是调用了initCamera,进入initCamera看看里面有什么
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();
}
}
这里我们关心两个点,cameraManager.openDriver(surfaceHolder)和CaptureActivityHandler,进入openDriver这个方法
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) {
// Driver failed
Log.w(TAG, "Camera rejected parameters. Setting only minimal safe-mode parameters");
Log.i(TAG, "Resetting to saved camera params: " + parametersFlattened);
// Reset:
if (parametersFlattened != null) {
parameters = cameraObject.getParameters();
parameters.unflatten(parametersFlattened);
try {
cameraObject.setParameters(parameters);
configManager.setDesiredCameraParameters(theCamera, true);
} catch (RuntimeException re2) {
// Well, darn. Give up
Log.w(TAG, "Camera rejected even safe-mode parameters! No configuration");
}
}
}
cameraObject.setPreviewDisplay(holder);
}
这里可以发现,主要是针对camera的一些参数的设定,另外还要说明的一点是sdk21以后,以前的camera类已经废弃了,google又给出了camera2来替他他们,但是目前zxing这个库里还没有使用camera2,关于camera的相关问题,等以后有时间单独来写一篇文章,这里我们主要针对的是流程的分析。这个方法里我们发现调用了CameraConfigurationManager的initFromCameraParameters和setDesiredCameraParameters,这两个方法里找出了相机预览的最佳大小和根据屏幕进行camera方向的旋转,感兴趣的话可以看一下这两个方法。
接下来我们来看CaptureActivityHandler
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,再看看DeocodeThread的run方法
@Override
public void run() {
Looper.prepare();
handler = new DecodeHandler(activity, hints);
handlerInitLatch.countDown();
Looper.loop();
}
这里又实例化了一个DeoceHandler,好,那就进入DeoceHandler,看看这货又是什么
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();
}
}
}
这个decode方法,就是我们心心念念的用来解析二维码的地方,multiFormatReader.decodeWithState(bitmap);得到结果后,返回给CaptureActivityHandler,captureActivityHandler在接到R.id.decode_succeededde message后,会调用CaptureActivity的handleDecode方法,在这里会调用
// Put up our own UI for how to handle the decoded contents.
private void handleDecodeInternally(Result rawResult, ResultHandler resultHandler, Bitmap barcode) {
maybeSetClipboard(resultHandler);
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this);
if (resultHandler.getDefaultButtonID() != null && prefs.getBoolean(PreferencesActivity.KEY_AUTO_OPEN_WEB, false)) {
resultHandler.handleButtonPress(resultHandler.getDefaultButtonID());
return;
}
statusView.setVisibility(View.GONE);
viewfinderView.setVisibility(View.GONE);
resultView.setVisibility(View.VISIBLE);
ImageView barcodeImageView = (ImageView) findViewById(R.id.barcode_image_view);
if (barcode == null) {
barcodeImageView.setImageBitmap(BitmapFactory.decodeResource(getResources(),
R.drawable.launcher_icon));
} else {
barcodeImageView.setImageBitmap(barcode);
}
TextView formatTextView = (TextView) findViewById(R.id.format_text_view);
formatTextView.setText(rawResult.getBarcodeFormat().toString());
TextView typeTextView = (TextView) findViewById(R.id.type_text_view);
typeTextView.setText(resultHandler.getType().toString());
DateFormat formatter = DateFormat.getDateTimeInstance(DateFormat.SHORT, DateFormat.SHORT);
TextView timeTextView = (TextView) findViewById(R.id.time_text_view);
timeTextView.setText(formatter.format(rawResult.getTimestamp()));
TextView metaTextView = (TextView) findViewById(R.id.meta_text_view);
View metaTextViewLabel = findViewById(R.id.meta_text_view_label);
metaTextView.setVisibility(View.GONE);
metaTextViewLabel.setVisibility(View.GONE);
Map metadata = rawResult.getResultMetadata();
if (metadata != null) {
StringBuilder metadataText = new StringBuilder(20);
for (Map.Entry entry : metadata.entrySet()) {
if (DISPLAYABLE_METADATA_TYPES.contains(entry.getKey())) {
metadataText.append(entry.getValue()).append('\n');
}
}
if (metadataText.length() > 0) {
metadataText.setLength(metadataText.length() - 1);
metaTextView.setText(metadataText);
metaTextView.setVisibility(View.VISIBLE);
metaTextViewLabel.setVisibility(View.VISIBLE);
}
}
CharSequence displayContents = resultHandler.getDisplayContents();
TextView contentsTextView = (TextView) findViewById(R.id.contents_text_view);
contentsTextView.setText(displayContents);
int scaledSize = Math.max(22, 32 - displayContents.length() / 4);
contentsTextView.setTextSize(TypedValue.COMPLEX_UNIT_SP, scaledSize);
TextView supplementTextView = (TextView) findViewById(R.id.contents_supplement_text_view);
supplementTextView.setText("");
supplementTextView.setOnClickListener(null);
if (PreferenceManager.getDefaultSharedPreferences(this).getBoolean(
PreferencesActivity.KEY_SUPPLEMENTAL, true)) {
SupplementalInfoRetriever.maybeInvokeRetrieval(supplementTextView,
resultHandler.getResult(),
historyManager,
this);
}
int buttonCount = resultHandler.getButtonCount();
ViewGroup buttonView = (ViewGroup) findViewById(R.id.result_button_view);
buttonView.requestFocus();
for (int x = 0; x < ResultHandler.MAX_BUTTON_COUNT; x++) {
TextView button = (TextView) buttonView.getChildAt(x);
if (x < buttonCount) {
button.setVisibility(View.VISIBLE);
button.setText(resultHandler.getButtonText(x));
button.setOnClickListener(new ResultButtonListener(resultHandler, x));
} else {
button.setVisibility(View.GONE);
}
}
}
用来展示最终的结果。
那么DeoceHandler又是什么时候调用deocode方法的呢?
@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;
}
}
DeoceHandler在收到what==R.id.deocde的message时会调用decode方法,那么是谁发送的这个message呢?还记得CaptureActivityHandler的构造函数里,调用了restartPreviewAndDeocode方法
private void restartPreviewAndDecode() {
if (state == State.SUCCESS) {
state = State.PREVIEW;
cameraManager.requestPreviewFrame(decodeThread.getHandler(), R.id.decode);
activity.drawViewfinder();
}
这个方法里不仅调用了drawViewFinder,绘制了取景框,还调用了
cameraManager.requestPreviewFrame(decodeThread.getHandler(), R.id.decode);这里传入了decodehandler,它又做了什么?
public synchronized void requestPreviewFrame(Handler handler, int message) {
OpenCamera theCamera = camera;
if (theCamera != null && previewing) {
previewCallback.setHandler(handler, message);
theCamera.getCamera().setOneShotPreviewCallback(previewCallback);
}
}
原来这个方法为camera设置了previewcallback,同时previewcallback还持有decodehandler的引用,这个previewcallback的
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");
}
}
在这里Message message = thePreviewHandler.obtainMessage(previewMessage, cameraResolution.x,
cameraResolution.y, data);
message.sendToTarget();
发送了what=previewMessage,而这个previewMessage就是之前CaptureActivityHandler传入的R.id.decode。
那么camera的setOneShotPreviewCallback这个方法是用来干什么的?查看源码看注释
单个预览帧将被返回给提供的处理程序。 数据将以byte []形式到达在message.obj字段中,宽度和高度编码为message.arg1message.arg2。
至此一个从预览到识别解析的流程差不多就分析完了,围绕这些,那些demo里的不需要的东西就可以删除了。
最后附上git地址:
github
作者:滑板上的老砒霜
链接:https://www.jianshu.com/p/a4ba10da4231
來源:简书
简书著作权归作者所有,任何形式的转载都请联系作者获得授权并注明出处。