心行慈善,何需努力看经—《西游记》
本系列主要讲述Android开发中Camera的相关操作、预览方式、视频录制等,项目结构代码耦合性低,旨在帮助大家能从中有所收获(方便copy :) ),对于个人来说也是一个总结的好机会
一. Camera操作
Android系统存在这么多年,google更新了不少API。光是对摄像头的操作目前就有3中API:
- android.hardware.Camera:最早用来自定义Camera的API
- android.hardware.camera2.*:Android5.0之后推荐使用的API,对Camera操作更灵活,功能更丰富
- CameraX:对Camera2的封装,API更简单
由于Android版本众多,考虑兼容性,本文我们还是对android.hardware.Camera
进行讲解,操作Camera具体需要哪些步骤呢?
- 设置Camera权限,Android6.0以上请动态申请
- 打开相机,
Camera.open()
- 开始预览,
startPreview
- 停止预览,
stopPreview
- 关闭相机,
release()
1. 打开相机
申请Camera权限,Android6.0以上记得在打开相机前进行动态申请
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="android.permission.CAMERA" />
<uses-feature android:name="android.hardware.camera" />
...
</manifest>
我们对Camera进行了简单的封装,无论是Camera还是Camera2我们预览视图都可以是SurfaceView、TextureView、GLSurfaceView,所以我们将Camera操作和预览视图进行分离,让预览View不在依赖具体的某种Camera实现
定义接口类ICameraManager:
/**
* Camera和Camera2通用接口
*
* @author xiaozhi
* @since 2024/8/15
*/
public interface ICameraManager {
/**
* 打开Camera
*/
void openCamera();
/**
* 关闭释放Camera
*/
void releaseCamera();
/**
* 开启预览
*
* @param surfaceHolder
*/
void startPreview(SurfaceHolder surfaceHolder);
/**
* 开启预览
*
* @param surfaceTexture
*/
void startPreview(SurfaceTexture surfaceTexture);
/**
* 停止预览
*/
void stopPreview();
...
}
CameraManager
实现ICameraManager
,打开摄像头
/**
* 打开Camera
*/
@Override
public synchronized void openCamera() {
Logs.i(TAG, "Camera open #" + mCameraId);
if (mCamera == null) {
if (mCameraId >= Camera.getNumberOfCameras()) {
onOpenError(CAMERA_ERROR_NO_ID, "No camera.");
return;
}
try {
mCamera = Camera.open(mCameraId);
Camera.getCameraInfo(mCameraId, mCameraInfo);
mCamera.setErrorCallback(errorCallback);
initCamera();
onOpen();
mOrientationEventListener.enable();
} catch (Exception e) {
onOpenError(CAMERA_ERROR_OPEN, e.getMessage());
}
}
}
打开摄像头之后我们可以设置摄像头的一些基本参数,如预览尺寸,拍照尺寸等
/**
* 配置Camera参数
*/
private void initCamera() {
if (mCamera != null) {
mParameters = mCamera.getParameters();
if (mDisplayOrientation == -1) {
setCameraDisplayOrientation(mContext, mCameraId, mCamera);
}
// 设置预览方向
mCamera.setDisplayOrientation(mDisplayOrientation);
// 设置拍照方向
mParameters.setRotation(mOrientation);
// 如果摄像头不支持这些参数都会出错的,所以设置的时候一定要判断是否支持
List<String> supportedFlashModes = mParameters.getSupportedFlashModes();
if (supportedFlashModes != null && supportedFlashModes.contains(Parameters.FLASH_MODE_OFF)) {
mParameters.setFlashMode(Parameters.FLASH_MODE_OFF); // 设置闪光模式
}
List<String> supportedFocusModes = mParameters.getSupportedFocusModes();
if (supportedFocusModes != null && supportedFocusModes.contains(Parameters.FOCUS_MODE_AUTO)) {
mParameters.setFocusMode(Parameters.FOCUS_MODE_AUTO); // 设置聚焦模式
}
mParameters.setPreviewFormat(ImageFormat.NV21); // 设置预览图片格式
mParameters.setPictureFormat(ImageFormat.JPEG); // 设置拍照图片格式
Camera.Size previewSize = getSuitableSize(mParameters.getSupportedPreviewSizes());
mPreviewWidth = previewSize.width;
mPreviewHeight = previewSize.height;
mPreviewSize = new Size(mPreviewWidth, mPreviewHeight);
mParameters.setPreviewSize(mPreviewWidth, mPreviewHeight);
Logs.d(TAG, "previewWidth: " + mPreviewWidth + ", previewHeight: " + mPreviewHeight);
Camera.Size pictureSize = mParameters.getPictureSize();
mParameters.setPictureSize(pictureSize.width, pictureSize.height);
Logs.d(TAG, "pictureWidth: " + pictureSize.width + ", pictureHeight: " + pictureSize.height);
mCamera.setParameters(mParameters);
isSupportZoom = mParameters.isSmoothZoomSupported();
}
}
2.开始预览
/**
* 使用Surfaceview开启预览
*
* @param holder
*/
@Override
public synchronized void startPreview(SurfaceHolder holder) {
Logs.i(TAG, "startPreview...");
if (isPreviewing) {
return;
}
if (mCamera != null) {
try {
mCamera.setPreviewDisplay(holder);
if (!mPreviewBufferCallbacks.isEmpty()) {
mCamera.addCallbackBuffer(new byte[mPreviewWidth * mPreviewHeight * 3 / 2]);
mCamera.setPreviewCallbackWithBuffer(mPreviewCallback);
}
mCamera.startPreview();
onPreview(mPreviewWidth, mPreviewHeight);
} catch (Exception e) {
onPreviewError(CAMERA_ERROR_PREVIEW, e.getMessage());
}
}
}
3. 停止预览
/**
* 关闭预览
*/
@Override
public synchronized void stopPreview() {
Logs.v(TAG, "stopPreview.");
if (isPreviewing && null != mCamera) {
try {
mCamera.setPreviewCallback(null);
mCamera.stopPreview();
mPreviewBufferCallbacks.clear();
} catch (Exception e) {
e.printStackTrace();
}
}
isPreviewing = false;
}
4. 关闭相机
/**
* 停止预览,释放Camera
*/
@Override
public synchronized void releaseCamera() {
Logs.v(TAG, "releaseCamera.");
if (null != mCamera) {
stopPreview();
try {
mCamera.release();
mCamera = null;
mCameraBytes = null;
mDisplayOrientation = -1;
} catch (Exception e) {
}
onClose();
}
}
二.SurfaceView使用
我们要预览Camera数据必须要使用一个视图承接,SurfaceView是最常用的,也是Camera最初的标配
SurfaceView的特点:在自己独立的线程中绘制,内部使用双缓冲机制,画面更流畅。相比于 TextureView,它内存占用低,绘制更及时,耗时也更低,但不支持动画和截图。
Camera预览需要将SurfaceHolder传递给Camera然后开启预览如何获取SurfaceHoler?
- 自定义CameraSurfaceView继承SurfaceView
- 实现SurfaceHolder.Callback接口,并在CameraSurfaceView初始化时设置回调
- 实现自定义CameraCallback接口,监听Camera状态
- 一定要实现
onResume
和onPause
接口,并在对应的Activity生命周期中调用。这是所有使用Camera的bug的源头
/**
* 摄像头预览SurfaceView
*
* @author xiaozhi
* @since 2024/8/22
*/
public class CameraSurfaceView extends SurfaceView implements SurfaceHolder.Callback, CameraCallback {
private static final String TAG = CameraSurfaceView.class.getSimpleName();
SurfaceHolder mSurfaceHolder;
private Context mContext;
private Handler mHandler;
private boolean hasSurface; // 是否存在摄像头显示层
private CameraManager mCameraManager;
private int mRatioWidth = 0;
private int mRatioHeight = 0;
private int mSurfaceWidth;
private int mSurfaceHeight;
public CameraSurfaceView(Context context) {
super(context);
init(context);
}
public CameraSurfaceView(Context context, AttributeSet attrs) {
super(context, attrs);
init(context);
}
public CameraSurfaceView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init(context);
}
private void init(Context context) {
mContext = context;
mHandler = new Handler(context.getMainLooper());
mSurfaceHolder = getHolder();
mSurfaceHolder.setFormat(PixelFormat.TRANSPARENT);//translucent半透明 transparent透明
mSurfaceHolder.setType(SurfaceHolder.SURFACE_TYPE_PUSH_BUFFERS);
mSurfaceHolder.addCallback(this);
mCameraManager = new CameraManager(context);
mCameraManager.setCameraCallback(this);
}
public CameraManager getCameraManager() {
return mCameraManager;
}
@Override
public void surfaceCreated(SurfaceHolder holder) {
Logs.i(TAG, "surfaceCreated..." + hasSurface);
if (!hasSurface && holder != null) {
hasSurface = true;
openCamera();
}
}
@Override
public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
Logs.i(TAG, "surfaceChanged [" + width + ", " + height + "]");
mSurfaceWidth = width;
mSurfaceHeight = height;
}
@Override
public void surfaceDestroyed(SurfaceHolder holder) {
Logs.v(TAG, "surfaceDestroyed.");
closeCamera();
hasSurface = false;
}
public SurfaceHolder getSurfaceHolder() {
return mSurfaceHolder;
}
public void onResume() {
if (hasSurface) {
// 当activity暂停,但是并未停止的时候,surface仍然存在,所以 surfaceCreated()
// 并不会调用,需要在此处初始化摄像头
openCamera();
}
}
public void onPause() {
closeCamera();
}
/**
* 打开摄像头
*/
private void openCamera() {
if (mSurfaceHolder == null) {
Logs.e(TAG, "SurfaceHolder is null.");
return;
}
if (mCameraManager.isOpen()) {
Logs.w(TAG, "Camera is opened!");
return;
}
mCameraManager.openCamera();
}
/**
* 关闭摄像头
*/
private void closeCamera() {
mCameraManager.releaseCamera();
}
private String getString(int resId) {
return getResources().getString(resId);
}
private void setAspectRatio(int width, int height) {
if (width < 0 || height < 0) {
throw new IllegalArgumentException("Size cannot be negative.");
}
mRatioWidth = width;
mRatioHeight = height;
requestLayout();
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int width = MeasureSpec.getSize(widthMeasureSpec);
int height = MeasureSpec.getSize(heightMeasureSpec);
if (0 == mRatioWidth || 0 == mRatioHeight) {
setMeasuredDimension(width, height);
} else {
if (width < height * mRatioWidth / mRatioHeight) {
setMeasuredDimension(width, width * mRatioHeight / mRatioWidth);
} else {
setMeasuredDimension(height * mRatioWidth / mRatioHeight, height);
}
}
}
@Override
public void onOpen() {
mCameraManager.startPreview(getSurfaceHolder());
}
@Override
public void onOpenError(int error, String msg) {
}
@Override
public void onPreview(int previewWidth, int previewHeight) {
if (mSurfaceWidth > mSurfaceHeight) {
setAspectRatio(previewWidth, previewHeight);
} else {
setAspectRatio(previewHeight, previewWidth);
}
}
@Override
public void onPreviewError(int error, String msg) {
}
@Override
public void onClose() {
}
}
1.Camera操作时机
- 在
surfaceCreated
回调中打开Camera,在surfaceDestroyed
中关闭摄像头
,这基本是所有Camera操作的常识,我们代码中也展示了同样的方式。 - 但这还不够完美,我们必须将Camera的操作和Activity的生命周期绑定,在onResume中也打开一次摄像头,在onPause中关闭一次摄像头,确保SurfaceHolder不可用以及Activity不在前台时正确关闭Camera
2.SurfaceView大小计算时机
在操作摄像头之前我们并不知道预览的尺寸,只能设置一个我们想要的尺寸,最终预览尺寸只能等到openCamera之后,CameraCallback中提供了回调接口onPreview
在此我们可以设置SurfaceView的大小比例来适配Camera预览尺寸,避免预览页面拉升或压缩。
三.最后
本文介绍了Camera+SurfaceView的基本操作及关键代码,但是你如果看github中代码会发现和文中出入很大。其原因在于文章我想用一种简单的方式让没有做过自定义Camera的人也能明白。而github中的项目我已经进行了多次重构、抽象。其中包括对Camera的抽象,定义ICameraManager接口。对预览的View的重构,定义BaseCameraView接口,以及预览视图的众多超类。
lib-camera库包结构如下:
包 | 说明 |
---|---|
camera | camera相关操作功能包,包括Camera和Camera2。以及各种预览视图 |
encoder | MediaCdoec录制视频相关,包括对ByteBuffer和Surface的录制 |
gles | opengles操作相关 |
permission | 权限相关 |
util | 工具类 |
每个包都可独立使用做到最低的耦合,方便白嫖直接拿来使用
github地址:https://github.com/xiaozhi003/AndroidCamera
参考:
- https://github.com/afei-cn/CameraDemo
- https://github.com/saki4510t/UVCCamera
- https://github.com/google/grafika