在zxing项目中,扫码的第一步就是要去调用 android 的相机服务,打开相机完成初始配置后以待使用。在相关代码中,CameraManager类是整个camera包中的核心类。因此本篇博客重点对该部分代码进行分析,了解摄像头配置过程,为后续分析扫码流程等核心代码做好铺垫。
首先先大体了解一下在Zxing中与Android系统camera服务相关的包和类:
public synchronized void openDriver(SurfaceHolder holder) throws IOException
这个方法的主要功能是打开相机驱动并且初始化硬件参数
//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
//是否已经初始化,没有初始化则进行初始化
if (!initialized) {
initialized = true;
//设置相机初始化参数
configManager.initFromCameraParameters(theCamera);
//设置相机界面矩形框的位置和大小
if (requestedFramingRectWidth > 0 && requestedFramingRectHeight > 0) {
setManualFramingRect(requestedFramingRectWidth, requestedFramingRectHeight);
requestedFramingRectWidth = 0;
requestedFramingRectHeight = 0;
}
}
在这里首先调用了CameraConfigurationManager实例对象的initFromCameraParameters方法,也就是初始化摄像头的参数,下面重点分析一下该方法。
在分析该部分代码之前,需要先对Android系统camera方向进行一下大体了解
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方法,下面详细分析下这部分代码
public static Point findBestPreviewSizeValue(Camera.Parameters parameters, Point screenResolution) {
//首先获取相机参数,获得相机支持的预览图片大小,返回值是一个List数组
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;
}
分析来看,这个方法就是对于通过相机参数所获得的所有支持的预览图片尺寸,进行遍历。排除掉那些与屏幕宽高比相差过大的一些尺寸后,优先选择精确等于屏幕大小的尺寸,其次选择最大尺寸,再其次返回默认尺寸。
这里遗留下来一个问题,对于默认尺寸是未进行任何处理筛选的,相机默认的尺寸可能与屏幕的尺寸比有较大的差距,这样就会出现预览图像变形的问题。这里将作为后续优化的一个方面。
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方法,为相机配置其他相关参数
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作为关键方法进行了详细分析,下面还有一些其他方法,这里进行简要说明,如果在后续扫码流程中用到再进行展开分析:
经过以上的代码分析,对于Android系统中的camera服务有了大致了解,基本上理清了摄像头开启并且配置的相关流程。但是在分析代码中发现,摄像头与预览画面的旋转角度问题是一个难点,这里需要再在实际应用中进行测试。同时在寻找相机最佳预览尺寸上,在没有找到最佳尺寸的情况下,Zxing直接使用了默认尺寸,这就有可能带来图形变形问题,为后续的图像二维码解析带来困难,因此这里也是一个可以进行优化的方面。