Android 掃描二維碼的實現(簡化zxing)

zxing

哎呀呀,在杭州2015 Hackthon的現場,由於沒有二維碼簽到功能,被吐槽low!這是我近期最丟臉的事啦~
因而回來就開始着手開發二維碼相關的東西了。java

一搜索Google和咱們SegmentFault,發如今Android上,Googlezxing這個二維碼的庫比較受歡迎,好,那就是它了(我就是這麼任性= =)android

OK,看下zxing這個項目https://github.com/zxing/zxing
好像很是大啊,如何快速用起來呢?git

答案就是github

簡化!算法

簡化的話,帶來的反作用就是適用性下降,好比在這個場景裏面,咱們不考慮橫屏的狀況,不考慮對攝像頭進行過多的配置,不存在截圖。數組

簡化過程

咱們看下這個項目有一個android目錄,它裏面其實就是條碼掃描器這個app的開源代碼,由於咱們暫時不須要深究它是如何進行圖像識別,如何幫咱們從圖像裏解析出二維碼的,因此咱們就不研究core相關的內容(可是裏面都是乾貨啊!圖像識別的乾貨啊!)app

clipboard.png
咱們最主要就是參考這個裏面的源碼啦。ide

先把整個項目clone下來,而後把android這個源碼導入到Android Studio裏面去,通過小小的配置,就能搞定啦,直接運行咱們就能看見這個App了。函數

先看下咱們要作的工做是啥:工具

  1. 對攝像頭進行管理(爲了兼容老設備,咱們要用即將被Google捨棄的android.hardware.camera類)。

  2. 對獲取的一幀圖片進行解析。

  3. 對解析的結果進行處理。

若是不帶着問題或者目的去看源碼的話,會很是沒頭緒,因此咱們必定要記得咱們想要作什麼。

OK,先看第一個問題,咱們須要對攝像頭進行管理。咱們能夠看到目錄中有三個文件和攝像機有關

clipboard.png
就是這三個文件啦。

用途
CameraConfigurationManager 這個類主要用於對攝像頭進行一些配置,包括旋轉角度、預覽圖尺寸等等
CameraConfigurationUtils 工具類,在CameraConfigurationManager調用,
CameraManager 控制攝像頭的生命週期,獲取預覽尺寸,生成原始數據發送給解析器

在這裏,我把CameraConfigurationUtils拷貝過來,另外兩個類合成到CameraManager裏面,在初始化的時候,作好配置。

攝像頭的管理(橫屏改豎屏)

由於原先zxing給的demo是橫屏的,這裏要改爲豎屏,就須要作幾個配置。

getFramingRectInPreview

getFramingRectInPreview這個函數中,cameraResolutionscreenResolution方向不對,因此

rect.left = rect.left * cameraResolution.y / screenResolution.x;
rect.right = rect.right * cameraResolution.y / screenResolution.x;
rect.top = rect.top * cameraResolution.x / screenResolution.y;
rect.bottom = rect.bottom * cameraResolution.x / screenResolution.y;

這裏須要把方向換一下。

binary data

還須要更改的地方,就是在獲取幀預覽中的原始數據,須要進行一個旋轉,由於zxing原先是對橫向的圖片一行一行讀取的,若是咱們給予縱向的數據,就必須旋轉數據,或者更改讀取算法。 這裏更改數據可能會更加方便,
buildLuminanceSource中,更改:

byte[] 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;

最後記得把攝像頭的預覽旋轉改爲90°便可。

camera.setDisplayOrientation(90);

附上這部分代碼(比較長)

public class CameraManager {

    private static final int MIN_FRAME_WIDTH = 240;
    private static final int MIN_FRAME_HEIGHT = 240;

    // 這裏修正掃描大小
    private static final int MAX_FRAME_WIDTH = 675; // = 5/8 * 1920
    private static final int MAX_FRAME_HEIGHT = 1200; // = 5/8 * 1080

    private Context mContext;
    private Point mScreenResolution;
    private Point mCameraResolution;
    private Point mBestPreviewSize;
    private Point mPreviewSizeOnScreen;

    private Rect mFramingRectInPreview;
    private Rect mFramingRect;

    private Camera mCamera;
    private PreviewCallback mPreviewCallback;
    private AutoFocusManager mAutoFocusManager;

    public CameraManager(Context context) {
        mContext = context;
        mPreviewCallback = new PreviewCallback(this);
    }

