我们要自定义一个相机的基本操作如下:
在合适的时间(如onStart())打开相机和预览:
- 打开相机(
Camera#open()
) - 配置相机参数(
Camera#Parameters
) - 配置预览方向(
Camera#setDisplayOrientation()
):保证相机在界面上显示正确 - 设置预览界面(
Camera#setPreviewDisplay(SurfaceView)
) - 启动预览(
Camera#startPreview()
)
然后在合适的时间(如onStop())停止相机预览:
- 停止预览(
Camera#stopPreview()
) - 关闭相机(
Camera#release()
)
基本的操作是相对简单的,但是我们要考虑的细节是很多的,比如各种参数配置,方向,尺寸比例等.这个我们后面都会提到.
相机打开和关闭
1. 打开相机
//先定义一个枚举,表示我们要打开的相机(不用系统自带的int常量值也是为了方便用户调用和通用)
public enum CameraFacing {
BACK, //后置
FRONT,//前置
;
}
//然后从相机列表中找到我们需要的相机
@Override
public CameraV open(CameraFacing type) {
this.mCameraFacing = type;
int numberOfCameras = Camera.getNumberOfCameras();
WeCameraLogger.d(TAG, "open camera:number of cameras=%d", numberOfCameras);
if (numberOfCameras <= 0) {
CameraErrors.throwError(CameraException.ofDevice("no camera can use:numberOfCameras is 0"));
return null;
}
Camera.CameraInfo cameraInfo = new Camera.CameraInfo();
//如果只有1个摄像头,则直接使用它.比如Oppo N1这种翻转摄像头的
if (numberOfCameras == 1) {
Camera.getCameraInfo(0, cameraInfo);
//这里要更正一下相机方向,否则会影响后面对角度的计算
this.mCameraFacing = getFacingType(cameraInfo.facing);
return openCamera(cameraInfo, 0);
}
for (int i = 0; i < numberOfCameras; i++) {
Camera.getCameraInfo(i, cameraInfo);
if (isTargetV1Type(type, cameraInfo.facing)) {
WeCameraLogger.i(TAG, "camera open:find dest camera:face=%s,camera id=%d", type.toString(), i);
return openCamera(cameraInfo, i);
}
}
return null;
}
private CameraV openCamera(Camera.CameraInfo cameraInfo, int i) {
this.mCamera = Camera.open(i);
this.mCameraInfo = cameraInfo;
this.mCameraId = i;
return cameraV();
}
//判断是否是指定相机
public static boolean isTargetV1Type(CameraFacing type, int facing) {
if (facing == Camera.CameraInfo.CAMERA_FACING_BACK && type == BACK) {
return true;
}
if (facing == Camera.CameraInfo.CAMERA_FACING_FRONT && type == FRONT) {
return true;
}
return false;
}
//返回一个封装的相机对象,存储了该相机的基本信息,这些信息后面还用用到,就不用再次取了
public CameraV1 cameraV() {
return new CameraV1()
.camera(mCamera)
.orientation(mCameraInfo.orientation)
.cameraInfo(mCameraInfo)
.cameraFacing(mCameraFacing)
.cameraId(mCameraId);
}
private CameraFacing getFacingType(int facing) {
if (facing == Camera.CameraInfo.CAMERA_FACING_BACK) {
return BACK;
} else if (facing == Camera.CameraInfo.CAMERA_FACING_FRONT) {
return FRONT;
}
return BACK;
}
2. 关闭相机
//关闭的操作很简单
@Override
public void close() {
if (mCamera != null) {
WeCameraLogger.d(TAG, "close camera:" + mCamera);
//释放相机资源
mCamera.release();
mCameraInfo = null;
mCamera = null;
}
}
配置相机
主要涉及的是Camera#Parameters
这个类,这个类中可以配置很多参数,但是具体要配置什么值呢? 这个可不是随便写的.需要当前相机支持才行,所以这个类也有很多类似getSupportedXXX()
的方法来表示当前相机支持哪些参数.你需要且只能从支持的参数列表中挑选一个.
所以配置相机的基本步骤如下(假如我们需要配置闪光灯模式):
- 拿到相机当前的
Parameters
对象:Camera.Parameters parameters = camera.getParameters()
- 拿到支持的闪光灯模式列表:
List<String> supportedFlashModes = parameters.getSupportedFlashModes()
; - 选择一个你想支持的.
- 设置到Parameters中去:
parameters.setFlashMode(flashMode);
- 将parameters设置回相机:
camera.setParameters(parameters);
每个参数的配置都要遵循上面的步骤.当然,我们也不是每个参数都重头来一遍,可以批量配置Parameters然后一次性设置回Camera
(也就是重复上面的2,3,4进行配置).
另外,最好给这些操作外面套一个try-catch
.说不定哪里就出错了.然后在catch的时候可以回滚.怎么操作呢? 其实通过Camera#getParameters()
拿到的是相机本身配置的一个副本,也就是你修改他并不能立马对相机生效,所以我们最后第5步才需要将他设置回相机去. 而且每次调用这个方法都会拿到一个新的副本,所以我们可以调用两次这个方法,对第二次调用拿到的Parameters
进行配置,如果出错了,将第一个副本设置回相机中去,这样就是多了一重保险了,不过这样,我们的配置就无法生效了.这种情况,麻烦把错误日志上传到后台进行原因排查.
我们常用的配置有:
- 预览大小(这个是必须配置的,否则预览可能会变形)
- 图片大小(假如需要使用原生拍照功能的话,Camera#takePicture())
- flashMode(闪光灯模式)
- focusMode(对焦模式)
- fps(帧率)
这些配置都是遵循前面的配置步骤,我们着重介绍第一个,因为这个对于自定义相机来说是最重要的(保证预览显示的效果).当然步骤就不说了.就是上面的步骤,主要说一下选择哪个预览尺寸的问题.当然这个有时候看的是实际需要.
比如我们的预览界面为是手机竖屏全屏.这种情况下我们的策略可能是选择一个和手机界面尺寸最接近的一个,这样显示效果最佳,最好宽高比是一致的,这样也不怕有黑边或者碰到预览被拉伸或者压缩的问题(当然,这种情况是可以处理掉的). 而且大部分相机都会有一个和手机屏幕尺寸一致的预览尺寸.(可惜是大部分,
对于少部分我们还是得处理)
我们可以定义一个特征选择接口:
public interface FeatureSelector<T> {
/**
* @param candidates 主备选列表
* @param cameraV 相机其他特性
* @return 选中的特性
*/
T select(List<T> candidates, CameraV cameraV);
}
这里的candidates
就是我们通过getSupportedXXX()
拿到的列表, CameraV是我们对相机的一个封装,里面有一些相机的属性,不需要再次获取,如相机朝向,相机ID等. 返回的结果只有一个,也就是我们最终选中的值. 实现这个接口也符合前面说的相机配置的步骤(从支持的列表中选择一个进行配置). 这个其实是设计模式中的策略模式. 这样,你在配置的时候其实配置的是策略.
例如,在选择预览尺寸时,我们可以选择的策略如下:
1. 先将预览尺寸按照面积从大到小排序(我们总是希望预览尺寸大一点好)
2. 寻找宽高比和我们需要的宽高比一致的.
3. 寻找高度或者宽度和我们需要的高度或宽度差别最小的.
4. 另外,可以排除一些预览尺寸过小的
5. 排除一些宽高比差异过大的.
6. 如果都无法找到,我们就需要决策, 我们就找一个尺寸和我们需要的尺寸相近的,至于宽高比导致的问题还是可以解决的么?要么留白要么就裁切掉一部分.
这里需要注意两点:
(1) 第一个是我们有一个角度问题需要考虑, 那就是相机角度和屏幕角度. 默认getSupportedXXX()
拿到的相机尺寸(宽高)是以相机的方向来确定的,屏幕尺寸是以屏幕方向来确定的. 这两个方向有可能是不一致的. 这样,你找到的预览尺寸也会有问题. 所以我们先把这个因素给去掉. 因为我们下一步会旋转相机的预览角度到和屏幕一致.所以,这里选择尺寸的时候也一定要保证他们是在一直的方向上进行选择的.
假如屏幕是竖屏(0度), 我们需要的预览尺寸是(1080*1920), 后置相机一般的角度是90度, 他支持的预览尺寸有(1920*1080,1280*720,640*480).角度不一致,我们这里只需要判断两个角度模除180的结果不一样即可(不一样说明他们一个是横向的,一个是竖向的).如果不一样,那把需要的预览尺寸的宽高互换为(1920*1080). 然后再进行选择, 这样才能找到我们真正需要的预览尺寸.
如果一个是0度,一个是180度(模除180都是0),类似这种情况,他们只是做了一个转向,不影响尺寸的选择)
关于各种涉及到角度的问题,参考我另外一篇博文: Android 相机(一):摄像头属性,预览方向,预览尺寸等问题探究
(2) 第二个是如果选择的预览尺寸和我们实际的界面尺寸宽高比不一致,必然会发生拉伸或者压缩.这种情况我们后面再说)
设置预览旋转角度
这一步就是调用Camera#setDisplayOrientation(int)
,因为我们的相机角度和屏幕方向是不一致的,直接显示出来可能是颠倒的或者左边朝上或者右边朝上的,没法正确的预览,我们要把相机的预览角度旋转到我们当前屏幕的方向上.
这个角度到底是多少. 其实源代码的注释里面已经给了一段代码,类似下面这样的:
/**
* 设置相机角度为屏幕一致的角度
*
* @param cameraOrientation 相机角度,0,90,180,270
* @param facing 相机朝向
* @param screenOrientation 屏幕角度,0,90,180,270
* @return 相机预览需要旋转的角度
*/
public static int getCorrectRotation(CameraFacing facing, int screenOrientation, int cameraOrientation) {
int result;
if (facing == CameraFacing.FRONT) {
result = (cameraOrientation + screenOrientation) % 360;
result = (360 - result) % 360; // compensate the mirror
} else { // back-facing
result = (cameraOrientation - screenOrientation + 360) % 360;
}
return result;
}
经过上面的设置,可以保证预览方向一定是正确的了.
设置预览界面
主要就是下面一行代码就OK了. surfaceView是SurfaceView的实例,他是View的子类,他持有一个SurfaceHolder,这里设置给Camera的就是他.
camera.setPreviewDisplay(surfaceView.getHolder())
这里就要考虑到前面说的预览画面可能变形的问题了. 我们可以通过下面的方式解决: 给SurfaceView外层套一个FrameLayout,SurfaceView是他的子View.这样,我们的显示大小由FrameLayout决定,然后我们来决定SurfaceView应该显示为多大. 这里只有一个原则,就是他的宽高比要和前面选择设置的预览尺寸一直. 这样,预览就不会变形. 所以, 我们就有一些情况要处理. 只要SurfaceView的宽高比和FrameLayout的不一致,我们就要处理SurfaceView如何布局的问题.
有两种方法:
1). FIT: 保证SurfaceView宽高都小于FrameLayout的宽高(并且至少宽高有一个刚好等于父View的宽高),这样,就会有留白:左右有留白则居中;上下如果有留白可以选择SurfaceView靠顶端对齐,或者居中,或者靠低端对齐,这三种情况视具体业务和产品形态选择.
FIT_CENTER:左右两侧有留白
2). CROP: 保证SurfaceView宽高都大于等于FrameLayout(并且宽和高中至少有一个刚好等于父View的宽高),这样,必然有一部分SurfaceView会看不到(被父类大小限制而被裁切): 如果左右被裁切则居中;上下被裁切则可以选择全部裁切上面的部分,或者上下各被裁切一半,或者全部裁切下面的部分.
这里说的上下都是相当于当前屏幕方向来说的.
下面两个图展示了两种宽高比不一致时的情况(红色框为预览界面的大小,也就是FrameLayout的大小,黑色框为SurfaceView设置的大小).
CROP_CENTER: 左右两侧被裁切.
经过上面的设置,可以保证,无论预览界面设置为多大.都不会导致预览变形了.
这里还需要注意一个问题.在调用这个方法时,我们必须保证SurfaceView持有的SurfaceHolder中的Surface已经创建完成,这是通过SurfaceHolder的Callback实现的.
mSurfaceView.getHolder().addCallback(new SurfaceHolder.Callback() {
@Override
public void surfaceCreated(SurfaceHolder holder) {
//Surface创建完毕
mSurfaceHolder = holder;
//调用countDown,此时CountDownLatch的值变为0,所有被await()方法阻塞的线程继续开始运行.
这里的CountDownLatch看下面的说明
mCountDownLatch.countDown();
}
@Override
public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
}
@Override
public void surfaceDestroyed(SurfaceHolder holder) {
}
});
因为我们所有的相机操作都是在相机线程(非主线程)中进行的,所以,这里我们通过CountDownLatch
这个类把这个操作转换为同步操作.也就是在SurfaceView的初始化时(这个是在主线程中执行的)创建一个count为1的CountDownLatch:
CountDownLatch mCountDownLatch = new CountDownLatch(1);
并且添加回调:如上上一段代码展示的那样.
最后,在调用startViewPreview()之前加上:
if (mSurfaceHolder == null) {
try {
WeCameraLogger.d(TAG, "attachCameraView:wait for surface create");
mCountDownLatch.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
这样,在没有到达surfaceCreated()
的时候,这个线程会一直阻塞,也就保证了我们调setPreviewDisplay()
的时候,Surface肯定是创建好了的.
启动预览和停止预览
1. 启动预览
设置完预览界面就可以启动预览了.
camera.startPreview();
2. 停止预览
停止预览对应的:
camera.stopPreview();
至此,我们的自定义相机就算完成了. 因为他有了最基本的功能:能够正确的进行相机的预览了. 后面我们说说在自定义预览中拍照的事情.