1. 程式人生 > >掃描二維碼研究總結(高仿微信掃一掃,輕鬆實現定製掃描介面)

掃描二維碼研究總結(高仿微信掃一掃,輕鬆實現定製掃描介面)

在正文之前說點題外話,加上這篇我已經寫了3篇部落格了,其實我寫部落格的初衷不是想證明自己有多牛,並且我也只是從事安卓開發只有半年時間的小渣,但是不想成為大牛的渣不是好渣,所以我想通過部落格把工作學習中遇到的問題進行研究總結,從而提高自己,與此同時如果能給廣大從事安卓開發的朋友們提供幫助或者是提供一點點思路我也是很心滿意足了!~~好了,廢話不多少進入正題吧!

先上介面圖。由於目前不會錄屏,所以直接上截圖吧!~另外掃描框內本身有一個從上之下迴圈的綠線,但是不知道為毛手機截圖沒顯示。。。汗。。
這裡寫圖片描述

最近公司需要一個二維碼掃描的需求,於是我便在網上下載了一個應用谷歌zxing的開源專案,但是這個專案介面不符合公司需求,並且不靈活,我便研究了下這個專案,在這個專案的基礎上進行了修改,編寫了ZxingScannerViewNew這個類。先看下目錄結構。
這裡寫圖片描述


整個專案是在Android studio上進行開發的,zxinglib是一個Android Lib modul,libs資料夾中便是谷歌zxing的jar包,主要解碼等方法都是這個包提供的。

BarcodeScannerView,CameraPreview,CameraUtils,DisplayUtils,ViewFinderView,這幾個檔案都是第三方專案提供的。我結合了BarcodeScannerView和ViewFinderView主要功能,並且進行了修改,使ZxingScannerViewNew更方便自定義掃描介面。CameraPreview是Camera的預覽介面。下面先看下ZxingScannerViewNew這個類。

ZxingScannerViewNew繼承FrameLayout並且實現Camera.PreviewCallback介面(後面有介紹)。看下成員變數。

    private Camera mCamera;
    private CameraPreview mPreview;//相機預覽介面
    private View showPanel;//掃描介面
    private QrSize qrSize;//自己定義的介面

其中QrSize是自己定義的介面,程式碼如下。

    public interface QrSize {
        public Rect getDetectRect
(); }

這個介面主要是獲取掃描區域的Rect矩形座標等資訊。接下來看下幾個重要的方法。

 private void init() {//初始化方法
        addView(mPreview = new CameraPreview(getContext()));
        //載入預設掃描介面
        showPanel = View.inflate(getContext(), R.layout.default_scan, null);
        addView(showPanel);
        initMultiFormatReader();//初始化解碼的類
    }

    public void setContentView(int res) {
        try {
            View panelView = View.inflate(getContext(), res, null);
            removeView(showPanel);
            showPanel = panelView;
            addView(showPanel);
        } catch (Exception e) {
            return;
        }
    }

在init方法中會初始化相機預覽介面CameraPreview並且加入到ZxingScannerViewNew中作為底層。接著初始化預設掃描介面,並且加入到ZxingScannerViewNew中。因為ZxingScannerViewNew是framelayout所以showPanel 會覆蓋到相機預覽介面之上。如果呼叫setContentView方法,會移除預設的掃描介面,並且把傳遞進來的layout覆蓋到相機預覽介面之上,所以通過這個方法可以輕鬆實現自定義掃面介面。

接下來介紹下Camera.PreviewCallback介面。

很多時候,android攝像頭模組不僅預覽拍照這麼簡單,而是需要在預覽的同時,能夠做出一些檢測,比如最常見的人臉檢測。在未按下拍照按鈕前,就檢測出人臉然後顯示矩形提示框,再按拍照。那麼如何獲得預覽幀視訊呢?只需要繼承PreviewCallback這個介面就行了。繼承這個方法後,會自動過載這個函式:
public void onPreviewFrame(byte[] data, Camera camera) {}。
這個函式裡的data就是實時預覽幀視訊資料。一旦程式呼叫PreviewCallback介面,就會自動呼叫onPreviewFrame這個函式。呼叫PreviewCallback的方法有三種
1,setPreviewCallback。
2,setOneShotPreviewCallback。
3, setPreviewCallbackWithBuffer,。
一般是使用第二種方式。

什麼時候觸發onPreviewFrame()這個函式呢?可以是按一個按鍵觸發一次,就在按鍵的監聽裡執行setOneShotPreviewCallback,便會自動觸發一次。但大多數希望程式自動每隔多長時間,自動進行一次檢測預覽幀。所以我會在程式中設定一個timer每隔1.5秒便執行一次。

