本篇主要就几个关键的类进行解释,并且对需要注意的点注释,此外再总结一下如何使用Camera2进行拍照和预览的流程。附上官方demo。
上面是Camera2的流程示意图,由于我喜欢从整体思路上分析代码,所以下面先就整个呆萌的思路拓展一下。
首先肯定是解决相机的问题啦,毕竟是主角嘛,但是相机是底层的,Android为我们抽象了CameraDevice
类,用来表示各个相机。我们又知道,设备的相机通常有好几个,前后的特点都不一样,比如镜像问题,单单一个CameraDevice
无法表示全。这时候可能有人会想把CameraDevice
做成抽象类,每个相机作为子类各自配置自己的属性,反正打开相机的操作不依赖相机本身,只需要相机的标识就行,打开相机以后再将该相机的实例对象返回给调用者就ok了,没错,呆萌也是这样想的:
@Override
public void onOpened(@NonNull CameraDevice cameraDevice) {
mCameraDevice = cameraDevice;
}
相机设备的问题解决了,但是谁来拍照啊,打开相机不可能让CameraDevice
自己打开自己吧,所以需要一个CameraManager
来管理相机的开启,相机是系统级别的,所以返回的Manager
都是一样的也无妨,就将其上升到服务的层次,只要有上下文就能获取,通过下面的方法获取并进行拍照。
CameraManager manager = (CameraManager) activity.getSystemService(Context.CAMERA_SERVICE);
manager.openCamera(mCameraId, mStateCallback, mBackgroundHandler);
呆萌又想,如果都交给CameraManager
来管理,它还是要管理好多东西哦,比如相机的取景方向,这是个大问题,见解析(1),为了满足单一职责原则和开闭原则,不如将相机设备的属性封装成一个类,这样CameraDevice
只需要专注于创建对话Session
就行了, 于是CameraCharacteristics
应运而生,其封装了指定ID的相机中的各种属性,例如想要获取指定ID相机的取景方向:
CameraCharacteristics characteristics = manager.getCameraCharacteristics(cameraId);
mSensorOrientation = characteristics.get(CameraCharacteristics.SENSOR_ORIENTATION);
我们知道,相机设备是嵌入在Android设备当中的,两者之间的通信需要底层的知识,但是我们不需要知道,所以Android为我们提供了CameraCaptureSession
这个上层类,翻译为相机捕捉的对话,显然这个对话为我们预览和拍摄做了封装,其作用相当于图中的pipeline
。
打开相机后就可以通过管道进行配置了,而相机的各种配置很多,用户的需求也是不一样的,如果在对话管道CameraCaptureSession
中封装了对相机的配置,显然很臃肿,所以此处抽象出来一个CaptureRequest
类,并且对其的配置使用了Builder模式,这个模式适合的场景就是Director
主动去配置对象的属性,提供了默认配置,而不需要在构造函数中将属性一次性配置完成。建议如果代码中需要配置较多的属性,并且这些属性都有默认值的话,使用Builder模式更佳。呆萌是这样体现的:
//创建Builder
mPreviewRequestBuilder = mCameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW);
//设置持续的自动对焦
mPreviewRequestBuilder.set(CaptureRequest.CONTROL_AF_MODE, CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_PICTURE);
//创建CaptureRequest
mPreviewRequest = mPreviewRequestBuilder.build();
配置完成后就可以进行预览和拍照了,返回的结果被封装成为CameraMetadata
,这是一个控制相机和保存相机信息的基类,它描述了查询信息,拍照结果等返回的key/value
组成的map
,有个很重要的问题是返回的结果以什么形式呈现,我们首先想到的肯定是以方法的返回值返回,但是这里涉及到何时拍照完成的问题,以及拍照中对焦距、闪光灯等配置的调节,都需要立即呈现给Android系统,显然观察者模式是不错的选择,通过设置回调来立即接受到配置的变化以及完成与否,呆萌是这样做的:
/* process(CaptureResult)是针对不同的结果和当前的状态对相机进行设置和操纵的方法 */
private CameraCaptureSession.CaptureCallback mCaptureCallback = new CameraCaptureSession.CaptureCallback() {
@Override
public void onCaptureProgressed(@NonNull CameraCaptureSession session,
@NonNull CaptureRequest request,
@NonNull CaptureResult partialResult) {
process(partialResult);
}
@Override
public void onCaptureCompleted(@NonNull CameraCaptureSession session,
@NonNull CaptureRequest request,
@NonNull TotalCaptureResult result) {
process(result);
}
}
有了上面这几个概念,整个流程也就差不多走通了,下面梳理一下:
CameraManager
,调用其getCameraIdList()
获取所有的CameraId,从中选择需要的,根据ID找到对应的CameraDevice
CameraCharacteristics
,获取各属性(相机取景方向,可支持的输出图像大小),根据属性配置显示的大小,并根据当前屏幕旋转位置和相机位置调整预览和拍照的方向Handler
对象,提供给Camera
用于后台操作 CameraDevice.StateCallback
对象,在开启和关闭的回调中启动和释放资源,并创建预览manager
的open
方法开启相机CameraCaptureSession.CaptureCallback
对象,在回调中根据返回结果进行相应操作CameraDevice
创建CaptureRequest.builder
对象进行配置,并创建Session
对话,在onConfigured
的回调中启动预览request
的目标Surface
改变,并且调用的是Session
的capture()
方法以上就是用Camera2
API拍照的全部流程,其中有不少细节需要注意,在下面将一一注释,如果真的需要用到此API,建议直接修改官方呆萌就够了。
具体见Android设备对新Camera2 API的支持问题:以华为M2为例,现在很多推出的设备虽然都是Android5.0以上的,也就是支持Camera2
的API,但是虽然它们实现了这些API,但是实际上可配置的参数比Camera
少,甚至性能都比Camera
差,为此厂商在系统中写明了对Camera2
的支持程度,通过:
CameraCharacteristics characteristics = manager.getCameraCharacteristics(cameraId);
characteristics.get(CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL);
可以获得支持的程度,得到的值中有这三种:LIMITED值为0,FULL值为1,LEGACY值为2,其支持程度为FULL > LIMITED > LEGACY。关于LEGACY是这样说的:
A LEGACY device does not support per-frame control, manual sensor control, manual post-processing, arbitrary cropping regions, and has relaxed performance constraints.
翻译一下:遗留的设备不支持每帧控制、手动传感器控制、手动后处理、任意裁剪区域,并且具有宽松的性能约束。可以说兼容性很差了,我在自己手机上验证了下,发现LEGACY,属于遗留机!可以预见很多手机上并不完全支持Camera2
API,所以如果手机并不是FULL支持的话,建议还是使用Camera
。
官方呆萌中使用了Semaphore
防止在相机未关闭前就退出了App,因为这样会导致无法正常关闭相机,释放资源,相机服务是系统级别的,App如果一直持有其引用就无法被gc清理,造成泄露,同时也防止并发状态下多个应用频繁启动关闭相机。Semaphore
简单来说就是食堂阿姨,初始化的时候给阿姨指定量的卡布奇诺,调用acquire
的时候拿走拿走一杯,调用release
阿姨就再多放一杯,如果卡布奇诺暂时卖完了,Semaphore
值为0,acquire
就阻塞,等待release
释放一个,还可以设置阻塞时限,如果超时就返回false
等等。在呆萌中这样写:
private final CameraDevice.StateCallback mStateCallback = new CameraDevice.StateCallback() {
@Override
public void onOpened(@NonNull CameraDevice cameraDevice) {
// This method is called when the camera is opened. We start camera preview here.
mCameraOpenCloseLock.release();
mCameraDevice = cameraDevice;
createCameraPreviewSession();
}
@Override
public void onDisconnected(@NonNull CameraDevice cameraDevice) {
mCameraOpenCloseLock.release();
cameraDevice.close();
mCameraDevice = null;
}
@Override
public void onError(@NonNull CameraDevice cameraDevice, int error) {
mCameraOpenCloseLock.release();
cameraDevice.close();
mCameraDevice = null;
Activity activity = getActivity();
if (null != activity) {
activity.finish();
}
}
}
@Override
public void onResume() {
super.onResume();
startBackgroundThread();
if (mTextureView.isAvailable()) {
openCamera(mTextureView.getWidth(), mTextureView.getHeight());
} else {
mTextureView.setSurfaceTextureListener(mSurfaceTextureListener);
}
}
private void openCamera(int width, int height){
if (!mCameraOpenCloseLock.tryAcquire(2500, TimeUnit.MILLISECONDS)) {
throw new RuntimeException("Time out waiting to lock camera opening.");
}
manager.openCamera(mCameraId, mStateCallback, mBackgroundHandler);
}
private void closeCamera(){
try {
mCameraOpenCloseLock.acquire();
...
} finally {
mCameraOpenCloseLock.release();
}
}
首先我们需要直到onDisconnected
方法在退出Activity后并不会被调用,然后我们假设一个场景,第一次打开Activity,此时Semaphore
为1,onResume
回调,mTextureView
不可用,调用setSurfaceTextureListener
方法,最终调用openCamera
,其中尝试获取许可,获得后Semaphore
为0,并打开相机,相机的onOpened
回调,release()
方法被调用,释放一个许可,Semaphore
为1,随后退出App,在OnPause
回调中关闭相机,首先调用acquire()
获取许可,并调用close()
方法关闭相机,注意关闭相机中并未退出Activity
,此时如果其他应用调用openCamera
方法,会因为Semaphore
为0而处于阻塞状态,等到相机关闭后finally
语句块调用,释放许可,允许其他应用访问相机。
在创建CameraCaptureSession
的时候需要传入一个回调对象,其中有一个回调方法是这样的:
@Override
public void onConfigureFailed(
@NonNull CameraCaptureSession cameraCaptureSession) {
showToast("Failed");
}
很多时候我们会遇到这个回调,特别是没有配置好的时候,这个回调发生的时机在官方文档中是这样的:
This can happen if the set of requested outputs contains unsupported sizes, or too many outputs are requested at once.
不支持的大小?没错,无论是Camera
还是Camera2
都有着支持的分辨率大小,如果请求输出的Surface
包括了不支持的分辨率,就会回调这个方法,并且无法正确打开相机,呆萌中的几个方法都是在设置合适的分辨率大小:
private void setUpCameraOutputs(int width, int height){}
private static Size chooseOptimalSize(Size[] choices, int textureViewWidth, int textureViewHeight, int maxWidth, int maxHeight, Size aspectRatio){}
具体的筛选见呆萌,就是在最大宽高范围内,根据textureView
的宽高,分为大的Size和不大的Size,如果有大的,在大的中选最小的,反之在小中选最大的。然后根据屏幕旋转和相机的方向,以及当前屏幕的Orientation
进行调整。总之输出的Surface
的大小也是很讲究的。
剩下的难点其实就是CaptureResult
中各个配置常量的含义,这点太多了,就不一一列举了,大家用到的时候自己去发掘吧。