哎呀呀,在杭州2015 Hackthon
的現場,由於沒有二維碼簽到功能,被吐槽low!這是我近期最丟臉的事啦~
因而回來就開始着手開發二維碼相關的東西了。java
一搜索Google
和咱們SegmentFault
,發如今Android
上,Google
的zxing
這個二維碼的庫比較受歡迎,好,那就是它了(我就是這麼任性= =)android
OK,看下zxing
這個項目https://github.com/zxing/zxing
好像很是大啊,如何快速用起來呢?git
答案就是github
簡化!算法
簡化的話,帶來的反作用就是適用性下降,好比在這個場景裏面,咱們不考慮橫屏的狀況,不考慮對攝像頭進行過多的配置,不存在截圖。數組
咱們看下這個項目有一個android
目錄,它裏面其實就是條碼掃描器
這個app的開源代碼,由於咱們暫時不須要深究它是如何進行圖像識別,如何幫咱們從圖像裏解析出二維碼的,因此咱們就不研究core
相關的內容(可是裏面都是乾貨啊!圖像識別的乾貨啊!)app
咱們最主要就是參考這個裏面的源碼啦。ide
先把整個項目clone下來,而後把android
這個源碼導入到Android Studio
裏面去,通過小小的配置,就能搞定啦,直接運行咱們就能看見這個App了。函數
先看下咱們要作的工做是啥:工具
對攝像頭進行管理(爲了兼容老設備,咱們要用即將被Google捨棄的
android.hardware.camera
類)。對獲取的一幀圖片進行解析。
對解析的結果進行處理。
若是不帶着問題或者目的去看源碼的話,會很是沒頭緒,因此咱們必定要記得咱們想要作什麼。
OK,先看第一個問題,咱們須要對攝像頭進行管理。咱們能夠看到目錄中有三個文件和攝像機有關
就是這三個文件啦。
類 | 用途 |
---|---|
CameraConfigurationManager |
這個類主要用於對攝像頭進行一些配置,包括旋轉角度、預覽圖尺寸等等 |
CameraConfigurationUtils |
工具類,在CameraConfigurationManager 調用, |
CameraManager |
控制攝像頭的生命週期,獲取預覽尺寸,生成原始數據發送給解析器 |
在這裏,我把CameraConfigurationUtils
拷貝過來,另外兩個類合成到CameraManager
裏面,在初始化的時候,作好配置。
由於原先zxing給的demo是橫屏的,這裏要改爲豎屏,就須要作幾個配置。
在getFramingRectInPreview
這個函數中,cameraResolution
和screenResolution
方向不對,因此
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;
這裏須要把方向換一下。
還須要更改的地方,就是在獲取幀預覽
中的原始數據,須要進行一個旋轉,由於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版本 即將上線!!