Android掃一掃:zxing的集成與優化

0.

最近項目裏須要實現二維碼的掃描功能,掃描兩個二維碼而後獲得數據進行綁定。目前比較常見的二維碼掃描庫就是zxing和zbar了,zxing是google官方的開源項目,有專門的維護,java編寫。zbar使用C語言寫的,並且github上多年沒有代碼提交了,因此我決定選用zxing。java

1.

附上zxing的項目地址:
zxing
打開zxing的github地址,發現彷佛沒有如何接入的文檔。沒關心,沒有文檔,可是有demo,咱們要作的就是修改demo,移除無用的功能,只保留二維碼的掃描和識別。android

2.

 

 

下載項目後,裏面不少東西咱們是不須要的,咱們須要的就是這個,如圖所示git

screenshot.pnggithub

 

這個就是剛纔所說的android的demo,新建一個android項目,將這個module導入工程並命名爲zxinglib,在這個module裏的gradle文件裏添加依賴。api

dependencies{
    api 'com.google.zxing:android-core:3.3.0'
    api 'com.google.zxing:core:3.3.2'
}

運行這個module,你會發現這就是一個已經集成好zxing二維碼掃描的app,同時還有一些不須要的功能,好比建立二維碼,歷史記錄等等,並且仍是相機預覽仍是橫屏。下面咱們就分析一下這個demo的二維碼識別流程app

3.

首先打開AndroidManifest文件,找到含有ide

<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是用來控制感光的,以此來控制閃光燈的開閉。而後咱們再來看佈局文件:oop

<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

做者:滑板上的老砒霜 連接:https://www.jianshu.com/p/a4ba10da4231 來源:簡書 簡書著做權歸做者全部,任何形式的轉載都請聯繫做者得到受權並註明出處。

相關文章
相關標籤/搜索