Android 基於Zxing的掃碼功能實現(二)
本篇文章已授權微信公眾號 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);這個方法主要是的內容是尋找最好的預覽尺寸。尋找最佳預覽尺寸的邏輯我就不說了,這塊,可以看下這位兄弟寫的
http://iluhcm.com/2016/01/08/scan-qr-code-and-recognize-it-from-picture-fastly-using-zxing/ 裡面說明了尋找最佳預覽尺寸的邏輯,及優化。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'(建議更新至2.1)
--->compile 'com.yangy:YZxing-lib:2.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);
// //設定是否啟用從相簿獲取二維碼(預設為FALSE,不啟用)。
// intent.putExtra(Constant.EXTRA_IS_ENABLE_SCAN_FROM_PIC,true);
// 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專案地址