目前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的一些小问题。 欢迎来踩。