1. 程式人生 > >Android 基於Zxing的掃碼功能實現(二)

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庫的實現效果:

效果圖1

效果圖2(掃碼成功)
(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專案地址

補充