    public void openDriver(Camera camera, SurfaceHolder holder) {
        mCamera = camera;

        Camera.Parameters parameters = camera.getParameters();

        CameraConfigurationUtils.setBarcodeSceneMode(parameters);
        CameraConfigurationUtils.setFocus(parameters,
                true,
                true,
                false);

        Point theScreenResolution = new Point();
        Rect rect = holder.getSurfaceFrame();
        theScreenResolution.set(rect.height(), rect.width());
        mScreenResolution = theScreenResolution;
        mCameraResolution = CameraConfigurationUtils.findBestPreviewSizeValue(parameters, mScreenResolution);
        mBestPreviewSize = CameraConfigurationUtils.findBestPreviewSizeValue(parameters, mScreenResolution);

        boolean isScreenPortrait = mScreenResolution.x < mScreenResolution.y;
        boolean isPreviewSizePortrait = mBestPreviewSize.x < mBestPreviewSize.y;

        if (isScreenPortrait == isPreviewSizePortrait) {
            mPreviewSizeOnScreen = mBestPreviewSize;
        } else {
            mPreviewSizeOnScreen = new Point(mBestPreviewSize.y, mBestPreviewSize.x);
        }

        parameters.setPreviewSize(mBestPreviewSize.x, mBestPreviewSize.y);
        camera.setParameters(parameters);
        camera.setDisplayOrientation(90);

        Camera.Parameters afterParameters = camera.getParameters();
        Camera.Size afterSize = afterParameters.getPreviewSize();
        if (afterSize != null && (mBestPreviewSize.x != afterSize.width || mBestPreviewSize.y != afterSize.height)) {
            mBestPreviewSize.x = afterSize.width;
            mBestPreviewSize.y = afterSize.height;
        }

    }

    public synchronized Rect getFramingRect() {
        if (mFramingRect == null) {
            Point screenResolution = mScreenResolution;
            if (screenResolution == null) {
                // Called early, before init even finished
                return null;
            }

            int width = findDesiredDimensionInRange(screenResolution.x, MIN_FRAME_WIDTH, MAX_FRAME_WIDTH);
            int height = findDesiredDimensionInRange(screenResolution.y, MIN_FRAME_HEIGHT, MAX_FRAME_HEIGHT);

            int leftOffset = (screenResolution.x - width) / 2;
            int topOffset = (screenResolution.y - height) / 2;
            mFramingRect = new Rect(leftOffset, topOffset, leftOffset + width, topOffset + height);
        }
        return mFramingRect;
    }

    public Point getCameraResolution() {
        return mCameraResolution;
    }

    public synchronized Rect getFramingRectInPreview() {
        if (mFramingRectInPreview == null) {
            Rect framingRect = getFramingRect();
            if (framingRect == null) {
                return null;
            }
            Rect rect = new Rect(framingRect);
            Point cameraResolution = mCameraResolution;
            Point screenResolution = mScreenResolution;
            if (cameraResolution == null || screenResolution == null) {
                // Called early, before init even finished
                return null;
            }
            rect.left = rect.left * cameraResolution.y / screenResolution.x;
            rect.right = rect.right * cameraResolution.y / screenResolution.x;
            rect.top = rect.top * cameraResolution.x / screenResolution.y;
            rect.bottom = rect.bottom * cameraResolution.x / screenResolution.y;
            mFramingRectInPreview = rect;
        }
        return mFramingRectInPreview;
    }

    private static int findDesiredDimensionInRange(int resolution, int hardMin, int hardMax) {
        int dim = 5 * resolution / 8; // Target 5/8 of each dimension
        if (dim < hardMin) {
            return hardMin;
        }
        if (dim > hardMax) {
            return hardMax;
        }
        return dim;
    }

    /**
     * A factory method to build the appropriate LuminanceSource object based on the format
     * of the preview buffers, as described by Camera.Parameters.
     *
     * @param data A preview frame.
     * @param width The width of the image.
     * @param height The height of the image.
     * @return A PlanarYUVLuminanceSource instance.
     */
    public PlanarYUVLuminanceSource buildLuminanceSource(byte[] data, int width, int height) {
        Rect rect = getFramingRectInPreview();
        if (rect == null) {
            return null;
        }

        byte[] 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;

        // Go ahead and assume it's YUV rather than die.
        return new PlanarYUVLuminanceSource(rotatedData, width, height, rect.left, rect.top,
                rect.width(), rect.height(), false);
    }

