文章目录
前言
在zxing项目中,扫码的第一步就是要去调用 android 的相机服务,打开相机完成初始配置后以待使用。在相关代码中,CameraManager类是整个camera包中的核心类。因此本篇博客重点对该部分代码进行分析,了解摄像头配置过程,为后续分析扫码流程等核心代码做好铺垫。
一、camera包
首先先大体了解一下在Zxing中与Android系统camera服务相关的包和类:
- CameraManager:该类封装了相机的所有服务,是camera包中的核心类
- CameraConfigurationManager:摄像头参数的设置类(具体模式等参数配置在CameraConfigurationUtils类中,该类只是调用)
- CameraConfigurationUtils:摄像头具体配置类,是为CameraConfigurationManager服务的工具类
- AutoFocusManager:自动对焦管理类,由于对焦不是一次性完成的任务(手抖),而系统提供的对焦仅有Camera.autoFocus()方法, 因此需要一个线程来不断调用Camera.autoFocus()直到用户满意按下快门为止
- FrontLightMode:闪光灯枚举类(开,关,自动)
- PreviewCallback:该类的作用是在预览界面加载好后向ui线程发消息
- open包:里面是打开摄像头的接口类
二、CameraManager代码分析
打开相机驱动方法 openDriver
public synchronized void openDriver(SurfaceHolder holder) throws IOException
这个方法的主要功能是打开相机驱动并且初始化硬件参数
- 这里加上了synchronized,保证同步性,即一次只能有一个线程进入该方法,其他线程要想在此时调用该方法,只能排队等候。
- SurfaceHolder是一个接口,类似于一个surface的监听器
1. 获取手机的摄像头
//OpenCamera是open包中的一个类,里面有一个Camera类作为属性
OpenCamera theCamera = camera;
if (theCamera == null) {
//直接调用打开相机的接口,其中requestedCameraId标识当前要打开的camera
theCamera = OpenCameraInterface.open(requestedCameraId);
if (theCamera == null) {
throw new IOException("Camera.open() failed to return object from driver");
}
camera = theCamera;
}
设备上每一个物理摄像都是有一个id的,id从0开始,到getNumberOfCameras() - 1 结束;比如一般的手机上都有前后两个摄像头,那么后置摄像头id就是0,前置摄像头id就是1
2. 相机参数初始化
//是否已经初始化,没有初始化则进行初始化
if (!initialized) {
initialized = true;
//设置相机初始化参数
configManager.initFromCameraParameters(theCamera);
//设置相机界面矩形框的位置和大小
if (requestedFramingRectWidth > 0 && requestedFramingRectHeight > 0) {
setManualFramingRect(requestedFramingRectWidth, requestedFramingRectHeight);
requestedFramingRectWidth = 0;
requestedFramingRectHeight = 0;
}
}
在这里首先调用了CameraConfigurationManager实例对象的initFromCameraParameters方法,也就是初始化摄像头的参数,下面重点分析一下该方法。
a. 初始化摄像头参数 initFromCameraParameters
在分析该部分代码之前,需要先对Android系统camera方向进行一下大体了解
- 自然方向:每个设备都有一个自然方向,手机和平板的自然方向不同。手机的自然方向是portrait(竖屏),平板的自然方向是landscape(横屏)
- 摄像头方向:相机的图像数据都是来自于硬件的图像传感器,摄像头的方向取决于图像传感器的安装方向。绝大部分安卓手机中图像传感器方向是横向的,且不能改变,所以orientation是90或是270,也就是说,当点击拍照后保存图片的时候,需要对图片做旋转处理,使其为"自然方向"。
- 相机预览方向:Android 系统提供一个 API 来手动设置 Camera 的预览方向,叫 setDisplayOrientation。默认情况下这个值是0,与图像传感器方向一致,所以对于横屏应用来说就不需要更改这个 Camera 预览方向。但是,如果应用是竖屏应用,就必须通过这个 API 将 Camera 的预览方向旋转 90 度,让摄像头预览方向与手机屏幕方向保持一致,这样才会得到正确的预览画面。同时前置摄像头在进行角度旋转之前,图像会进行一个水平的镜像翻转,所以用户在看预览图像的时候就像照镜子一样。
下面分析initFromCameraParameters方法中处理图像方向的代码
void initFromCameraParameters(OpenCamera camera) {
Camera.Parameters parameters = camera.getCamera().getParameters();
WindowManager manager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
Display display = manager.getDefaultDisplay();
//获取相机预览方向
int displayRotation = display.getRotation();
//这个角度值是相机预览图片需要顺时针旋转至自然方向的角度值
int cwRotationFromNaturalToDisplay;
switch (displayRotation) {
case Surface.ROTATION_0:
cwRotationFromNaturalToDisplay = 0;
break;
case Surface.ROTATION_90:
cwRotationFromNaturalToDisplay = 90;
break;
case Surface.ROTATION_180:
cwRotationFromNaturalToDisplay = 180;
break;
case Surface.ROTATION_270:
cwRotationFromNaturalToDisplay = 270;
break;
default:
// 特殊情况下,可能返回值是负数如-90,需要进行下处理
if (displayRotation % 90 == 0) {
cwRotationFromNaturalToDisplay = (360 + displayRotation) % 360;
} else { //其他值报错
throw new IllegalArgumentException("Bad rotation: " + displayRotation);
}
}
Log.i(TAG, "Display at: " + cwRotationFromNaturalToDisplay);
//这个角度值是相机所采集的图片需要顺时针旋转至自然方向的角度值
int cwRotationFromNaturalToCamera = camera.getOrientation();
Log.i(TAG, "Camera at: " + cwRotationFromNaturalToCamera);
// 使用前置摄像头时需要进行镜像翻转
if (camera.getFacing() == CameraFacing.FRONT) {
cwRotationFromNaturalToCamera = (360 - cwRotationFromNaturalToCamera) % 360;
Log.i(TAG, "Front camera overriden to: " + cwRotationFromNaturalToCamera);
}
//计算最终需要调整的角度
cwRotationFromDisplayToCamera =
(360 + cwRotationFromNaturalToCamera - cwRotationFromNaturalToDisplay) % 360;
Log.i(TAG, "Final display orientation: " + cwRotationFromDisplayToCamera);
// 使用前置摄像头时需要进行镜像翻转
if (camera.getFacing() == CameraFacing.FRONT) {
Log.i(TAG, "Compensating rotation for front camera");
cwNeededRotation = (360 - cwRotationFromDisplayToCamera) % 360;
} else {
cwNeededRotation = cwRotationFromDisplayToCamera;
}
Log.i(TAG, "Clockwise rotation from display to camera: " + cwNeededRotation);
以上是Zxing在设置预览方向的代码,但是只设置预览方向还是不够的,还要根据屏幕的宽高比来找到相机采集图片最合适的预览尺寸,否则就会出现相机预览图拉伸变形的问题
//获取屏幕分辨率,从这个变量中可以分别获取屏幕宽高的像素值
Point theScreenResolution = new Point();
display.getSize(theScreenResolution);
screenResolution = theScreenResolution;
Log.i(TAG, "Screen resolution in current orientation: " + screenResolution);
//获取相机的最佳分辨率
cameraResolution = CameraConfigurationUtils.findBestPreviewSizeValue(parameters, screenResolution);
Log.i(TAG, "Camera resolution: " + cameraResolution);
//获取相机的最佳预览尺寸
bestPreviewSize = CameraConfigurationUtils.findBestPreviewSizeValue(parameters, screenResolution);
Log.i(TAG, "Best available preview size: " + bestPreviewSize);
boolean isScreenPortrait = screenResolution.x < screenResolution.y;
boolean isPreviewSizePortrait = bestPreviewSize.x < bestPreviewSize.y;
if (isScreenPortrait == isPreviewSizePortrait) {
previewSizeOnScreen = bestPreviewSize;
} else {
previewSizeOnScreen = new Point(bestPreviewSize.y, bestPreviewSize.x);
}
Log.i(TAG, "Preview size on screen: " + previewSizeOnScreen);
在上述代码中,最为重要部分的就是获取相机的最佳分辨率(预览尺寸)了,这里调用了CameraConfigurationUtils 类中 findBestPreviewSizeValue方法,下面详细分析下这部分代码
b. 获取相机最佳分辨率 findBestPreviewSizeValue
public static Point findBestPreviewSizeValue(Camera.Parameters parameters, Point screenResolution) {
//首先获取相机参数,获得相机支持的预览图片大小,返回值是一个List<Size>数组
List<Camera.Size> rawSupportedSizes = parameters.getSupportedPreviewSizes();
if (rawSupportedSizes == null) {
Log.w(TAG, "Device returned no supported preview sizes; using default");
// 如果未获取到相机支持的预览图片大小,直接设置默认值
Camera.Size defaultSize = parameters.getPreviewSize();
if (defaultSize == null) {
throw new IllegalStateException("Parameters contained no preview size!");
}
//返回默认的宽高
return new Point(defaultSize.width, defaultSize.height);
}
if (Log.isLoggable(TAG, Log.INFO)) {
StringBuilder previewSizesString = new StringBuilder();
for (Camera.Size size : rawSupportedSizes) {
previewSizesString.append(size.width).append('x').append(size.height).append(' ');
}
Log.i(TAG, "Supported preview sizes: " + previewSizesString);
}
//计算屏幕宽高比
double screenAspectRatio = screenResolution.x / (double) screenResolution.y;
// 找的合适的size以及最大分辨率
int maxResolution = 0;
Camera.Size maxResPreviewSize = null;
for (Camera.Size size : rawSupportedSizes) {
int realWidth = size.width;
int realHeight = size.height;
int resolution = realWidth * realHeight;
if (resolution < MIN_PREVIEW_PIXELS) {
continue;
}
//判断size是竖向还是横向
boolean isCandidatePortrait = realWidth < realHeight;
int maybeFlippedWidth = isCandidatePortrait ? realHeight : realWidth;
int maybeFlippedHeight = isCandidatePortrait ? realWidth : realHeight;
//根据宽高比值差异(当前size分辨率和屏幕分辨率的差异)进行淘汰,差异大于MAX_ASPECT_DISTORTION,这个值就会从列表中删除
double aspectRatio = maybeFlippedWidth / (double) maybeFlippedHeight;
double distortion = Math.abs(aspectRatio - screenAspectRatio);
if (distortion > MAX_ASPECT_DISTORTION) {
continue;
}
//当前的尺寸与屏幕大小相等,则作为最优尺寸返回
if (maybeFlippedWidth == screenResolution.x && maybeFlippedHeight == screenResolution.y) {
Point exactPoint = new Point(realWidth, realHeight);
Log.i(TAG, "Found preview size exactly matching screen size: " + exactPoint);
return exactPoint;
}
// 遍历中记录下最大的分辨率
if (resolution > maxResolution) {
maxResolution = resolution;
maxResPreviewSize = size;
}
}
//如果没有找到精确等于屏幕大小的尺寸,则选择最大的预览尺寸
if (maxResPreviewSize != null) {
Point largestSize = new Point(maxResPreviewSize.width, maxResPreviewSize.height);
Log.i(TAG, "Using largest suitable preview size: " + largestSize);
return largestSize;
}
// 如果没有找到精确尺寸和最大尺寸,则返回默认尺寸
Camera.Size defaultPreview = parameters.getPreviewSize();
if (defaultPreview == null) {
throw new IllegalStateException("Parameters contained no preview size!");
}
Point defaultSize = new Point(defaultPreview.width, defaultPreview.height);
Log.i(TAG, "No suitable preview sizes, using default: " + defaultSize);
return defaultSize;
}
分析来看,这个方法就是对于通过相机参数所获得的所有支持的预览图片尺寸,进行遍历。排除掉那些与屏幕宽高比相差过大的一些尺寸后,优先选择精确等于屏幕大小的尺寸,其次选择最大尺寸,再其次返回默认尺寸。
这里遗留下来一个问题,对于默认尺寸是未进行任何处理筛选的,相机默认的尺寸可能与屏幕的尺寸比有较大的差距,这样就会出现预览图像变形的问题。这里将作为后续优化的一个方面。
3. 相机参数配置
Camera cameraObject = theCamera.getCamera();
Camera.Parameters parameters = cameraObject.getParameters();
String parametersFlattened = parameters == null ? null : parameters.flatten(); // flatten()是android.hardware.camera中的一个方法,把相机的所有参数都放到一个字符串里
try {
//设置相机模式等配置参数
configManager.setDesiredCameraParameters(theCamera, false);
} catch (RuntimeException re) {
// Driver failed
Log.w(TAG, "Camera rejected parameters. Setting only minimal safe-mode parameters");
Log.i(TAG, "Resetting to saved camera params: " + parametersFlattened);
// Reset:
if (parametersFlattened != null) {
parameters = cameraObject.getParameters();
parameters.unflatten(parametersFlattened);
try {
cameraObject.setParameters(parameters);
configManager.setDesiredCameraParameters(theCamera, true);
} catch (RuntimeException re2) {
// Well, darn. Give up
Log.w(TAG, "Camera rejected even safe-mode parameters! No configuration");
}
}
}
//设置一个Surface对象用来实时预览
cameraObject.setPreviewDisplay(holder);
在这里调用了CameraConfigurationManager实例对象的setDesiredCameraParameters方法,为相机配置其他相关参数
相机参数配置 setDesiredCameraParameters
void setDesiredCameraParameters(OpenCamera camera, boolean safeMode) {
//获取设备的参数
Camera theCamera = camera.getCamera();
Camera.Parameters parameters = theCamera.getParameters();
if (parameters == null) {
Log.w(TAG, "Device error: no camera parameters are available. Proceeding without configuration.");
return;
}
Log.i(TAG, "Initial camera parameters: " + parameters.flatten());
//判断是否处于安全模式
if (safeMode) {
Log.w(TAG, "In camera config safe mode -- most settings will not be honored");
}
//SharedPreferences是一个轻量级的存储类,特别适合用于保存软件配置参数
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
//初始化闪光灯
initializeTorch(parameters, prefs, safeMode);
//设置聚焦
CameraConfigurationUtils.setFocus(
parameters,
//是否自聚焦(当光线较暗时自动打开闪光灯)
prefs.getBoolean(PreferencesActivity.KEY_AUTO_FOCUS, true),
//是否持续聚焦
prefs.getBoolean(PreferencesActivity.KEY_DISABLE_CONTINUOUS_FOCUS, false),
safeMode);
if (!safeMode) {
//是否反置颜色
if (prefs.getBoolean(PreferencesActivity.KEY_INVERT_SCAN, false)) {
CameraConfigurationUtils.setInvertColor(parameters);
}
//是否设置条形码场景
if (!prefs.getBoolean(PreferencesActivity.KEY_DISABLE_BARCODE_SCENE_MODE, true)) {
CameraConfigurationUtils.setBarcodeSceneMode(parameters);
}
if (!prefs.getBoolean(PreferencesActivity.KEY_DISABLE_METERING, true)) {
//设置视频稳定模式
CameraConfigurationUtils.setVideoStabilization(parameters);
//设置焦点区域
CameraConfigurationUtils.setFocusArea(parameters);
//设置自动白平衡和自动曝光补偿
CameraConfigurationUtils.setMetering(parameters);
}
parameters.setRecordingHint(true);
}
//设置相机预览尺寸
parameters.setPreviewSize(bestPreviewSize.x, bestPreviewSize.y);
//为相机配置参数
theCamera.setParameters(parameters);
//将捕获的画面旋转cwRotationFromDisplayToCamera角度显示
theCamera.setDisplayOrientation(cwRotationFromDisplayToCamera);
//获取相机预览尺寸
Camera.Parameters afterParameters = theCamera.getParameters();
Camera.Size afterSize = afterParameters.getPreviewSize();
if (afterSize != null && (bestPreviewSize.x != afterSize.width || bestPreviewSize.y != afterSize.height)) {
Log.w(TAG, "Camera said it supported preview size " + bestPreviewSize.x + 'x' + bestPreviewSize.y +
", but after setting it, preview size is " + afterSize.width + 'x' + afterSize.height);
bestPreviewSize.x = afterSize.width;
bestPreviewSize.y = afterSize.height;
}
}
这部分代码中,使用了许多CameraConfigurationUtils类中参数设置的方法,这也是前面讲其为CameraConfigurationManager的工具类的原因。而在CameraConfigurationUtils类中也是调用android.hardware.Camera.Parameters中的服务进行摄像头参数配置,层级调用,界限分明。
CameraManager类中以openDriver作为关键方法进行了详细分析,下面还有一些其他方法,这里进行简要说明,如果在后续扫码流程中用到再进行展开分析:
其他方法
- 关闭相机驱动 closeDriver:这里需要调用camera.getCamera().release()方法释放摄像头资源
- 开始预览 startPreview:使相机硬件在屏幕上绘制预览界面
- 结束预览 stopPreview:停止绘制预览界面
- 设置闪光灯 setTorch
- 返回相机预览界面中的一帧 requestPreviewFrame
- 获取相机预览界面的矩形框 getFramingRectInPreview
- 设置相机预览界面矩形框的位置和大小 setManualFramingRect
- 构造基于平面的YUV亮度源 buildLuminanceSource
总结
经过以上的代码分析,对于Android系统中的camera服务有了大致了解,基本上理清了摄像头开启并且配置的相关流程。但是在分析代码中发现,摄像头与预览画面的旋转角度问题是一个难点,这里需要再在实际应用中进行测试。同时在寻找相机最佳预览尺寸上,在没有找到最佳尺寸的情况下,Zxing直接使用了默认尺寸,这就有可能带来图形变形问题,为后续的图像二维码解析带来困难,因此这里也是一个可以进行优化的方面。