×

Android扫一扫:zxing的集成与优化

96
滑板上的老砒霜
2018.04.21 15:21* 字数 1456

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文件,找到含有

    <intent-filter>
      <action android:name="android.intent.action.MAIN"/>
      <category     android:name="android.intent.category.LAUNCHER"/>
    </intent-filter>

的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 android:id="@+id/preview_view"
           android:layout_width="fill_parent"
           android:layout_height="fill_parent"/>

  <com.google.zxing.client.android.ViewfinderView
  android:id="@+id/viewfinder_view"
  android:layout_width="fill_parent"
  android:layout_height="fill_parent"/>

布局文件里比较重要的就是这两个,一个surfaceview和一个viewfinderView,一个是照相机用来预览的界面,一个是取景框的界面,剩下的控价都是用来展示扫描结果的,和我们的需求没有太大关系,这里就不说了。

4.

扫描二维码自然不能少的就是相机的调用了,在AndroidManifest文件里,我们也看到了相关权限的声明

<uses-permission android:name="android.permission.CAMERA"/>
  <uses-permission    android:name="android.permission.INTERNET"/>
  <uses-permission android:name="android.permission.VIBRATE"/>
  <uses-permission android:name="android.permission.FLASHLIGHT"/>

有相机,震动和闪光灯的权限。
接着看一下是在哪里初始化相机并调用预览的,在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<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,再看看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<ResultMetadataType,Object> metadata = rawResult.getResultMetadata();
if (metadata != null) {
  StringBuilder metadataText = new StringBuilder(20);
  for (Map.Entry<ResultMetadataType,Object> 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

关注我的公众号

image
日记本
Web note ad 1