目前github及各個博客平臺有關掃碼用的都是Camera1, 筆者今天就來個用Camera2實現的, 且功能齊全
Step1 Camera2實現閱覽
要實現閱覽要先得選配各個參數,包括預覽尺寸, 輸出尺寸等
StreamConfigurationMap map = mCameraCharacteristics.get(
CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP);
for (android.util.Size size : map.getOutputSizes(mPreview.getOutputClass())) {
int width = size.getWidth();
int height = size.getHeight();
if (width <= MAX_PREVIEW_WIDTH && height <= MAX_PREVIEW_HEIGHT) {
mPreviewSizes.add(new Size(width, height));
}
}
這段代碼則可以拿到相機支持的所有輸出比例中所有 不超過 MAX_PREVIEW_WIDTH , MAX_PREVIEW_HEIGHT 的尺寸,將其存儲。
Size prelargest = mPreviewSizes.sizes(mAspectRatio).last();
mYuvReader = ImageReader.newInstance(prelargest.getWidth(), prelargest.getHeight(),
ImageFormat.YUV_420_888, /* maxImages */ 2);
mYuvReader.setOnImageAvailableListener(mOnYuvAvailableListener, WorkThreadServer
這裏的prelargest就是根據外部傳的閱覽比例及上一步我們存儲的所有比例進行匹配一個最大的尺寸
Camera2 不同與Camera1 我們能拿到的是一個ImageReader, 我們可以指定輸出格式, 推薦用的是ImageFormat.YUV_420_888,當相機輸出數據後便會走mOnYuvAvailableListener回調
準備做好後就可以打開相機
mCameraManager = (CameraManager) context.getSystemService(Context.CAMERA_SERVICE);
mCameraManager.openCamera(mCameraId, mCameraDeviceCallback, WorkThreadServer.getInstance().getBgHandle());
相機打開後會走CameraDevice.StateCallback回到,我們需要在這裏邊實現閱覽
CaptureRequest.Builder mPreviewRequestBuilder = mCamera.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW);
mPreviewRequestBuilder.addTarget(surface);
mPreviewRequestBuilder.addTarget(mYuvReader.getSurface());
mCamera.createCaptureSession(Arrays.asList(surface
, mImageReader.getSurface()
, mYuvReader.getSurface()
),
mSessionCallback, WorkThreadServer.getInstance().getBgHandle());
先創建Builder , 在build中添加兩個輸出目標, 一個就是閱覽的SurfaceView, 或者TextureView的surface, 推薦使用TextureView, 經實測, 華爲高版本機型的SurfaceView仍然是窗口屬性。 另一個就是之前創建的ImageReader, 這裏調用createCaptureSession,在mSessionCallback回調用開始輸出。
mCaptureSession.setRepeatingRequest(mPreviewRequestBuilder.build(),
mCaptureCallback, WorkThreadServer.getInstance().getBgHandle());
調用setRepeatingRequest 畫布上就能看到內容了,同時也會走ImageReader中的回調
Step2 調整TextureView寬高,避免拉伸
只要能保證TexrureView控件的寬高比 與相機輸出的保持一致即可
//當顯示的寬高比,與相機輸出的寬高比不同時
//當實際略寬時, 調整高度保證與輸出比例相同
if (height < width * ratio.getY() / ratio.getX()) {
mImpl.getView().measure(
MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY),
MeasureSpec.makeMeasureSpec(width * ratio.getY() / ratio.getX(),
MeasureSpec.EXACTLY));
}
//當實際略高時,調整寬度保證與輸出比例相同
else {
mImpl.getView().measure(
MeasureSpec.makeMeasureSpec(height * ratio.getX() / ratio.getY(),
MeasureSpec.EXACTLY),
MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY));
}
我們在自定義TextureView的onMeasure方法中調整寬高,這裏的ratio是相機的輸出比例
OK, 這些完成後,父容器無論怎麼變化, 畫面也不會拉伸
Step3 ImageReader中讀取YUV數據
/***
* yuv數據回調
*/
private ImageReader.OnImageAvailableListener mOnYuvAvailableListener = new ImageReader.OnImageAvailableListener() {
@Override
public void onImageAvailable(ImageReader reader) {
mCallback.onPreviewByte(CameraHelper.readYuv(reader));
}
};
這裏的CameraHelper.readYuv(reader)是一個讀取ImageReader的方法
/***
* ImageReader中讀取YUV
*/
public static byte[] readYuv(ImageReader reader) {
if (android.os.Build.VERSION.SDK_INT < android.os.Build.VERSION_CODES.KITKAT) {
return null;
}
Image image = null;
image = reader.acquireLatestImage();
if (image == null)
return null;
byte[] data = getByteFromImage(image);
image.close();
return data;
}
private static byte[] getByteFromImage(Image image) {
if (android.os.Build.VERSION.SDK_INT < android.os.Build.VERSION_CODES.KITKAT) {
return null;
}
int w = image.getWidth(), h = image.getHeight();
int i420Size = w * h * 3 / 2;
Image.Plane[] planes = image.getPlanes();
//remaining0 = rowStride*(h-1)+w => 27632= 192*143+176
int remaining0 = planes[0].getBuffer().remaining();
int remaining1 = planes[1].getBuffer().remaining();
//remaining2 = rowStride*(h/2-1)+w-1 => 13807= 192*71+176-1
int remaining2 = planes[2].getBuffer().remaining();
//獲取pixelStride,可能跟width相等,可能不相等
int pixelStride = planes[2].getPixelStride();
int rowOffest = planes[2].getRowStride();
byte[] nv21 = new byte[i420Size];
byte[] yRawSrcBytes = new byte[remaining0];
byte[] uRawSrcBytes = new byte[remaining1];
byte[] vRawSrcBytes = new byte[remaining2];
planes[0].getBuffer().get(yRawSrcBytes);
planes[1].getBuffer().get(uRawSrcBytes);
planes[2].getBuffer().get(vRawSrcBytes);
if (pixelStride == w) {
//兩者相等,說明每個YUV塊緊密相連,可以直接拷貝
System.arraycopy(yRawSrcBytes, 0, nv21, 0, rowOffest * h);
System.arraycopy(vRawSrcBytes, 0, nv21, rowOffest * h, rowOffest * h / 2 - 1);
} else {
byte[] ySrcBytes = new byte[w * h];
byte[] vSrcBytes = new byte[w * h / 2 - 1];
for (int row = 0; row < h; row++) {
//源數組每隔 rowOffest 個bytes 拷貝 w 個bytes到目標數組
System.arraycopy(yRawSrcBytes, rowOffest * row, ySrcBytes, w * row, w);
//y執行兩次,uv執行一次
if (row % 2 == 0) {
//最後一行需要減一
if (row == h - 2) {
System.arraycopy(vRawSrcBytes, rowOffest * row / 2, vSrcBytes, w * row / 2, w - 1);
} else {
System.arraycopy(vRawSrcBytes, rowOffest * row / 2, vSrcBytes, w * row / 2, w);
}
}
}
System.arraycopy(ySrcBytes, 0, nv21, 0, w * h);
System.arraycopy(vSrcBytes, 0, nv21, w * h, w * h / 2 - 1);
}
return nv21;
}
稍微有點長, 直接就貼出來了 , 拿到byte數組後就可以交給Zxing了,
Step4 線程池任務控制實現併發掃碼
if (Holder.INSTANCE.executor == null) {
Holder.INSTANCE.executor = new ThreadPoolExecutor(
2, 5, 1, TimeUnit.SECONDS,
new ArrayBlockingQueue<>(20, true), new ThreadPoolExecutor.DiscardOldestPolicy());
}
這裏創建了一個20長度捨棄頭部數據策略的線程池,保證併發的同時不至於OOM
創建一個像素資源管理者
public class AbleManager extends PixsValuesAble {
List<PixsValuesAble> ableList = new ArrayList<>();
private AbleManager(Handler handler) {
super(handler);
//ableList.add(new XQRScanAble(handler));
ableList.add(new XQRScanZoomAble(handler));
ableList.add(new LighSolveAble(handler));
}
public static AbleManager getInstance(Handler handler) {
return new AbleManager(handler);
}
@Override
public void cusAction(byte[] data, int dataWidth, int dataHeight) {
for (PixsValuesAble able : ableList) {
WorkThreadServer.getInstance()
.post(() -> able.cusAction(data, dataWidth, dataHeight));
}
}
public void release() {
ableList.clear();
WorkThreadServer.getInstance().quit();
}
}
XQRScanAble 只能夠掃碼
XQRScanZoomAble 繼承XQRScanAble, 掃碼同時可以放大二維碼
LighSolveAble 計算環境亮度
最後遍歷ableList分別交給線程池管理執行。
Step5 掃碼的細節實現
Zxing掃碼需要一個BinaryBitmap
/***
* 字節轉BinaryBitmap
*/
public static BinaryBitmap byteToBinaryBitmap(byte[] bytes, int dataWidth, int dataHeight) {
getScanByteRect(dataWidth, dataHeight);
PlanarYUVLuminanceSource source = buildLuminanceSource(bytes, dataWidth, dataHeight,
Config.scanRect.getScanR());
return new BinaryBitmap(new HybridBinarizer(source));
}
這裏bytes則是輸出的整個預覽範圍,dataWide是數據寬, dataHeight數據高,通過getScanByteRect(dataWidth, dataHeight);來剪裁數據區域
/***
* 獲取顯示區域對應的相機源數據解碼區域
* @return
*/
public static Rect getScanByteRect(int dataWidth, int dataHeight) {
if (Config.scanRect.getScanR() == null) {
Config.scanRect.setScanR(new Rect());
Config.scanRect.getScanR().left = (int) (Config.scanRect.getRect().left * dataHeight);
Config.scanRect.getScanR().top = (int) (Config.scanRect.getRect().top * dataWidth);
Config.scanRect.getScanR().right = (int) (Config.scanRect.getRect().right * dataHeight);
Config.scanRect.getScanR().bottom = (int) (Config.scanRect.getRect().bottom * dataWidth);
Config.scanRect.setScanR(rotateRect(Config.scanRect.getScanR()));
}
return Config.scanRect.getScanR();
}
依照的就是,TexrureView的真實寬高和其父容器的寬高比較, 最後對應到數據矩形中,得到掃碼區域矩形, 這樣能準備的保證可使區域都可以得到解析。
if (result != null)
return;
//先生產掃碼需要的BinaryBitmap
binaryBitmap = ScanHelper.byteToBinaryBitmap(data, dataWidth, dataHeight);
result = reader.decode(binaryBitmap);
if (result != null) {
Message.obtain(handler, Config.SCAN_RESULT, result).sendToTarget();
binaryBitmap = null;
}
}
這樣掃描有結果後回調到View中。
這裏是 自動縮放 的實現
Zxing提供一個探測器DetectorResult, 通過它可以獲取碼幾個點座標, 我們根據左邊估算出碼長度, 這樣就可以縮放碼了。
super.cusAction(data, dataWidth, dataHeight);
if (binaryBitmap == null)
return;
DetectorResult decoderResult = null;
ResultPoint[] points;
try {
decoderResult = new Detector(binaryBitmap.getBlackMatrix()).detect(null);
} catch (NotFoundException | FormatException e) {
e.printStackTrace();
}
if (decoderResult == null)
return;
points = decoderResult.getPoints();
int lenght = ScanHelper.getQrLenght(points);
if (lenght < Config.scanRect.getPreX() / 3 * 2) {
//自動變焦時間間隔爲500ms
if (System.currentTimeMillis() - zoomTime < 500)
return;
Message.obtain(handler, Config.AUTO_ZOOM, Config.currentZoom + 0.05 + "")
.sendToTarget();
zoomTime = System.currentTimeMillis();
}
}
這裏是 計算環境亮度 , 解析像素字節顏色值得到一個亮度值,與我們設定的值比較, 變化後發出Message,在View中作出動作。
int avDark = LightHelper.getAvDark(data, dataWidth, dataHeight);
if (avDark > STANDVALUES && !isBright) {
isBright = true;
Message.obtain(handler, Config.LIGHT_CHANGE, true)
.sendToTarget();
}
if (avDark < STANDVALUES && isBright) {
isBright = false;
Message.obtain(handler, Config.LIGHT_CHANGE, false)
.sendToTarget();
}
}
手勢縮放
相對於Camera1, 的zoom縮放, Camera2 較爲複雜
try {
mPreviewRequestBuilder.set(CaptureRequest.SCALER_CROP_REGION, CameraHelper.getZoomRect(mCameraCharacteristics, 1));
mCaptureSession.setRepeatingRequest(mPreviewRequestBuilder.build(), mCaptureCallback, WorkThreadServer.getInstance().getBgHandle());
} catch (Exception e) {
}
}
先的計算出縮放後的區域然後設置CaptureRequest.SCALER_CROP_REGION屬性才能實現
掃碼條動畫 用的是跟支付寶微信同樣風格
animator = ValueAnimator.ofFloat(0f, measuredHeight.toFloat())
.setDuration(2000)
animator.addUpdateListener { it ->
val values = it.animatedValue as Float
if (values <= ALPHA_LENGHT) {
alpha = (values / ALPHA_LENGHT).let {
if (it > 1f)
1f
else it
}
} else {
alpha = ((measuredHeight - values) / ALPHA_LENGHT).let {
if (it < 0f)
0f
else it
}
}
translationY = values
}
animator.repeatCount = Int.MAX_VALUE - 1
animator.repeatMode = ValueAnimator.RESTART
animator.start()
頭部和尾部漸變, 這樣觀感挺不錯。
掃碼格式歸納
經可能準確的提供需要的Reader,能夠有效提升解碼效率
/**
* 所有格式
*/
ALL,
/**
* 所有一維條碼格式
*/
ONE_DIMENSION,
/**
* 所有二維條碼格式
*/
TWO_DIMENSION,
/**
* 僅 QR_CODE
*/
ONLY_QR_CODE,
/**
* 僅 CODE_128
*/
ONLY_CODE_128,
/**
* 僅 EAN_13
*/
ONLY_EAN_13,
/**
* 高頻率格式,包括 QR_CODE、ISBN13、UPC_A、EAN_13、CODE_128
*/
HIGH_FREQUENCY,
Zxing的掃碼就是各種Reader遍歷, 我們將需要的類型區分後分別填入不同的Reader遍歷掃碼。這也是Zxing提供的MultiFormatReader的實現, 這裏是將其更細分了。
具體的細節筆者實在描述不清, 請看github
其實是源於CameraView, 筆者分別基於Camera1 ,Camera2 都做了Zxing掃碼適配, 以及改善了CameraView的一些小問題。 歡迎來踩。