【基於zxing的編解碼實戰】zxing項目源碼解讀(2.3.0版本,Android部分)

zxing2.3.0目錄結構(android相關)

下載zxing2.3.0後,與android相關的有三個目錄:java

android:就是Barcode Scanner,中文名"條碼掃描器"。下文中簡寫爲BS。android

android-integration:提供一種簡單的途徑將Barcode Scanner整合到調用方app中git

androidtest:模擬調用方app,經過android-integration整合Barcode Scanner數組

若是將androidtest看作你的app的話,那麼上面三者的關係將會以下圖:瀏覽器

integration


android-integration

zxing項目的本意是將BS做爲一個獨立的app,能夠單獨使用,同時亦可簡單被其餘app調用,在調用的過程當中給用戶的感受它們是一個總體。在這中間起到」簡單整合」做用的即是android-integration。微信

正如android-integration的文檔中所述,android-integration的做用在於爲調用方提供一個簡單的方法實現掃描並接受掃描的結果,調用方徹底沒必要了解BS的代碼原理(沒必要爲了整合到調用方app中而學習BS代碼),只需按照簡單的幾個步驟便可輕鬆實現編解碼。網絡

可是這樣看起來簡單的整合存在一個問題:做爲獨立應用的BS必須與調用方app一塊兒被安裝,不然android-integration會要求用戶到應用市場下載BS。而BS同時包含了編解碼功能,若是隻須要解碼功能,那多餘的那部分編碼功能只是徒增了app的大小卻沒有任何用處。研究BS源碼是勢在必行。app

integration的使用方法只需簡單的兩步,具體使用方法可參考IntentIntegrator類的註釋。函數


androidtest

這是一個完整的應用Barcode Scanner的實例,從主界面上便可看出可用的操做,其實大部分操做都是在BS中完成的:工具

該例子須要引入android-support-v4.jar,core-2.3.0.jar,還依賴android-integration。


androidtest代碼比較簡單,基本上沒什麼須要解釋的。第一個Run benchmark是一個用來解碼本地圖片的功能,該功能會掃描${sdcard}/zxingbenchmark目錄下的條碼圖片,並解碼,例子中僅僅打印了圖片的編碼格式,能夠經過com.google.zxing.Result.getText()獲取被編碼的文本。

該功能的核心類就是com.google.zxing.MultiFormatReader和com.google.zxing.Result。後面咱們在分析Barcode Scanner的時候會再次遇到MultiFormatReader。


Barcode Scanner

是一個功能強大的條碼掃描器,不只支持多種類型的條碼,還支持多國語言,分享二維碼,查看掃描歷史,反向掃描等功能。

包結構

image

android:與CaptureActivity直接相關的核心組件。包含了發生震動管理器,閃光燈等等,後面介紹

book:若是查詢的結果是圖書信息,用戶能夠選擇查詢該書的更進一步的詳細信息,該包即包含了搜索與展現書籍的相關類。

camera/camera.open:攝像頭相關組件,核心類是CameraManager

clipboard:剪貼板

encode:編碼功能的各個組件集合。核心類爲QRCodeEncoder,最終實施編碼的是MultiFormatWriter類

history:掃描歷史管理,核心類是HistoryManager

result:條碼掃描的結果被分爲不一樣的類型,全部的類型都定義在com.google.zxing.client.result.ParsedResultType中,對於不一樣的類型都有對應的處理方法:xxxResultHandler,全部的ResultHandler都包含在此包中。不一樣的xxxResultHandler還提供了掃描結果頁面要展現幾個button,每一個button的文本以及須要綁定的事件等等。

result.supplement:對已經掃描並解碼的結果作額外處理的工具集。好比掃描出來的是isbn號,若是在設置中選擇了「檢索更多信息」則會在掃描出isbn號以後自動去網上查詢該書的信息,最後將書的信息展現出來,而若是沒選中,則只會將isbn號碼展現。

share:分享二維碼功能,亦是編碼功能的入口所在。

wifi:是WifiResultHandler的輔助類集合。若是掃描到的二維碼是對wifi信息的編碼,那麼最終掃描結果頁會展現一個「鏈接到網絡」的按鈕,點擊此按鈕就會自動嘗試鏈接。該包中所包含的類則是連接網絡所需的工具類。


代碼解析

按照功能來分,其實BS是由:掃描 、歷史和分享(生成二維碼) 組成。


## 新增內容(2014/3/16) ##

概覽

在較爲詳細地對代碼進行全面分析以前,我把掃碼+解碼的流程說的更簡單一些,由於不少功能咱們並不須要太關注,好比聲音、對焦、閃光燈等等,咱們關注的只是這一套動做是怎麼實現的:

* 打開預覽界面。zxing藉助的確定是camera的功能,經過分析和了解camera的工做原理可知,調用Camera.startPreview()後,就能夠從屏幕中看到預覽界面。

* 捕捉畫面。處在previewing狀態的畫面,隨時均可以捕獲(即便不對焦也能夠,只是模糊一點而已)。調用Camera.setOneShotPreviewCallback(),該方法會在啓動preview以後,預覽界面展現出來後調用(只調用一次便自動解綁),向回調函數Camera.PreviewCallback.onPreviewFrame()中傳回當前畫面的字節數組。