    /**
     * 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.
     *
     * @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) {
        if (mCamera != null) {
            mPreviewCallback.setHandler(handler, message);
            mCamera.setOneShotPreviewCallback(mPreviewCallback);
        }
    }

    public synchronized void startPreview() {
        mCamera.startPreview();
        mAutoFocusManager = new AutoFocusManager(mContext, mCamera);
    }

    public synchronized void stopPreview() {
        if (mAutoFocusManager != null) {
            mAutoFocusManager.stop();
            mAutoFocusManager = null;
        }
        if (mCamera != null) {
            mCamera.stopPreview();
        }
        mPreviewCallback.setHandler(null, 0);

    }

    public synchronized void closeDriver() {
        mCamera.release();
        mCamera = null;
    }
}

獲取圖像數據

Camera提供這麼一個函數:setOneShotPreviewCallback,看名字就知道,它是會在某個時間點,回調你給的接口,而後傳入一個二進制的圖像數組,讓你去解析,這正是咱們想要的東西,因此咱們看下這個PreviewCallback,它只有一個函數,

@Override
public void onPreviewFrame(byte[] data, Camera camera) {
}

很顯然,第一個參數就是圖像數組了,咱們看看zxing裏面是怎麼處理這個數據的。

解析數據

@Override
public void onPreviewFrame(byte[] data, Camera camera) {
    Point cameraResolution = mCameraManager.getCameraResolution();
    Handler thePreviewHandler = mPreviewHandler;
    if (cameraResolution != null && thePreviewHandler != null) {
        Message message = thePreviewHandler.obtainMessage(mPreviewMessage, cameraResolution.x,
                cameraResolution.y, data);
        message.sendToTarget();
        mPreviewHandler = null;
    } else {
    }
}

zxing是發送到另一個handler,也就是說,這個解析數據的過程比較浪費時間,因此要開線程來解決這個事。
咱們跟蹤這個previewHanlder能夠發現,它是一個DecodeHandler,其中有個重要的decode函數

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();
        }
    }
}

這裏用到咱們剛剛重寫的buildLuminanceSource這個函數了,能夠理解這個函數是對咱們的原始數據作一個包裝,來給解析器讀取。

解析器的配置咱們能夠看看DecodeThread這個類(專門用於解析圖像的線程)

...
 decodeFormats = EnumSet.noneOf(BarcodeFormat.class);
decodeFormats.addAll(DecodeFormatManager.PRODUCT_FORMATS);
decodeFormats.addAll(DecodeFormatManager.INDUSTRIAL_FORMATS);
decodeFormats.addAll(DecodeFormatManager.QR_CODE_FORMATS);
decodeFormats.addAll(DecodeFormatManager.DATA_MATRIX_FORMATS);
mHints.put(DecodeHintType.POSSIBLE_FORMATS, decodeFormats);

這裏咱們能夠爲咱們的掃描器加更多格式的支持(好比條形碼、二維碼、XX碼)

zxing是使用List來管理這些解析器,而後每次讀取數據,都通過這些解析器過濾一遍,若是解析成功就有結果,不然就沒有結果。

最後咱們跟蹤R.id.decode_success找到CaptureActivityHandler
這裏的handleMessage中有

case R.id.decode_succeeded:
        state = State.SUCCESS;
        Bundle bundle = message.getData();
        Bitmap barcode = null;
        float scaleFactor = 1.0f;
        if (bundle != null) {
            byte[] compressedBitmap = bundle.getByteArray(DecodeThread.BARCODE_BITMAP);
            if (compressedBitmap != null) {
                barcode = BitmapFactory.decodeByteArray(compressedBitmap, 0, compressedBitmap.length, null);
                // Mutable copy:
                barcode = barcode.copy(Bitmap.Config.ARGB_8888, true);
            }
            scaleFactor = bundle.getFloat(DecodeThread.BARCODE_SCALED_FACTOR);
        }
        activity.handleDecode((Result) message.obj, barcode, scaleFactor);
        break;

咱們讀取Result對象的text成員變量就是咱們想要的二維碼信息了!最後的工做固然是解析咱們的二維碼中的URI,而後進行後續的工做啦~

PS: 附帶二維碼掃描的SegmentFault for Android版本 即將上線!!

歡迎關注我Github 以及 weibo@Gemini

相關文章
相關標籤/搜索