背景
机缘巧合,需要自定义相机,几日折腾下来,对相机开发有了一定认识,做个小结。
既然是自定义相机,在设想里,相机和UI是结构是这样的:
关键信息有:
- 由CameraView去接收相机设备所给予的反馈
- 同时CameraView也是遥控器,外部能通过CameraView操作相机设备
这样做的好处是:
- 相机控制,预览,操作等应该是视图无关的。放在任意的页面,做简单的配置便可以使用。
- CameraView对外屏蔽了种种使用相机可能造成的隐患,仅需要暴露必要的接口达到安全操控相机的操作即可。
- 对于具体UI来说,相机回执到的数据如何接收如何展示,让CameraView自己处理就好了,不关心,我给它一个显示位置就可以了。
通过类似的处理,能让自定义相机成为可复用、易维护、低成本的功能,而不是冗杂的一次性消费。
方案寻找
确定预想之后,就要寻求具体的开发实现了。几经寻找,看到的博文等零碎非常严重,让我痛苦不堪。好在最终找到了Google给出来的开源方案。
Google相机开源方案
对于着急的同学,可以直接拿去用了,给CameraView添加CameraView.Callback基本能达到目的。
public abstract static class Callback {
/**
* 相机打开
*/
public void onCameraOpened(CameraView cameraView) {
}
/**
* 相机关闭
*/
public void onCameraClosed(CameraView cameraView) {
}
/**
* 相机拿到静态图片数据
*/
public void onPictureTaken(CameraView cameraView, byte[] data) {
}
}
记得别忘了添加权限以及在合适的生命周期释放相应资源即可。
但是作为头铁大分队小队长,想去看看Google的方案是如何实现的,填了什么坑。万一以后有什么骚操作,也好有个照应。余下的内容均是对于此方案的实现分析,以达到目的的同学可以先撤了。
相机开发须知
分裂的API
Android 5.0以下的API为Camera 而 Android 5.0以上的API为Camera2,并且各大手机厂商对于Camera2的支持程度也不同。对于不支持Camera2的设备来说,需要降级使用Camera.
SurfaceView/TextureView
界面渲染主要涉及到SurfaceView 和 TextureView , 在4.0以上才能使用TextureView 。两者的简要区别如图:
SurfaceView不受View Hierarchy约束,拥有自己的Surface,可以理解为是另一个Window。因此一些View特性无法使用,但也因此不会影响主线程,可以放到其它的线程进行渲染,性能友好。
TextureView 则与普通的View类似,受View Hierarchy约束,相机发送过来的数据经由SurfaceTexture交接,让TextureView能以硬件加速渲染的方式存在视图树,也因此更耗费性能。
易出问题场景
除了API的分离,设备的支持程度外,面对生命周期的变化,也需谨慎处理。常见有后台场景,锁屏场景。相机是共享资源,其它程序访问使用时容易发生冲突,因此需要正确地释放。
Google CameraView
面对以上问题,CameraView 提出的方案如图
图片来源
运转由CameraView来完成。至于具体使用的是哪种视图渲染,由PreviewImpl来决定。与此相似,具体操作哪种相机的API也由CameraViewImpl进行对接。PreviewImpl和CameraViewImpl则提供对外一致的操作接口。对于外部来说,CameraView是外观,使用CameraView开放的操作,即可完成操作,内部变化一无所知。
先看CameraViewImpl
abstract class CameraViewImpl {
// 相机基础事件回调
protected final Callback mCallback;
// 渲染视图
protected final PreviewImpl mPreview;
CameraViewImpl(Callback callback, PreviewImpl preview) {
mCallback = callback;
mPreview = preview;
}
// 获取渲染视图
View getView() {
return mPreview.getView();
}
// 启动相机
abstract boolean start();
// 暂停相机
abstract void stop();
// 相机使用状态
abstract boolean isCameraOpened();
// 设置使用哪一个相机,简单如前置相机、后置相机
abstract void setFacing(int facing);
// 获取当前相机标识
abstract int getFacing();
// 获取相机支持的预览比例
abstract Set<AspectRatio> getSupportedAspectRatios();
// 设置拍摄照片比例
abstract boolean setAspectRatio(AspectRatio ratio);
// 获取相机当前摄照片比例
abstract AspectRatio getAspectRatio();
// 设置自动聚焦
abstract void setAutoFocus(boolean autoFocus);
// 获取自动聚焦
abstract boolean getAutoFocus();
// 设置闪光状态
abstract void setFlash(int flash);
// 获取闪光状态
abstract int getFlash();
// 获取静态图片,即拍照
abstract void takePicture();
// 设置相机方向
abstract void setDisplayOrientation(int displayOrientation);
// 相机基础回调接口
interface Callback {
// 相机已打开
void onCameraOpened();
// 相机已关闭
void onCameraClosed();
// 相机获取到静态图片
void onPictureTaken(byte[] data);
}
}
CameraViewImpl陈列了共性的可能的相机操作,挑一些做说明:
setFacing()
设置使用具体相机,简单的如前置相机、后置相机。在相机更变后,原本的预览视图可能因为Rotate而呈现出了不一样的视图,因此不仅需要更具需求切换到正确的相机,还需将预览视图进行矫正。
getSupportedAspectRatios()
不同的设备支持的预览视图是不同的,常见的预览视图如4:3、16:9等,因此可以支持的预览视图并不是完全相同的,试具体情况而定。而AspectRatioc存有的就是比例信息,并支持了一些比较、匹配操作。
setAutoFocu()
一般来说,相机设备自动聚焦是默认开启的。当然如更多的聚焦模式如固定聚焦、景深、远景、微焦等也是可以另外支持的。当然,对于我等没有摄友的人来说,玩转不来。
setFlash()
闪光灯状态一般有如自动、关闭、拍照、防红眼等。
接着是PreviewImpl
abstract class PreviewImpl {
interface Callback {
// surface发生了变动
void onSurfaceChanged();
}
private Callback mCallback;
// 预览视图高度
private int mWidth;
// 预览视图宽度
private int mHeight;
void setCallback(Callback callback) {
mCallback = callback;
}
abstract Surface getSurface();
// 获取实际的渲染View
abstract View getView();
// 输出源
abstract Class getOutputClass();
// 预览方向
abstract void setDisplayOrientation(int displayOrientation);
// 渲染视图是否达到可用状态
abstract boolean isReady();
// 分发surface 的更变
protected void dispatchSurfaceChanged() {
mCallback.onSurfaceChanged();
}
// 主要是为了由SurfaceView渲染的情况
SurfaceHolder getSurfaceHolder() {
return null;
}
// 主要是为了由TextureView渲染的情况
Object getSurfaceTexture() {
return null;
}
// 设置缓冲区大小
void setBufferSize(int width, int height) {
}
void setSize(int width, int height) {
mWidth = width;
mHeight = height;
}
int getWidth() {
return mWidth;
}
int getHeight() {
return mHeight;
}
}
PreviewImpl的主要作用,是提供了必要信息使能接收CameraImpl给予的信息,并进行渲染。
准备好了PreviewImpl和CameraImpl之后,CameraView就可以工作了
public class CameraView extends FrameLayout{
public CameraView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
if (isInEditMode()){
mCallbacks = null;
mDisplayOrientationDetector = null;
return;
}
// 获取正确的渲染视图
final PreviewImpl preview = createPreviewImpl(context);
// 是CamereViewImpl.Callback实现类,接收相机基础事件
mCallbacks = new CallbackBridge();
// 根据SDK使用正确的相机API
if (Build.VERSION.SDK_INT < 21) {
mImpl = new Camera1(mCallbacks, preview);
} else if (Build.VERSION.SDK_INT < 23) {
mImpl = new Camera2(mCallbacks, preview, context);
} else {
mImpl = new Camera2Api23(mCallbacks, preview, context);
}
// 初始化配置信息,主要是相机参数,包括聚焦、闪光、预览比例、屏幕方向等
TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.CameraView, defStyleAttr,
R.style.Widget_CameraView);
// 自动自动调整预览视图,以和拍摄照片比例一致
mAdjustViewBounds = a.getBoolean(R.styleable.CameraView_android_adjustViewBounds, false);
// 更新相机,默认后置
setFacing(a.getInt(R.styleable.CameraView_facing, FACING_BACK));
// 更新拍摄照片比例,默认为4:3
String aspectRatio = a.getString(R.styleable.CameraView_aspectRatio);
if (aspectRatio != null) {
setAspectRatio(AspectRatio.parse(aspectRatio));
} else {
setAspectRatio(Constants.DEFAULT_ASPECT_RATIO);
}
// 更新聚焦模式
setAutoFocus(a.getBoolean(R.styleable.CameraView_autoFocus, true));
// 更新闪光灯
setFlash(a.getInt(R.styleable.CameraView_flash, Constants.FLASH_AUTO));
a.recycle();
// 屏幕方向探测
mDisplayOrientationDetector = new DisplayOrientationDetector(context) {
@Override
public void onDisplayOrientationChanged(int displayOrientation) {
mImpl.setDisplayOrientation(displayOrientation);
}
};
}
...
}
在CameraView的初始化过程,根据实际情况获取相应的PreviewImpl和CameraImpl,具体对相机的执行实现和渲染实现,CameraView,并不关心。createPreviewImpl()获取PreviewImpl的方式也是根据版本信息拿到,不做展开。
有几个细节:
mAdjustViewBounds
此参数是用来调整相机的预览视图达到与拍摄图片一样的尺寸比例。预览视图即你拍摄预览时看到的视图,拍摄出来的图片的尺寸比例和预览视图很可能是不一样的。比如,前者可能是16:9,后者可能是4:3。使用时需要注意,特别是当要对拍摄出的静态图片做二次操作的时候,尺寸信息可能不如我们所想。但当两者比例一致的时候,无此顾虑。
mDisplayOrientationDetector
CameraView内部类,作用是为了探测屏幕方向,横屏竖屏,方便做出调整以免预览视图出现不合适的角度。
拍摄图片比例4:3
这是一个预设值,CameraView并不知道外部的使用情况,且相机能支持多种图片比例,因此需要根据实际需求做调整。
CameraView需要在onMeasure()根据配置情况做测量
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
if (isInEditMode()){
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
return;
}
// 需要自动预览视图和照片比例一致
if (mAdjustViewBounds) {
if (!isCameraOpened()) {
mCallbacks.reserveRequestLayoutOnOpen();
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
return;
}
final int widthMode = MeasureSpec.getMode(widthMeasureSpec);
final int heightMode = MeasureSpec.getMode(heightMeasureSpec);
if (widthMode == MeasureSpec.EXACTLY && heightMode != MeasureSpec.EXACTLY) {
final AspectRatio ratio = getAspectRatio();
assert ratio != null;
int height = (int) (MeasureSpec.getSize(widthMeasureSpec) * ratio.toFloat());
if (heightMode == MeasureSpec.AT_MOST) {
height = Math.min(height, MeasureSpec.getSize(heightMeasureSpec));
}
super.onMeasure(widthMeasureSpec,
MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY));
} else if (widthMode != MeasureSpec.EXACTLY && heightMode == MeasureSpec.EXACTLY) {
final AspectRatio ratio = getAspectRatio();
assert ratio != null;
int width = (int) (MeasureSpec.getSize(heightMeasureSpec) * ratio.toFloat());
if (widthMode == MeasureSpec.AT_MOST) {
width = Math.min(width, MeasureSpec.getSize(widthMeasureSpec));
}
super.onMeasure(MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY),
heightMeasureSpec);
} else {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}
} else {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}
int width = getMeasuredWidth();
int height = getMeasuredHeight();
AspectRatio ratio = getAspectRatio();
// 根据屏幕方向调整预览视图
if (mDisplayOrientationDetector.getLastKnownDisplayOrientation() % 180 == 0) {
ratio = ratio.inverse();
}
assert ratio != null;
if (height < width * ratio.getY() / ratio.getX()) {
mImpl.getView().measure(
MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY),
MeasureSpec.makeMeasureSpec(width * ratio.getY() / ratio.getX(),
MeasureSpec.EXACTLY));
} else {
mImpl.getView().measure(
MeasureSpec.makeMeasureSpec(height * ratio.getX() / ratio.getY(),
MeasureSpec.EXACTLY),
MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY));
}
}
代码虽长,但主要做了两件事情:
- 根据自动调整状态和测量模式对尺寸进行测量
- 对屏幕方向进行响应
之前说过,CameraView对外部暴露了一定的接口间接地使用相机设备,具体的实现情况基本是转发到CameraImpl和PreViewImpl,不贴出。此外,CameraView是View,因此需要处理恢复视图的情况。
@Override
protected Parcelable onSaveInstanceState() {
SavedState state = new SavedState(super.onSaveInstanceState());
state.facing = getFacing();
state.ratio = getAspectRatio();
state.autoFocus = getAutoFocus();
state.flash = getFlash();
return state;
}
@Override
protected void onRestoreInstanceState(Parcelable state) {
if (!(state instanceof SavedState)) {
super.onRestoreInstanceState(state);
return;
}
SavedState ss = (SavedState) state;
super.onRestoreInstanceState(ss.getSuperState());
setFacing(ss.facing);
setAspectRatio(ss.ratio);
setAutoFocus(ss.autoFocus);
setFlash(ss.flash);
}
SavedState存储里相机配置和图片配置信息,不做纠缠。
CameraView的工作大致如此:
- 获取合适的CameraImpl和PreViewImpl,间接完成操作相机以及渲染视图工作
- 对外部操作做出响应,也对相机的使用配置情况做出响应
- 协调CameraImpl和PreViewImpl
虽然知道了CameraView的处理过程,但是具体的实现过程还很模糊,现在,通过对Camera2的使用,抛砖引玉,一探究竟。
Camera2
从CameraView里看到,版本>=21情况下,CameraImpl实际为Camera2,Camera2自然实现了相应的接口。对于使用相机来说,最感兴趣的无非开启、关闭、拍摄以及更变配置等操作。在看具体过程之前,先了解一些知识点,有个概念即可。
- CameraManager: 摄像头管理器,用于打开和关闭系统摄像头
- CameraCharacteristics:摄像头的各种特性描述
- CameraDevice: 系统摄像头
- CameraCaptureSeesion: 在操作摄像头时,需要透过此类会话
- CaptureRequest: 操作请求
- CaptureResult: 操作结果
具体操作流程如图
图片来源
在与摄像头建立会话后,操作请求会发送到CameraDevice,CameraDevice处理完后将操作结果发送出去,将Image Buffer发送到Surface。
开启相机
一切,从启动相机开始
@Override
boolean start() {
// 获取摄像头
if (!chooseCameraIdByFacing()) {
return false;
}
// 获取相机信息
collectCameraInfo();
// 配置接收源信息,即想拿到什么样的静态图片
prepareImageReader();
// 启动相机
startOpeningCamera();
return true;
}
在能获取到摄像头后,才能有之后的操作
private boolean chooseCameraIdByFacing() {
try {
// 获取当前的所需要的摄像头, 默认为后置
int internalFacing = INTERNAL_FACINGS.get(mFacing);
// 获取所有的摄像头id
final String[] ids = mCameraManager.getCameraIdList();
// 没有摄像头的情况
if (ids.length == 0) {
throw new RuntimeException("No camera available.");
}
// 遍历查找摄像头
for (String id : ids) {
CameraCharacteristics characteristics = mCameraManager.getCameraCharacteristics(id);
// 获取硬件等级,即相机的兼容性
Integer level = characteristics.get(
CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL);
// 获取不到硬件等级或向后兼容模式,不符合
if (level == null ||
level == CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LEGACY) {
continue;
}
// 获取该摄像头处于的屏幕方向
Integer internal = characteristics.get(CameraCharacteristics.LENS_FACING);
if (internal == null) {
throw new NullPointerException("Unexpected state: LENS_FACING null");
}
// 与所需要的相机方向一致,为合适的摄像头
if (internal == internalFacing) {
mCameraId = id;
mCameraCharacteristics = characteristics;
return true;
}
}
// 没有找到一致的摄像头,取第一个,之后的过程与上面类似
mCameraId = ids[0];
mCameraCharacteristics = mCameraManager.getCameraCharacteristics(mCameraId);
Integer level = mCameraCharacteristics.get(
CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL);
if (level == null ||
level == CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LEGACY) {
return false;
}
Integer internal = mCameraCharacteristics.get(CameraCharacteristics.LENS_FACING);
if (internal == null) {
throw new NullPointerException("Unexpected state: LENS_FACING null");
}
for (int i = 0, count = INTERNAL_FACINGS.size(); i < count; i++) {
if (INTERNAL_FACINGS.valueAt(i) == internal) {
mFacing = INTERNAL_FACINGS.keyAt(i);
return true;
}
}
// 到这里说明用了外设,当作后置摄像头处理
mFacing = Constants.FACING_BACK;
return true;
} catch (CameraAccessException e) {
throw new RuntimeException("Failed to get a list of camera devices", e);
}
}
选取摄像头的过程,正常情况下出现三种情况:
- 选取到指定的摄像头
- 选取不到指定的摄像头,默认使用系统下发的第一个摄像头
- 选取到外设摄像头
选取到合适的摄像头之后,需要收集摄像头的信息
private void collectCameraInfo() {
// 获取设备支持的stream配置
StreamConfigurationMap map = mCameraCharacteristics.get(
CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP);
if (map == null) {
throw new IllegalStateException("Failed to get configuration map: " + mCameraId);
}
mPreviewSizes.clear();
// 获取输出配置
for (android.util.Size size : map.getOutputSizes(mPreview.getOutputClass())) {
int width = size.getWidth();
int height = size.getHeight();
if (width <= MAX_PREVIEW_WIDTH && height <= MAX_PREVIEW_HEIGHT) {
// 收集输出配置,即支持的预览视图比例
mPreviewSizes.add(new Size(width, height));
}
}
mPictureSizes.clear();
// 收集拍照图片比例, 默认区格式为JPEG的拍照比例
collectPictureSizes(mPictureSizes, map);
// 移除不合适的预览视图比例
for (AspectRatio ratio : mPreviewSizes.ratios()) {
if (!mPictureSizes.ratios().contains(ratio)) {
mPreviewSizes.remove(ratio);
}
}
// 如果当前预览视图比例不被支持的话,取第一个
if (!mPreviewSizes.ratios().contains(mAspectRatio)) {
mAspectRatio = mPreviewSizes.ratios().iterator().next();
}
}
经过collectCameraInfo() 之后,拥有了预览视图比例集合和拍照视图比例集合,在进行拍照后,拿到的静态图片比例,与当前支持的预览视图比例一致。
而之后的prepareImageReader()和startOpeningCamera()就不贴了,很清楚,说一下要点。
ImageReader会接受到相机设备给出图像信息,在ImageReader.OnImageAvailableListener能拿到byte[], 再将此数据发送给CameraViewImpl.Callback。
开启相机则通过 mCameraManager.openCamera(mCameraId, mCameraDeviceCallback, null)进行,其中mCameraId是之前选取摄像头拿到的摄像头id,mCameraDeviceCallback是用来接受CameraDevice的执行状态 。
public static abstract class CameraDevice.StateCallback{
// 相机设备打开
public abstract onOpened(@NonNull CameraDevice camera);
// 相机设备关闭
public void onClosed(@NonNull CameraDevice camera);
// 相机设备脱离连接
public abstract onDisconnected(@NonNull CameraDevice camera);
// 错误
public abstract onError(@NonNull CameraDevice camera, int error);
...
};
当前的操作是打开相机,因此,收到相机设备打开回调
private final CameraDevice.StateCallback mCameraDeviceCallback
= new CameraDevice.StateCallback() {
@Override
public void onOpened(@NonNull CameraDevice camera) {
// 记录相机设备
mCamera = camera;
// 回调CamereView.Impl
mCallback.onCameraOpened();
// 开启相机会话
startCaptureSession();
}
....
};
见开启相机会话
void startCaptureSession() {
// 有CameraDevice,Preview达到可用,Imagereader可用
if (!isCameraOpened() || !mPreview.isReady() || mImageReader == null) {
return;
}
// 获取合适的预览视图尺寸
Size previewSize = chooseOptimalSize();
// 设置image buffer 尺寸
mPreview.setBufferSize(previewSize.getWidth(), previewSize.getHeight());
Surface surface = mPreview.getSurface();
try {
// 构造相机预览请求
mPreviewRequestBuilder = mCamera.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW);
// 提供surface , 此数据用于渲染
mPreviewRequestBuilder.addTarget(surface);
// 提供surface, 此数据用于其它作用
mCamera.createCaptureSession(Arrays.asList(surface, mImageReader.getSurface()),
mSessionCallback, null);
} catch (CameraAccessException e) {
throw new RuntimeException("Failed to start camera session");
}
}
首先,需要获取合适的image buffer尺寸,一般为预览视图尺寸,因为源Image buffer很大,因此需要提供合适的尺寸已达到满足预览需求即可。
其次,这里提供了两个surface,为何如此?参考之前给的拍照流程图就明白了,其中的一个surface所接受到的相机发来的图像数据,最终是要传到PreviewImpl的具体渲染视图进行渲染的;而另一个surface将拍摄请求结果的图像数据传递给Imagereader,可做他用,比如生成静态图片。看起来,像下图
现在相机已经正常工作了,现在进行正常拍照。
@Override
void takePicture() {
if (mAutoFocus) {
lockFocus();
} else {
captureStillPicture();
}
}
根据是否自动聚焦,执行到不同动作
// 自动聚焦执行
private void lockFocus() {
mPreviewRequestBuilder.set(CaptureRequest.CONTROL_AF_TRIGGER,
CaptureRequest.CONTROL_AF_TRIGGER_START);
try {
mCaptureCallback.setState(PictureCaptureCallback.STATE_LOCKING);
mCaptureSession.capture(mPreviewRequestBuilder.build(), mCaptureCallback, null);
} catch (CameraAccessException e) {
Log.e(TAG, "Failed to lock focus.", e);
}
}
// 非自动聚执行
void captureStillPicture() {
try {
CaptureRequest.Builder captureRequestBuilder = mCamera.createCaptureRequest(
CameraDevice.TEMPLATE_STILL_CAPTURE);
captureRequestBuilder.addTarget(mImageReader.getSurface());
captureRequestBuilder.set(CaptureRequest.CONTROL_AF_MODE,
mPreviewRequestBuilder.get(CaptureRequest.CONTROL_AF_MODE));
switch (mFlash) {
case Constants.FLASH_OFF:
captureRequestBuilder.set(CaptureRequest.CONTROL_AE_MODE,
CaptureRequest.CONTROL_AE_MODE_ON);
captureRequestBuilder.set(CaptureRequest.FLASH_MODE,
CaptureRequest.FLASH_MODE_OFF);
break;
case Constants.FLASH_ON:
captureRequestBuilder.set(CaptureRequest.CONTROL_AE_MODE,
CaptureRequest.CONTROL_AE_MODE_ON_ALWAYS_FLASH);
break;
case Constants.FLASH_TORCH:
captureRequestBuilder.set(CaptureRequest.CONTROL_AE_MODE,
CaptureRequest.CONTROL_AE_MODE_ON);
captureRequestBuilder.set(CaptureRequest.FLASH_MODE,
CaptureRequest.FLASH_MODE_TORCH);
break;
case Constants.FLASH_AUTO:
captureRequestBuilder.set(CaptureRequest.CONTROL_AE_MODE,
CaptureRequest.CONTROL_AE_MODE_ON_AUTO_FLASH);
break;
case Constants.FLASH_RED_EYE:
captureRequestBuilder.set(CaptureRequest.CONTROL_AE_MODE,
CaptureRequest.CONTROL_AE_MODE_ON_AUTO_FLASH);
break;
}
/**
* 根据相机传感器参数,计算出得出的图片的方向,并结合相机位置进行调整
**/
@SuppressWarnings("ConstantConditions")
int sensorOrientation = mCameraCharacteristics.get(
CameraCharacteristics.SENSOR_ORIENTATION);
captureRequestBuilder.set(CaptureRequest.JPEG_ORIENTATION,
(sensorOrientation +
mDisplayOrientation * (mFacing == Constants.FACING_FRONT ? 1 : -1) +
360) % 360);
// 取消之前的拍摄操作
mCaptureSession.stopRepeating();
// 发送拍摄请求
mCaptureSession.capture(captureRequestBuilder.build(),
new CameraCaptureSession.CaptureCallback() {
@Override
public void onCaptureCompleted(@NonNull CameraCaptureSession session,
@NonNull CaptureRequest request,
@NonNull TotalCaptureResult result) {
// 处理完结果后, 重启开启相机
unlockFocus();
}
}, null);
} catch (CameraAccessException e) {
Log.e(TAG, "Cannot capture a still picture.", e);
}
}
不管是在自动聚焦还是不是自动聚焦,都需根据所要求的相机配置给captureRequestBuilder即CaptureRequest.Builder设置相应的标志,而自动聚焦使用了打多少默认标志,因此不再配置。而当不是自动聚焦的情况时,需要配置闪光灯的标志,且需要在拍摄请求完成后解锁自动聚焦,并重启相机,释放。在准备好各项参数后,通过相机会话mCaptureSession发送拍照请求。
如果相机通过unlockFocus()重启,则mPreviewRequestBuilder会根据相应的配置信息,重制到合适的状态,各项参数通过mPreviewRequestBuilder.set()以key value 形式设置,详情见源码 updateAutoFocus() 和 updateFlash() 不列出,仅需要知道,在二者里设置了拍照回调监听。
// 使拍照请求能不断执行
mCaptureSession.setRepeatingRequest(mPreviewRequestBuilder.build(),
mCaptureCallback, null);
所有的拍摄回调会由mCaptureCallback监听,并控制状态。
private static abstract class PictureCaptureCallback
extends CameraCaptureSession.CaptureCallback {
static final int STATE_PREVIEW = 0;
static final int STATE_LOCKING = 1;
static final int STATE_LOCKED = 2;
static final int STATE_PRECAPTURE = 3;
static final int STATE_WAITING = 4;
static final int STATE_CAPTURING = 5;
private int mState;
PictureCaptureCallback() {
}
void setState(int state) {
mState = state;
}
@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);
}
......
}
拍照会话请求过程会回调到PictureCaptureCallback,PictureCaptureCallback所做的,保证整个拍照过程状态正确切换,主要处理自动聚焦情况,感兴趣的可以自理PictureCaptureCallBack.process()。这里仅放出状态切换图
哪如何拿到拍摄的静态图片数据呢?
根据之前的分析,拍照会话请求时,ImageReader提供了Surface以接收静态图片数据,而这项数据最终会由ImageReader.OnImageAvailableLisetner接收到
private final ImageReader.OnImageAvailableListener mOnImageAvailableListener
= new ImageReader.OnImageAvailableListener() {
@Override
public void onImageAvailable(ImageReader reader) {
try (Image image = reader.acquireNextImage()) {
Image.Plane[] planes = image.getPlanes();
if (planes.length > 0) {
ByteBuffer buffer = planes[0].getBuffer();
byte[] data = new byte[buffer.remaining()];
buffer.get(data);
mCallback.onPictureTaken(data);
}
}
}
};
如果确实拿到能用的图片数据的话,通过 mCallback.onPictureTaken(data) 将数据发送给外部,当前情况,则是CameraView.CallbackBridge,而CallBackBridge会继续分发个CameraView.Callback。 因此,具体UI添加CameraView.Callback监听,即可拿到静态图片数据。
关闭操作就不解析了。
总结
- 根据具体设备的摄像头配置,收集摄像头的主要特性信息
- 通过CaptureRequest.Builder构建相机会话请求,并保存相应相机操作配置
- 需要考虑相机位置和拍摄图片的关系,预览视图和拍摄视图的关系
- 通过ImageReader拿到拍摄图片数据
对于Camera2的实现分析大致如此,Camera就自行阅读了。当然,以上所说均是皮毛而已,有很多实现还云里雾里,要明白了解透彻,还有很长的路要走,励之勉之。
参考
Android Camera2 简介
Android Camera2 使用总结
Android平台Camera开发实践指南
Android 5.0(Lollipop)中的SurfaceTexture,TextureView, SurfaceView和GLSurfaceView