* 解析。拿到字節數組就能夠解碼了。這部分邏輯是包含在core中的,zxing的應用層無需關注的邏輯。

## 新增內容 ##


詳述

在分析這三部分功能以前,先來看下配置。在界面上對應「設置」,在代碼中則是利用了PreferenceFragment類,將用戶對Camera以及掃描行爲的設置保存在SharePreferences中。後文提到的「配置」均指用戶在設置中進行的設置。


掃描

進入掃描界面後大體作了以下的事情:配置Camera並啓動Camera、構建preview與掃描窗口、捕捉畫面並解碼、將解碼結果交給不一樣ResultHandler去處理。下面逐一進行分析。

1. 配置Camera並啓動Camera

啓動Camera是在CaptureActivity.initCamera中進行的,最重要的兩句代碼是:

cameraManager.openDriver(surfaceHolder);
...
	handler = new CaptureActivityHandler(this, decodeFormats, decodeHints,
			characterSet, cameraManager);
...

CameraManager是相機管理類,是BS中惟一與Camera打交道的類,CameraManager.openDriver主要作了三件事:

    /**
     * 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 {
        Camera theCamera = camera;
        if (theCamera == null) {
        	// 1. 獲取手機背面的攝像頭
            theCamera = OpenCameraInterface.open();
            if (theCamera == null) {
                throw new IOException();
            }
            camera = theCamera;
        }
        
        // 2. 設置攝像頭預覽view
        theCamera.setPreviewDisplay(holder);

        if (!initialized) {
            initialized = true;
            configManager.initFromCameraParameters(theCamera);
            if (requestedFramingRectWidth > 0 && requestedFramingRectHeight > 0) {
                setManualFramingRect(requestedFramingRectWidth, requestedFramingRectHeight);
                requestedFramingRectWidth = 0;
                requestedFramingRectHeight = 0;
            }
        }

        Camera.Parameters parameters = theCamera.getParameters();
        String parametersFlattened = parameters == null ? null : parameters.flatten(); // Save
                                                                                       // these,
                                                                                       // temporarily
        try {
        	// 3. 讀取配置並設置相機參數
            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 = theCamera.getParameters();
                parameters.unflatten(parametersFlattened);
                try {
                    theCamera.setParameters(parameters);
                    configManager.setDesiredCameraParameters(theCamera, true);
                } catch (RuntimeException re2) {
                    // Well, darn. Give up
                    Log.w(TAG, "Camera rejected even safe-mode parameters! No configuration");
                }
            }
        }

    }

CameraConfigurationManager是相機輔助類,主要用於設置相機的各種參數。核心方法有兩個:

  • initFromCameraParameters:計算了屏幕分辨率和當前最適合的相機像素

  • setDesiredCameraParameters:讀取配置設置相機的對焦模式、閃光燈模式等等

CaptureActivityHandler類是一個針對掃描任務的Handler,可接收的message有啓動掃描(restart_preview)、掃描成功(decode_succeeded)、掃描失敗(decode_failed)等等。

在建立一個CaptureActivityHandler對象的時候也作了三件事:

    CaptureActivityHandler(CaptureActivity activity, Collection<BarcodeFormat> decodeFormats,
            Map<DecodeHintType, ?> baseHints, String characterSet, CameraManager cameraManager) {
        this.activity = activity;
        
        // 1. 啓動掃描線程
        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;
        
        // 2. 開啓相機預覽界面
        cameraManager.startPreview();
        
        // 3. 將preview回調函數與decodeHandler綁定、調用viewfinderView
        restartPreviewAndDecode();
    }

restartPreviewAndDecode方法又調用了CameraManager.requestPreviewFrame:

    /**
     * A single preview frame will be returned to the handler supplied. The data
     * will arrive as byte[] in the message.obj field, with width and height
     * encoded as message.arg1 and message.arg2, respectively.
     * <br/>
     * 
     * 1:將handler與preview回調函數綁定;<br/>
     * 2:註冊preview回調函數<br/>
     * 綜上,該函數的做用是當相機的預覽界面準備就緒後就會調用hander向其發送傳入的message
     * 
     * @param handler The handler to send the message to.
     * @param message The what field of the message to be sent.
     */
    public synchronized void requestPreviewFrame(Handler handler, int message) {
        Camera theCamera = camera;
        if (theCamera != null && previewing) {
            previewCallback.setHandler(handler, message);
            
            // 綁定相機回調函數,當預覽界面準備就緒後會回調Camera.PreviewCallback.onPreviewFrame
            theCamera.setOneShotPreviewCallback(previewCallback);
        }
    }

2. 構建preview與掃描窗口

首先相機有本身的preview界面,而後咱們須要構造一個掃描窗口,引導用戶將條碼置於窗口中完成掃描。

