一、背景
最近将项目minSdkVersion升级到21以上后,AS lint开始报warning:
'android.hardware.Camera' is deprecated as of API 21: Android 5.0 (Lollipop)
原因是扫码工具中使用的camera API已经废弃,查看Camera类,可以看到javadoc的注解描述写着建议使用新API的camera2。
/**
* @deprecated We recommend using the new {@link android.hardware.camera2} API for new
* applications.
*/
@Deprecated
public class Camera {
}
camera2相比camera,不是简单地对老接口的@param和@return做了调整,也不是替换了个别调用接口,而是完全换了一套调用方式。
二、框架
对项目中的扫码工具进行了camera到camera2的升级重构,解码部分仍然使用zxing库实现,下面是扫码工具的框架图。
鉴于camera2相比camera巨大的差异,与其说在原项目上进行重构,不如抛弃老代码,重新再写一个,下面将一一对框架各个层级的设计进行阐述。
三、UI层设计
1.扫码框View,根据自己的需要去自定义View。
public final class QRFinderView extends View {
private Rect frame;
public QRFinderView(Context context, AttributeSet attrs) {
super(context, attrs);
}
public void setFrame(Rect frame) {
this.frame = frame;
}
public void onDraw(Canvas canvas) {
if (frame == null) {
return;
}
...
this.postInvalidateDelayed(14L, frame.left, frame.top, frame.right, frame.bottom);
}
}
2.为了交互更友好,可以在扫码成功时播放提示音,使用播放器MediaPlayer加载本地voice资源实现。
soundPlayer.setDataSource(context, uri)
soundPlayer.start()
3.闪光灯,与旧版camera通过Camera.setParameters()设置相机参数不同,camera2开启闪光灯使用发起CaptureRequest.FLASH_MODE请求实现。
public void openFlashLight() {
previewRequestBuilder.set(CaptureRequest.FLASH_MODE, CaptureRequest.FLASH_MODE_TORCH);
try {
captureSession.setRepeatingRequest(previewRequestBuilder.build(), sessionCaptureCallback, null);
} catch (CameraAccessException e) {
e.printStackTrace();
}
}
4.预览画面,使用TextureView呈现,
//构建预览请求
previewRequestBuilder = cameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW);
SurfaceTexture texture = textureView.getSurfaceTexture();
texture.setDefaultBufferSize(mPreviewSize.getWidth(), mPreviewSize.getHeight());
Surface surface = new Surface(texture);
//添加显示预览视图
previewRequestBuilder.addTarget(surface);
预览视图需要选择合适的尺寸mPreviewSize,而不是直接使用TextureView的size,而是从相机支持的所有预览尺寸中选择,不超过view寸尺的最大尺寸,作为实际预览尺寸。
private void setUpCameraOutputs(int viewWidth, int viewHeight) {
try {
CameraCharacteristics characteristics = getCameraManager().getCameraCharacteristics(getBackCameraId());
StreamConfigurationMap map = characteristics.get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP);
//当前相机设备支持的预览尺寸
Size[] supportedSizes = map.getOutputSizes(SurfaceTexture.class);
//屏幕方向
int windowRotation = getActivity().getWindowManager().getDefaultDisplay().getRotation();
//相机方向
int cameraOrientation = characteristics.get(CameraCharacteristics.SENSOR_ORIENTATION);
//是否需要根据屏幕方向调整相机旋转方向(当屏幕方向和相机方向不一致时需要)
boolean needSwappedDimensions = false;
switch (windowRotation) {
case Surface.ROTATION_0:
case Surface.ROTATION_180:
if (cameraOrientation == 90 || cameraOrientation == 270) {
needSwappedDimensions = true;
}
break;
case Surface.ROTATION_90:
case Surface.ROTATION_270:
if (cameraOrientation == 0 || cameraOrientation == 180) {
needSwappedDimensions = true;
}
break;
}
int rotatedPreviewWidth = viewWidth;
int rotatedPreviewHeight = viewHeight;
if (needSwappedDimensions) {
rotatedPreviewWidth = viewHeight;
rotatedPreviewHeight = viewWidth;
}
//从相机支持的所有预览尺寸中选择,不超过view寸尺的最大尺寸,作为实际预览尺寸
mPreviewSize = new Size(rotatedPreviewWidth, rotatedPreviewHeight);
float ration = (float) rotatedPreviewWidth / rotatedPreviewHeight;
for (Size option : supportedSizes) {
if ((float) option.getWidth() / option.getHeight() == ration
&& option.getWidth() <= rotatedPreviewWidth
&& option.getHeight() <= rotatedPreviewHeight) {
mPreviewSize = option;
break;
}
}
} catch (CameraAccessException | NullPointerException e) {
e.printStackTrace();
}
}
四、扫码层设计
1.开启相机。与旧版camera直接使用Camera.open()就可以开启不同,开启新版CameraDevice需要先指定cameraId,然后在open时监听相机的状态。
/**
* 开启相机
*/
private void openCamera() {
try {
cameraManager.openCamera(getBackCameraId(), deviceStateCallback, null);
} catch (CameraAccessException e) {
e.printStackTrace();
}
}
/**
* 获取后置相机id
*/
private String getBackCameraId() {
CameraManager cameraManager = getCameraManager();
try {
String[] ids = cameraManager.getCameraIdList();
for (String id : ids) {
CameraCharacteristics characteristics = cameraManager.getCameraCharacteristics(id);
int orientation = characteristics.get(CameraCharacteristics.LENS_FACING);
if (orientation == CameraCharacteristics.LENS_FACING_BACK) {
return id;
}
}
} catch (CameraAccessException e) {
e.printStackTrace();
} return null;
}
//相机状态
private CameraDevice.StateCallback deviceStateCallback = new CameraDevice.StateCallback() {
@Override
public void onOpened(@NonNull CameraDevice camera) {
cameraDevice = camera;
startPreview();
}
@Override
public void onDisconnected(@NonNull CameraDevice camera) {
camera.close();
cameraDevice = null;
}
@Override
public void onError(@NonNull CameraDevice camera, int error) {
}
};
2.开启预览。当监听到CameraDevice onPened()后,便可以开始预览了。在旧版API中,是通过Camera.startPreview()开启预览,并通过PreviewCallback接收预览结果。但CameraDevice没有提供startPreview(),而是将显示预览与捕获预览结果分开成两个独立请求过程。显示预览的请求在前面UI层设计已经说明了。而请求获取预览结果需要先通过createCaptureSession()设置预览监听CameraCaptureSession.StateCallback,并在onConfigured()回调中拿到CameraCaptureSession,
private void startPreview() {
try {
//构建预览请求
previewRequestBuilder = cameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW)
SurfaceTexture texture = textureView.getSurfaceTexture();
texture.setDefaultBufferSize(mPreviewSize.getWidth(), mPreviewSize.getHeight());
Surface surface = new Surface(texture);
//添加显示预览视图
previewRequestBuilder.addTarget(surface);
//发送预览请求(请求结果将输出到ImageReader)
cameraDevice.createCaptureSession(Arrays.asList(surface, imageReader.getSurface()), sessionStateCallback, null);
} catch (CameraAccessException e) {
e.printStackTrace();
}
}
//预览状态
private CameraCaptureSession.StateCallback sessionStateCallback = new CameraCaptureSession.StateCallback() {
@Override
public void onConfigured(@NonNull CameraCaptureSession session) {
captureSession = session;
previewRequestBuilder.set(CaptureRequest.CONTROL_AF_MODE, CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_PICTURE);
previewRequestBuilder.set(CaptureRequest.CONTROL_AE_MODE, CaptureRequest.CONTROL_AE_MODE_ON_AUTO_FLASH);
try {
//一直请求预览
session.setRepeatingRequest(previewRequestBuilder.build(), sessionCaptureCallback, null);
} catch (CameraAccessException e) {
e.printStackTrace();
}
}
}
最后,由CameraCaptureSession发出预览请求。
session.setRepeatingRequest(previewRequestBuilder.build(), sessionCaptureCallback, null);
3.接收预览结果。由于需要展示实时预览画面,得先指定好预览画面的Surface,这个前面UI层设计中已经提到了。关键是接收预览结果,需要事先指定预览接收ImagerReader。
//预览输出到ImageReader中
ImageReader imageReader = ImageReader.newInstance(mPreviewSize.getWidth(), mPreviewSize.getHeight(), ImageFormat.YUV_420_888, /*maxImages*/2);
//监听预览输出
imageReader.setOnImageAvailableListener(imageAvailableListener, null);
并设置好预览捕获结果监听OnImageAvaliableListener,当捕获到预览图片时,便会回调其onImageAvailable()方法,传回Image字节流数据。
//预览图片完成
private ImageReader.OnImageAvailableListener imageAvailableListener = new ImageReader.OnImageAvailableListener() {
@Override
public void onImageAvailable(ImageReader reader)
Image image = reader.acquireLatestImage();
if (image == null) {
return;
}
ByteBuffer buffer = image.getPlanes()[0].getBuffer();
int imageWidth = image.getWidth();
int imageHeight = image.getHeight();
byte[] data = new byte[buffer.remaining()];
buffer.get(data);
image.close();
decodeImageByZxing(data, imageWidth, imageHeight);
}
};
五、解码层设计
zxing是google出品的一款非常流行的解码工具,引入时可以去其官网上查看最新版本号。
implementation 'com.google.zxing:core:3.4.1'
zxing解码的过程:将相机获取的图片数据流byte[],转为对应的PlanarYUVLuminanceSource数据,然后解析source获取Result,最后根据Result判断是否扫码成功。
1.构建PlanarYUVLuminanceSource数据。CameraDevice会通过ImagerReader输出一个图片数据流byte[],结合指定的输出预览区域边界,可以构建出zxing解码所需要的PlanarYUVLuminanceSource数据。
private void decodeImageByZxing(byte[] imageData, int imageWidth, int imageHeight) {
Rect rect = new Rect(mFramingRectInPreview);
PlanarYUVLuminanceSource planarYUVLuminanceSource = new PlanarYUVLuminanceSource(imageData, imageWidth, imageHeight, rect.left, rect.top, rect.width(), rect.height(), false);
}
2.指定解码数据格式。格式类型从BarcodeFormat枚举中选定,这里要识别二维码,就需要添加BarcodeFormat.QR_CODE格式。
private MultiFormatReader getMultiFormatReader() {
MultiFormatReader mMultiFormatReader = new MultiFormatReader();
Collection<BarcodeFormat> decodeFormats = EnumSet.noneOf(BarcodeFormat.class);
//指定扫码数据类型
decodeFormats.add(BarcodeFormat.QR_CODE);
decodeFormats.add(BarcodeFormat.DATA_MATRIX);
final Map<DecodeHintType, Object> hints = new EnumMap<>(DecodeHintType.class);
hints.put(DecodeHintType.POSSIBLE_FORMATS, decodeFormats);
hints.put(DecodeHintType.CHARACTER_SET, "UTF8");
hints.put(DecodeHintType.NEED_RESULT_POINT_CALLBACK, new QRFinderResultPointCallback(finderView));
mMultiFormatReader.setHints(hints);
return mMultiFormatReader;
}
3.执行解码。先将PlanarYUVLuminanceSource包装成BinaryBitmap,使用指定好数据格式的MultiFormatReader进行decode,解码结果通过Result对象输出。
private Result decode(PlanarYUVLuminanceSource planarYUVLuminanceSource) {
MultiFormatReader multiFormatReader = getMultiFormatReader();
Result result = null;
if (planarYUVLuminanceSource != null) {
BinaryBitmap bitmap = new BinaryBitmap(new HybridBinarizer(planarYUVLuminanceSource));
try {
result = multiFormatReader.decodeWithState(bitmap);
} catch (ReaderException re) {
Log.e(TAG, ": ", re);
} finally {
multiFormatReader.reset();
}
}
return result;
}
4.解析解码Result。扫码采用一直请求预览的方式,每一次预览捕获到Image数据后,都会传入zxing进行解码,并停止预览等待解码结果。当未扫描到二维码时,解码输出的Result为null,这时就需要继续发出预览请求,直到扫描到有效二维码后,得到的解码Result不为null,成功识别出二维码信息。
private void handleScanResult(Result result) {
if (result != null) {
//扫码成功
if (qrScanResultCallback != null) {
qrScanResultCallback.onResult(result.getText());
}
} else {
//扫码不成功,重新再来
try {
captureSession.setRepeatingRequest(previewRequestBuilder.build(), sessionCaptureCallback, null);
} catch (CameraAccessException e) {
e.printStackTrace();
}
}
}
六、集成QRScanSdk
在AndroidStudio中集成:
Step 1. Add it in your root build.gradle at the end of repositories:
allprojects {
repositories {
...
maven { url 'https://jitpack.io' }
}
}
Step 2. Add the dependency
dependencies {
implementation 'com.github.zouhecan:QRScanSdk:2.1'
}
Step 3. Using QRScanSdk in your activity
class MainActivity : AppCompatActivity(), QRScanResultCallback {
fun startScan() {
QRScanManager.startScan(this, this)
}
override fun onResult(result: String?) {
//do something when the scan is completed
}
}