BarcodeScanner源碼分析

項目地址: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.類關系圖

BarcodeScanner.png

從類圖上看大部分類都跟BarcodeScannerView相關,BarcodeScannerView是繼承自FrameLayout的,這個布局是用來放置我們用于相機預覽的SurfaceView以及我們自定義的掃描界面IViewFinder以及實現(xiàn)控制閃光燈的功能。先大致了解這么多,下面我們詳細的來看。

4.源碼分析

BarcodeScanner的實現(xiàn)并不復雜,我們也就按照慣例,從它的調用流程開始看:

1.BarcodeScannerView的實現(xiàn)

可以在使用方法中首先看到,我們在ActivityonCreate()方法里實例化了一個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,IViewFinderCameraHandlerThread,這些都是具體用來做什么的呢?我相信大家通過名字和其中的一些方法應該已經大致知道,下面我們就一起來看看這些類到底有些什么作用.

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.PreviewCallbackonPreviewFrame()方法的回調都在子線程中了。這樣做的好處是什么呢?我大致列舉了兩個

  • 在子線程中初始化相機,能增加掃碼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()方法。所以到此我們就大致知道了BarcodeScannerZxing的掃碼是如何實現(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ā)更多有用的知識給大家.

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
【社區(qū)內容提示】社區(qū)部分內容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內容

友情鏈接更多精彩內容