private void startFreshThread(final Camera camera, final Camera.PreviewCallback callback) {
       //初始化每timer每隔1.5秒設定一次 setOneShotPreviewCallback
        timer = new Timer();
        timer.scheduleAtFixedRate(new TimerTask() {
            @Override
            public void run() {
                Log.e("Run", "Running");
                if (camera == null || !cameraAvailable()) {
                    cancelTask();
                    return;
                }
                camera.setOneShotPreviewCallback(callback);
            }
        }, 0, 1500);
    }

    private byte[] rotatedData;
    private boolean onlyOnce;

    @Override
    public void onPreviewFrame(byte[] data, Camera camera) {
        if (onlyOnce) {//只開啟timer一次
            startFreshThread(camera, this);
            onlyOnce = false;
        }
        Camera.Parameters parameters = camera.getParameters();
        //獲取幀視訊size
        Camera.Size size = parameters.getPreviewSize();
        int width = size.width;//獲取幀視訊寬度
        int height = size.height;//獲取幀視訊高度
        if (DisplayUtils.getScreenOrientation(getContext()) == Configuration.ORIENTATION_PORTRAIT) {//判斷是否是豎屏
            if (rotatedData == null) {
                rotatedData = new byte[data.length];
            }
            for (int y = 0; y < height; y++) {
                for (int x = 0; x < width; x++)
                    rotatedData[x * height + height - y - 1] = data[x + y * width];
            }
            //交換高和寬的值
            int tmp = width;
            width = height;
            height = tmp;
            data = rotatedData;
        }
        Result rawResult = null;
        //呼叫zxing jar包中的方法 生成解碼所需的YUV型別資料
PlanarYUVLuminanceSource source = buildLuminanceSource(data, width, height);
        if (source != null) {
        //呼叫zxing jar包中的方法將source轉為bitmap
        BinaryBitmap bitmap = new BinaryBitmap(new HybridBinarizer(source));
try {
     //呼叫zxing jar包中的方法進行解碼
      rawResult = mMultiFormatReader.decodeWithState(bitmap);
    } catch (NullPointerException npe) {

    } catch (ArrayIndexOutOfBoundsException aoe) {

    } finally {
                mMultiFormatReader.reset();
    }
   }
        if (rawResult != null) {
            if (mResultHandler != null) {
                //將結果返回給回撥方法handleResult
                mResultHandler.handleResult(rawResult);
            }
        }
    }

需要說明的是預設情況下獲取的幀視訊是倒著的,所以如果手機是豎屏我們需要交換寬和高的值。上面程式碼中通過buildLuminanceSource方法形成解碼所需資料,所以我們看下這個方法。

 public PlanarYUVLuminanceSource buildLuminanceSource(byte[] data, int width, int height) {
        Rect rect = getFramingRectInPreview(width, height);
        if (rect == null) {
            return null;
        }
        // Go ahead and assume it's YUV rather than die.
        PlanarYUVLuminanceSource source = null;

        try {
        //新建PlanarYUVLuminanceSource物件
            source = new PlanarYUVLuminanceSource(data, width, height, rect.left, rect.top,
                    rect.width(), rect.height(), false);
        } catch (Exception e) {
        }

        return source;
    }

這個方法有3個引數,第一個是幀視訊資料,第二個第三個為幀視訊的寬和高,通過getFramingRectInPreview獲得掃描區的rect ,接著把rect的左邊界座標,上邊界座標、高度、寬度還有幀視訊的高寬交給zxing jar包提供的方法來new一個PlanarYUVLuminanceSource物件並把這個物件返回。

接著看下getFramingRectInPreview方法的程式碼。

public synchronized Rect getFramingRectInPreview(int previewWidth, int previewHeight) {
        if (qrSize == null || qrSize.getDetectRect() == null) {
            return null;
        }
        //通過回撥方法獲取掃描區的rect
        Rect rect = qrSize.getDetectRect();

        int width=showPanel.getWidth();
        int height=showPanel.getHeight();
        if ((rect.right - rect.left) != 0 && (rect.top - rect.bottom) != 0) {
            rect.left = rect.left * previewWidth / width;
            rect.right = rect.right * previewWidth / width;
            rect.top = rect.top * previewHeight / height;
            rect.bottom = rect.bottom * previewHeight / height;
            return rect;
        } else {
            return null;
        }
    }

