項目地址:BarcodeScanner,本文分析版本: 78278e7
1.簡介
項目開發(fā)中我們經常遇到需要掃描二維碼功能的需求,二維碼識別算法我們都知道有zxing,但是它提供的demo里并不支持豎屏掃描,而且定制性也不好,不能拿來即用。所以github上又產生一些對zxing封裝的項目,能很簡易的集成而且又提供良好定制性。今天我們就來介紹其中一個項目BarcodeScanner,也是我認為相當不錯的一個項目。
BarcodeScanner同時提供了zxing掃描方案和zbar掃描方案。zbar的掃描算法是C實現(xiàn)的,掃描速度要比zxing快,但是錯誤率好像要高于zxing。我并沒有在項目中實際使用過zbar,具體是參照這里。本文我們并不分析zxing的實現(xiàn)方式(我也不會。。),我們只分析BarcodeScanner具體的使用和實現(xiàn)方式。
2.使用方法
BarcodeScanner的集成和使用方法相當簡單,我們放上一個集成好的Activity代碼:
public class SimpleScannerActivity extends BaseScannerActivity implements ZXingScannerView.ResultHandler {
private ZXingScannerView mScannerView;
private static final String TAG = "SimpleScannerActivity";
@Override
public void onCreate(Bundle state) {
super.onCreate(state);
setContentView(R.layout.activity_simple_scanner);
setupToolbar();
ViewGroup contentFrame = (ViewGroup) findViewById(R.id.content_frame);
mScannerView = new ZXingScannerView(this);
contentFrame.addView(mScannerView);
Log.d(TAG, "onCreate id : " + Thread.currentThread().getName());
}
@Override
public void onResume() {
super.onResume();
mScannerView.setResultHandler(this);
mScannerView.startCamera();
}
@Override
public void onPause() {
super.onPause();
mScannerView.stopCamera();
}
@Override
public void handleResult(Result rawResult) {
Toast.makeText(this, "Contents = " + rawResult.getText() +
", Format = " + rawResult.getBarcodeFormat().toString(), Toast.LENGTH_SHORT).show();
}
}
- 首先實現(xiàn)
ZXingScannerView.ResultHandler接口,該接口的handleResult(Result rawResult);回調方法就是用來處理掃描出的結果rawResult的. - 如果你不需要自定制掃描的界面,
BarcodeScanner提供給了我們一個默認實現(xiàn)的樣式,所以使用默認樣式直接mScannerView = new ZXingScannerView(this);如果需要自定義界面則需要重寫ZXingScannerView的父類BarcodeScannerView中的createViewFinderView(Context context)方法,代碼如下:
mScannerView = new ZXingScannerView(this) {
@Override
protected IViewFinder createViewFinderView(Context context) {
//返回我們自定義的IViewFinder
return new CustomViewFinderView(context);
}
};
- 分別在
onResume()和onPause()里執(zhí)行響應的函數。
此外BarcodeScanner還支持:
- 開關閃光燈
- 是否開啟自動對焦
- 自定義掃描條形碼或者二維碼的格式
- 前后置攝像頭切換
代碼這里就不貼了,大家可以參照demo中具體的這個類FullScannerActivity。
3.類關系圖