構造掃描窗口是在CaptureActivityHandler.restartPreviewAndDecode中,經過調用activity.drawViewfinder()來實現的。這裏有個畫掃描窗口的類叫ViewfinderView,該類也是想要改變掃描窗口風格所必須重構的一個類。

因爲網上已經有不少重構ViewfinderView的帖子,本文再也不贅述,能夠參考《基於google Zxing實現二維碼、條形碼掃描,仿微信二維碼掃描效果》

相機的preview界面顯示出來後便可開始掃描,因此須要監聽preview是否已經顯示這個事件,這就是Camera.PreviewCallback的做用。PreviewCallback.onPreviewFrame作的事即是當preview界面展現出來的時候向DecodeHandler發送一個decode消息,DecodeHandler收到該消息後會執行decode方法來解碼。

注意,檢測並觸發捕獲畫面動做的,是Camera.setOneShotPreviewCallback()這個方法。該函數被調用後,若是預覽界面已經打開,就會將包含當前preview frame的byte數組傳給回調函數,此時再向DecodeHandler發送decode消息。


3. 捕捉畫面並解碼

具體參考DecodeHandler.decode方法。(本文只從宏觀上對zxing進行分析,對於解碼的原理將會另開博文進行介紹)


4. 將解碼結果交給不一樣ResultHandler去處理

當DecodeHandler.decode完成解碼後會向CaptureActivityHandler發消息。若是編碼成功則調用CaptureActivity.handleDecode方法對掃描到的結果進行分類處理。

該方法中首先獲取ResultHandler:

// 解析rawResult,根據不一樣類型result生成對應的ResultHandler
ResultHandler resultHandler = ResultHandlerFactory.makeResultHandler(this, rawResult);

而後調用handleDecodeInternally和handleDecodeExternally對ResultHandler進行處理。談到這兩個方法,就不得再也不分析一下IntentSource。

enum IntentSource {

  /**
   * 本地app向BS(Barcode Scanner)發起的啓動指令
   * 好比在androidtest項目中,利用整合的android-integration對BS發起調用指令:com.google.zxing.client.android.SCAN
   * BS中該啓動命令對應的Source類型即是NATIVE_APP_INTENT
   */
  NATIVE_APP_INTENT,
  
  /**
   * 打開BS的時候傳入查詢商品的url,與最終掃描到的product id結合進行查詢
   * 兩種url的形式不一樣
   */
  PRODUCT_SEARCH_LINK,
  ZXING_LINK,
  
  /**
   * 直接打開BS
   */
  NONE

}

結合CaptureActivity.onResume中的部分代碼來理解:

            } else if (dataString != null && dataString.contains("http://www.google")
                    && dataString.contains("/m/products/scan")) {

                // Scan only products and send the result to mobile Product
                // Search.
                source = IntentSource.PRODUCT_SEARCH_LINK;
                sourceUrl = dataString;
                decodeFormats = DecodeFormatManager.PRODUCT_FORMATS;
            } else if (isZXingURL(dataString)) {

                // Scan formats requested in query string (all formats if none
                // specified).
                // If a return URL is specified, send the results there.
                // Otherwise, handle it ourselves.
                source = IntentSource.ZXING_LINK;
                sourceUrl = dataString;
                Uri inputUri = Uri.parse(dataString);
                scanFromWebPageManager = new ScanFromWebPageManager(inputUri);
                decodeFormats = DecodeFormatManager.parseDecodeFormats(inputUri);
                // Allow a sub-set of the hints to be specified by the caller.
                decodeHints = DecodeHintManager.parseDecodeHints(inputUri);
            }

NATIVE_APP_INTENT和NONE很好理解,而PRODUCT_SEARCH_LINK和ZXING_LINK是指定查詢商品的url(而不是交給zxing分析後再決定去哪裏查詢),將掃描出來的內容拼湊到url中,而後在瀏覽器中展現結果。

理解了IntentSource,就容易看懂handleDecodeInternally其實就是將結果展現到界面上。handleDecodeExternally稍複雜些,當source == IntentSource.NATIVE_APP_INTENT時,BS會將掃描分析的結果存到Intent中返回給調用方app,所以調用方app在啓動BS的時候必定要使用startActivityForResult。這一點能夠在androidtest的IntentIntegrator.initiateScan方法的最後看到。


歷史

每掃描成功一次會有一次記錄,邏輯相對簡單,再也不花更多時間分析。


分享

對於分享,其實咱們更關注的是其編碼的實現。這部分代碼就比掃描少得多,精簡事後,核心包已經減小到2個:

image

點擊分享會進入到分享界面,每一種分享方式最終會生成一個二維碼,二維碼界面是EncodeActivity,核心代碼只需參考onResume中的這一句:

Bitmap bitmap = qrCodeEncoder.encodeAsBitmap();

進入encodeAsBitmap中,最終看到的是與MultiFormatReader相對應的方法MultiFormatWriter,此方法是生成二維碼的關鍵。


以上是對zxing項目android部分的代碼簡單介紹。

Barcode Scanner的代碼我已經加了較爲完整的註釋,代碼上傳至git.oschina.net:Zxing-Simplification

相關文章
相關標籤/搜索