程式碼中主要通過回撥方法getDetectRect獲取掃描區的rect,另外有一個計算公式。

         rect.left = rect.left * previewWidth / width;
         rect.right = rect.right * previewWidth / width;
         rect.top = rect.top * previewHeight / height;
         rect.bottom = rect.bottom * previewHeight /height; 

這部分程式碼根據視訊幀的高度、寬度和掃描介面高寬對掃描區進行縮放,主要是為了防止相機預覽介面不是全屏導致rect資料錯誤問題。

好了ZxingScannerViewNew這個類我們已經介紹的差不多了。接下來我們只需在自己的activity中通過setContentView方法把自己定製的掃描layout傳遞給ZxingScannerViewNew,然後把掃描區域的rect計算好,通過QrSize介面傳遞給ZxingScannerViewNew,最後在回撥方法handleResult中接收掃描結果即可~~~

微信掃一掃中會與一根綠線自上而下迴圈顯示,我的解決方案是在掃描區域中設定一根綠線imageview起始狀態設定為gone,介面初始化後讓它visiable,同時新增自上而下的迴圈移動動畫。

最後貼上activity程式碼。

package com.afun.zxingcore;

import android.app.Activity;
import android.graphics.Rect;
import android.os.Bundle;
import android.view.View;
import android.view.animation.Animation;
import android.view.animation.TranslateAnimation;
import android.widget.ImageView;
import android.widget.TextView;

import com.afun.zxinglib.ScanView.ZXingScannerViewNew;
import com.google.zxing.BarcodeFormat;
import com.google.zxing.Result;


import java.util.ArrayList;
import java.util.List;

public class QrScanActivity extends Activity implements ZXingScannerViewNew.ResultHandler, ZXingScannerViewNew.QrSize, View.OnClickListener {
    ZXingScannerViewNew scanView;
    private TextView result;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        //初始化自己定製的掃描介面
        scanView = new ZXingScannerViewNew(this);
        scanView.setContentView(R.layout.logistics_scan_qr);
        scanView.setQrSize(this);
        setContentView(scanView);
        setupFormats();
        initUI();
    }

    private void initUI() {
        findViewById(R.id.confirm).setOnClickListener(this);
        result= (TextView) findViewById(R.id.editText);
    }

    @Override
    protected void onResume() {
        super.onResume();
        //設定處理掃描結果的回撥方法
        scanView.setResultHandler(this);
        //設定camera
        scanView.startCamera(-1);
        scanView.setFlash(false);
        scanView.setAutoFocus(true);
    }

    @Override
    public void handleResult(Result rawResult) {
        //回撥方法中接收掃描結果並且設定到textview中
        result.setText(rawResult.toString());
    }

    @Override
    protected void onPause() {
        super.onPause();
        scanView.stopCamera();
    }

    //zxing專案可以解析二維碼和條形碼等,我們這裡只新增二維碼格式,讓他只處理二維碼
    public void setupFormats() {
        List<BarcodeFormat> formats = new ArrayList<BarcodeFormat>();
        formats.add(BarcodeFormat.QR_CODE);
        if (scanView != null) {
            scanView.setFormats(formats);
        }
    }

       //計算掃描區域的rect,演算法比較簡單就不解釋了
    @Override
    public Rect getDetectRect() {
        View view = findViewById(R.id.scan_window);
        int top = ((View) view.getParent()).getTop() + view.getTop();
        int left = view.getLeft();
        int width = view.getWidth();
        int height = view.getHeight();
        Rect rect = null;
        if (width != 0 && height != 0) {
            rect = new Rect(left, top, left + width, top + height);
            addLineAnim(rect);
        }
        return rect;
    }

    //給掃描框內的綠線設定移動動畫,讓它在掃描區域內迴圈移動
    private void addLineAnim(Rect rect) {
        ImageView imageView = (ImageView) findViewById(R.id.scanner_line);
        imageView.setVisibility(View.VISIBLE);
        if (imageView.getAnimation() == null) {
            TranslateAnimation anim = new TranslateAnimation(0, 0, 0, rect.height());
            anim.setDuration(1500);
            anim.setRepeatCount(Animation.INFINITE);
            imageView.startAnimation(anim);
        }
    }

    @Override
    public void onClick(View v) {
        int id = v.getId();
        if (id == R.id.confirm){
            //TODO something
        }
    }
}

我自己定製的掃描介面如下圖
這裡寫圖片描述
其實layout佈局也很簡單,下面我貼出下載地址,感興趣的話大家可以下載下來研究研究!~~~

好了,寫到這了,希望能給大家帶來思路上的提示!~歡迎大家留言一起交流!~