從類圖上看大部分類都跟
BarcodeScannerView相關,BarcodeScannerView是繼承自FrameLayout的,這個布局是用來放置我們用于相機預覽的SurfaceView以及我們自定義的掃描界面IViewFinder以及實現(xiàn)控制閃光燈的功能。先大致了解這么多,下面我們詳細的來看。
4.源碼分析
BarcodeScanner的實現(xiàn)并不復雜,我們也就按照慣例,從它的調用流程開始看:
1.BarcodeScannerView的實現(xiàn)
可以在使用方法中首先看到,我們在Activity的onCreate()方法里實例化了一個ZXingScannerView對象,并添加到我們的布局里。進入ZXingScannerView類里看到它是繼承自BarcodeScannerView類的,所以我們再跟進到BarcodeScannerView中去看看具體的實現(xiàn),發(fā)現(xiàn)BarcodeScannerView繼承自FrameLayout并且實現(xiàn)了Camera.PreviewCallback接口,下面是BarcodeScannerView的具體實現(xiàn),省略了部分非主要代碼:
public abstract class BarcodeScannerView extends FrameLayout implements Camera.PreviewCallback {
//Camera對象
private Camera mCamera;
//Camera預覽對象,繼承自SurfaceView,負責刷新相機預覽界面
private CameraPreview mPreview;
//自定義的掃描遮罩,控制掃描框的大小
private IViewFinder mViewFinderView;
//掃描區(qū)域的大小
private Rect mFramingRectInPreview;
//相機HandlerThread,負責在子線程中開啟相機
private CameraHandlerThread mCameraHandlerThread;
//閃光燈狀態(tài)
private Boolean mFlashState;
private boolean mAutofocusState = true;
public BarcodeScannerView(Context context) {
super(context);
}
public BarcodeScannerView(Context context, AttributeSet attributeSet) {
super(context, attributeSet);
}
public final void setupLayout(Camera camera) {
removeAllViews();
//實例化相機預覽對象,并添加到一個RelativeLayout里
mPreview = new CameraPreview(getContext(), camera, this);
RelativeLayout relativeLayout = new RelativeLayout(getContext());
relativeLayout.setGravity(Gravity.CENTER);
relativeLayout.setBackgroundColor(Color.BLACK);
relativeLayout.addView(mPreview);
addView(relativeLayout);
//實例化mViewFinderView對象,并添加到布局中
mViewFinderView = createViewFinderView(getContext());
if (mViewFinderView instanceof View) {
addView((View) mViewFinderView);
} else {
throw new IllegalArgumentException("IViewFinder object returned by " +
"'createViewFinderView()' should be instance of android.view.View");
}
}
//創(chuàng)建二維碼掃描樣式,重寫此方法即可自定義樣式,
protected IViewFinder createViewFinderView(Context context) {
return new ViewFinderView(context);
}
//通過HandlerThread開啟相機
public void startCamera(int cameraId) {
if(mCameraHandlerThread == null) {
mCameraHandlerThread = new CameraHandlerThread(this);
}
mCameraHandlerThread.startCamera(cameraId);
}
//開啟相機預覽
public void setupCameraPreview(Camera camera) {
mCamera = camera;
if(mCamera != null) {
setupLayout(mCamera);
mViewFinderView.setupViewFinder();
if(mFlashState != null) {
setFlash(mFlashState);
}
setAutoFocus(mAutofocusState);
}
}
public void startCamera() {
startCamera(-1);
}
//關閉相機
public void stopCamera() {
if(mCamera != null) {
mPreview.stopCameraPreview();
mPreview.setCamera(null, null);
mCamera.release();
mCamera = null;
}
if(mCameraHandlerThread != null) {
mCameraHandlerThread.quit();
mCameraHandlerThread = null;
}
}
//停止預覽
public void stopCameraPreview() {
if(mPreview != null) {
mPreview.stopCameraPreview();
}
}
//恢復預覽
protected void resumeCameraPreview() {
if(mPreview != null) {
mPreview.showCameraPreview();
}
}
//得到預覽區(qū)域中需要掃描的區(qū)域的Rect
public synchronized Rect getFramingRectInPreview(int previewWidth, int previewHeight) {
if (mFramingRectInPreview == null) {
Rect framingRect = mViewFinderView.getFramingRect();
int viewFinderViewWidth = mViewFinderView.getWidth();
int viewFinderViewHeight = mViewFinderView.getHeight();
if (framingRect == null || viewFinderViewWidth == 0 || viewFinderViewHeight == 0) {
return null;
}
Rect rect = new Rect(framingRect);
rect.left = rect.left * previewWidth / viewFinderViewWidth;
rect.right = rect.right * previewWidth / viewFinderViewWidth;
rect.top = rect.top * previewHeight / viewFinderViewHeight;
rect.bottom = rect.bottom * previewHeight / viewFinderViewHeight;
mFramingRectInPreview = rect;
}
return mFramingRectInPreview;
}
//省略了部分閃光燈相關代碼
}
可以看到BarcodeScannerView是一個抽象類,它主要是用來組織我們用來預覽圖像的SurfaceView以及決定我們二維碼掃描樣式的IViewFinder對象,以及提供出一些控制相機和閃光燈的方法。以及實現(xiàn)了Camera.PreviewCallback接口,但是在這里并未實現(xiàn),所以繼承自BarcodeScannerView的子類需要實現(xiàn)這個接口,并處理相機的回調數據。
所以在BarcodeScannerView里有還有三個對象比較重要依次是:CameraPreview,IViewFinder和CameraHandlerThread,這些都是具體用來做什么的呢?我相信大家通過名字和其中的一些方法應該已經大致知道,下面我們就一起來看看這些類到底有些什么作用.
2.CameraPreview的實現(xiàn)
CameraPreview是繼承自SurfaceView的,做過相機開發(fā)的都知道,一般我們需要在SurfaceView中更新相機的預覽畫面,所以CameraPreview就是一個標準的SurfaceView的實現(xiàn),這里我們就不再在分析SurfaceView是如何使用的.我只要知道CameraPreview類里是真正控制
相機預覽以及顯示相機預覽的就可以了,具體的代碼希望大家自行去研究。
值得一提的是CameraPreview中設置預覽畫面大小的方法值得我們學習。因為我們知道不同手機使用的攝像頭是不同的,而且攝像頭所支持的分辨率也不盡相同,而且我們更有可能給我們的掃碼界面設置不一樣的大小,所以一個好的掃碼庫一定要做到自適應大小才是最好的,那讓我們看看它是怎么做的:
public void setupCameraParameters() {
//首先根據當前View的寬高,再根據所有相機支持的分辨率
//來找出一個最適合的相機Size
Camera.Size optimalSize = getOptimalPreviewSize();
//獲得相機的參數并且設定
Camera.Parameters parameters = mCamera.getParameters();
parameters.setPreviewSize(optimalSize.width, optimalSize.height);
mCamera.setParameters(parameters);
//再調整View的寬高
adjustViewSize(optimalSize);
}
實現(xiàn)的流程已經在上面了,詳細的方法我們這里就不展開說了,有興趣的可以自己查看.
3.IViewFinder的實現(xiàn)
IViewFinder是一個為了讓我們能自定義掃碼框而抽象的一個接口,代碼如下:
public interface IViewFinder {
//當相機預覽開始的時候調用,一般推薦用來更新掃描框的大小和刷新View
void setupViewFinder();
//返回掃碼識別的區(qū)域
Rect getFramingRect();
//返回View的寬度,因為View中已經實現(xiàn),所以你不需要重寫這個方法.
int getWidth();
//返回View的高度,因為View中已經實現(xiàn),所以你不需要重寫這個方法.
int getHeight();
}
IViewFinder是為了幫助我們能自定義掃碼框以及掃碼的區(qū)域抽象出來的一個接口,在前面我們知道.我們在實現(xiàn)任何掃碼框的樣式的時候都需要繼承自View否則在添加時將會拋出異常,實現(xiàn)起來就比較簡單了,當然BarcodeScanner中為我們默認實現(xiàn)了一個ViewFinderView。代碼我們就補貼了,大家可以自行閱讀。
4.CameraHandlerThread的實現(xiàn)
public class CameraHandlerThread extends HandlerThread {
private static final String LOG_TAG = "CameraHandlerThread";
private BarcodeScannerView mScannerView;
public CameraHandlerThread(BarcodeScannerView scannerView) {
super("CameraHandlerThread");
mScannerView = scannerView;
start();
}
public void startCamera(final int cameraId) {
Handler localHandler = new Handler(getLooper());
localHandler.post(new Runnable() {
@Override
public void run() {
//在子線程中獲得camera對象
final Camera camera = CameraUtils.getCameraInstance(cameraId);
Handler mainHandler = new Handler(Looper.getMainLooper());
mainHandler.post(new Runnable() {
@Override
public void run() {
mScannerView.setupCameraPreview(camera);
}
});
}
});
}
}
CameraHandlerThread的實現(xiàn)看起來相當簡單,但是它的作用卻是相當重要的,根據代碼我們可以看到,其實是在子線程中獲取到了camera對象,再通過Looper.getMainLooper()初始化一個運行在主線程中的Handler通知mScannerView.setupCameraPreview(camera);。這樣處理之后我們整個的相機就運行在子線程中了,包括相機預覽的刷新以及Camera.PreviewCallback的onPreviewFrame()方法的回調都在子線程中了。這樣做的好處是什么呢?我大致列舉了兩個
- 在子線程中初始化相機,能增加掃碼Activity的進入速度。
- 由于相機更新是在子線程中的,那么我們如果再在主線程中實現(xiàn)各種動畫就會相當流暢。不會有卡頓現(xiàn)象。
所以CameraHandlerThread還是相當有必要的。
5.ZXingScannerView的實現(xiàn)
所以在ZXingScannerView中只要做初始化Zxing的相關類以及在onPreviewFrame(byte[] data, Camera camera)回調方法中處理我們相機回調的數據掃描即可,為了驗證onPreviewFrame(byte[] data, Camera camera)是運行在子線程中我們可以看到方法的最后:
@Override
public void onPreviewFrame(byte[] data, Camera camera) {
if(mResultHandler == null) {
return;
}
.....省略部分掃碼相關代碼
final Result finalRawResult = rawResult;
if (finalRawResult != null) {
Handler handler = new Handler(Looper.getMainLooper());
handler.post(new Runnable() {
@Override
public void run() {
ResultHandler tmpResultHandler = mResultHandler;
mResultHandler = null;
stopCameraPreview();
if (tmpResultHandler != null) {
tmpResultHandler.handleResult(finalRawResult);
}
}
});
} else {
camera.setOneShotPreviewCallback(this);
}
}
通過一個Handler在主線程中回調handleResult()方法。所以到此我們就大致知道了BarcodeScanner中Zxing的掃碼是如何實現(xiàn)的了,聰明的同學應該也已經知道這個庫中Zbar掃碼的模塊也應該是如何實現(xiàn)的了,無非也是繼承自BarcodeScannerView只不過是使用Zbar的掃描算法來完成掃碼識別,那么到底是不是這樣?還歡迎大家把BarcodeScanner這個庫clone來自己驗證了!好了源碼實現(xiàn)我們就寫到這。
5.個人評價
BarcodeScanner是我在項目開發(fā)中使用過的二維碼掃描庫中個人覺得結構和可拓展性都比較好的庫,在當初使用這個庫的時候,它并沒有實現(xiàn)CameraHandlerThread,當時自己也實現(xiàn)了在HandlerThread中開啟相機的功能,實現(xiàn)的方法這里就不寫了,因為BarcodeScanner的這種實現(xiàn)方式要比我自己實現(xiàn)的要好。所以總體來說值得推薦!
多說一句,因為最近眼睛做了手術,所以我只寫了一遍博客,并沒有review難免有錯字或者寫錯的地方,還希望大家?guī)兔χ赋?最后謝謝大家的支持。_
我每周會寫一篇源代碼分析的文章,以后也可能會有其他主題.
如果你喜歡我寫的文章的話,歡迎關注我的新浪微博@達達達達sky
地址: http://weibo.com/u/2030683111
每周我會第一時間在微博分享我寫的文章,也會積極轉發(fā)更多有用的知識給大家.