前言
android.hardware.camera2包提供了与设备关联的相机的接口。它替代了之前的相机接口Camera类。博主阅读了android官方的camera2的demo,发现camera2的使用较Camera要复杂一些,但功能更强大。纸上得来终觉浅,于是自己也写了个demo,实现了自动对焦拍照。
功能虽然简单,但是代码一大串,并且在实现的过程中,也发现了一些值得注意的问题。
思路
借用了一下
http://wiki.jikexueyuan.com/project/android-actual-combat-skills/android-hardware-camera2-operating-guide.html里的图,下图简单展示了camera2的工作流程。
我的demo中,使用TextureView作为camera2的展示预览的视图,实现流程大致如下:
获取TextureView对象,为该对象设置SurfaceTextureListener;
SurfaceTexture有效时,创建预览处理线程,为打开相机做准备,并打开相机;
在相机成功打开的回调中,为打开预览会话做准备,并打开预览会话;
在预览会话成功创建的回调中,创建请求,并发出连续请求,开始预览会话。
设置Button的响应事件,当相机成功打开后,点击Button拍照。
实现
1 获取TextureView对象,在SurfaceTexture有效后,创建相机线程并打开相机
在活动的onCreate()中获取TextureView对象,并设置监听器;
mTextureView=(TextureView) mSelf.findViewById(R.id.camera_view);
mTextureView.setSurfaceTextureListener(mSurfaceTextureListener);
mSurfaceTextureListener随着活动一起创建,其实现如下:
//TextureView's SurfaceTexture Listener
private TextureView.SurfaceTextureListener mSurfaceTextureListener=new TextureView.SurfaceTextureListener() {
@Override
public void onSurfaceTextureAvailable(SurfaceTexture surface, int width, int height) {
Log.d(TAG, "onSurfaceTextureAvailable:");
initCameraThread();
openCamera();
}
@Override
public void onSurfaceTextureSizeChanged(SurfaceTexture surface, int width, int height) {
Log.d(TAG, "onSurfaceTextureSizeChanged:");
}
@Override
public boolean onSurfaceTextureDestroyed(SurfaceTexture surface) {
Log.d(TAG, "onSurfaceTextureDestroyed:");
closeCamera();
stopCameraThread();
return true;
}
@Override
public void onSurfaceTextureUpdated(SurfaceTexture surface) {
}
};
一旦SurfaceTexture有效,则立刻创建相机线程,并打开相机,一旦SurfaceTexture失效,便关闭相机,且结束相机线程。
创建相机线程方法如下:
private void initCameraThread(){
if (mCamThread==null){
Log.d(TAG, "initCameraThread: init camera thread ");
mCamThread=new HandlerThread("CameraThread");
mCamThread.start();
mCamHandler=new Handler(mCamThread.getLooper());
return;
}
if (mCamThread.isAlive()){
Log.d(TAG, "initCameraThread: camera thread is alive.");
return;
}else{
Log.d(TAG, "initCameraThread: camera thread isn't null , and start it now.");
mCamThread.start();
}
}
打开相机的代码如下:
private void openCamera(){
prepareCameraOpen();
Log.d(TAG, "openCamera: check camera permission");
if ( ContextCompat.checkSelfPermission(mSelf, Manifest.permission.CAMERA)!= PackageManager.PERMISSION_GRANTED ){
ActivityCompat.requestPermissions(mSelf,new String[]{Manifest.permission.CAMERA},CAMERA_PERMISSION);
return;
}
Log.d(TAG, "openCamera: check write_external permission");
if ( ContextCompat.checkSelfPermission(mSelf, Manifest.permission.WRITE_EXTERNAL_STORAGE)!= PackageManager.PERMISSION_GRANTED ){
ActivityCompat.requestPermissions(mSelf,new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE},WRITE_EXTERNAL);
return;
}
Log.d(TAG, "openCamera: check read_external permission");
if ( ContextCompat.checkSelfPermission(mSelf, Manifest.permission.READ_EXTERNAL_STORAGE)!= PackageManager.PERMISSION_GRANTED ){
ActivityCompat.requestPermissions(mSelf,new String[]{Manifest.permission.READ_EXTERNAL_STORAGE},READ_EXTERNAL);
return;
}
Log.d(TAG, "openCamera:all permissions has checked.");
try{
mCamManager.openCamera(mCamId,mCamOpenStateCallback,mCamHandler);
}catch (Exception e){
e.printStackTrace();
}
}
首先调用prepareCameraOpen(),为打开相机做准备;之后是验证危险权限:使用相机和外部存储的读写。最终调用CameraManager.openCamera()打开相机。
prepareCameraOpen()如下:
private void prepareCameraOpen(){
mCamManager=(CameraManager) getSystemService(Context.CAMERA_SERVICE);
StreamConfigurationMap map=null;
try{
for (String cameraId : mCamManager.getCameraIdList()) {
CameraCharacteristics characteristics= mCamManager.getCameraCharacteristics(cameraId);
Integer facing = characteristics.get(CameraCharacteristics.LENS_FACING);
if (facing == null || facing !=CameraCharacteristics.LENS_FACING_BACK) {
continue;
}
mCamId=cameraId;
map = characteristics.get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP);
//获取默认配置文件中JPG的最大尺寸给ImageReader
Size largest = Collections.max(Arrays.asList(map.getOutputSizes(ImageFormat.JPEG)), new CompareSizesByArea());
initReaderToGetImgData(largest);
return;
}
}catch (Exception e){
e.printStackTrace();
}
}
首先从系统服务中获取CameraManager对象,CameraManager能获取设备上可使用的摄像头的id;
根据Id返回某个摄像头对应的CameraCharacteristics对象,该对象包含了某个摄像头的固定信息和属性。当LENS_FACING这个key对应的值为LENS_FACING_BACK(即后视摄像头)时,获取该摄像头的id和SCALER_STREAM_CONFIGURATION_MAP对象(获取该摄像头一些有效的配置)。
通过SteamConfigurationMap.getOutputSizes获取JPEG格式所有的有效尺寸,并通过自定义的排序获得最大尺寸。
传入获取的最大尺寸,传给initReaderTogetImgData()方法,初始化获取图像数据的ImageReader对象。
initReaderTogetImgData()方法如下:
private void initReaderToGetImgData(Size res){
//ImageReader最后可以获取7帧的数据
mImgReader=ImageReader.newInstance(res.getWidth(),res.getHeight(), ImageFormat.JPEG,2);
mImgReader.setOnImageAvailableListener(mImgAvailableListener,mCamHandler);
}
ImageReader的监听器,在成果获取拍照得到的数据后会回调。至于怎么实现之后再将。
上述即prepareOpenCamera()中的实现。
思路回到openCamera()中,当prepareOpenCamera()执行后,将依次检验权限。用户授予权限后,再重新调用openCamera()。
最后调用CameraManager.openCamera( cameraId , 成功打开camera后的回调 , 线程的Handler )。线程handler即为绑定在CameraThread中的Handler。回调对象如下所示:
private CameraDevice.StateCallback mCamOpenStateCallback=new CameraDevice.StateCallback() {
@Override
public void onOpened(CameraDevice camera) {
Log.d(TAG, "onOpened: camera is openned.");
mCurCam=camera;
mIsCamOpen=true;
createCameraPreviewSession();
}
@Override
public void onDisconnected(CameraDevice camera) {
Log.d(TAG, "onDisconnected: camera losed connection.");
mCurCam=camera;
mCurCam.close();
mCurCam=null;
}
@Override
public void onError(CameraDevice camera, int error) {
Log.d(TAG, "onError: camera-open error");
mCurCam=camera;
mCurCam.close();
mCurCam=null;
}
};
当CameraManager成功open对应相机后,该回调会回调onOpened()方法。于是在onOpened()中处理接下来的工作。
2 打开相机预览会话
在onOpened()回调中,调用createCameraPreviewSession(),企图打开预览会话。
private void createCameraPreviewSession(){
try {
SurfaceTexture texture = mTextureView.getSurfaceTexture();
assert texture != null;
texture.setDefaultBufferSize(mTextureView.getWidth(),mTextureView.getHeight());
Surface surface = new Surface(texture);
//设置为相机预览模式
mPreviewBuilder = mCurCam.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW);
mPreviewBuilder.addTarget(surface);
//预览数据输出到previewSurface和imageReader
mCurCam.createCaptureSession(Arrays.asList(surface,mImgReader.getSurface()),
mOpenCamPreviewSessionStateCallback,mCamHandler);
}catch (Exception e){
e.printStackTrace();
Log.d(TAG, "createCameraPreviewSession: error occured when creating capture session.");
return;
}
}
前几步通过TextureView创建了Surface对象,该对象将作为预览数据的传输目标,展示相机的预览情况。
以TEMPLATE_PREVIEW作为模板创建了用于预览的请求CaptureRequest对象的Builder,该Builder之后将会向会话发送连续的请求,以到达预览的效果。注意将Surface对象添加给CapureRequest.Builder作为数据的传输目标。
最后调用CameraDevice.createCaptureSession()方法。第一个参数类型为List<Surface>,可提供一个或多个Surface作为CaptureRequest的关联目标;第二个参数为回调CameraCaptureSesson.StateCallback对象,第三个参数为用于该Session指定执行线程的Handler。
当会话成功创建后,会调用StateCallback对象的相关方法。
3 在会话创建的回调中,发送连续请求,开启相机的预览模式
CameraCaptureSesson.StateCallback定义如下:
private CameraCaptureSession.StateCallback mOpenCamPreviewSessionStateCallback=new CameraCaptureSession.StateCallback() {
@Override
public void onConfigured(CameraCaptureSession session) {
Log.d(TAG, "onConfigured:create preview session successfully");
mSession=session;
startPreview();
}
@Override
public void onConfigureFailed(CameraCaptureSession session) {
Log.d(TAG, "onConfigureFailed: preview session failed to init.");
mSession=session;
mSession=null;
}
};
可以看到,在会话成功创建后,为mSession对象赋值,并调用startPreview()。
private void startPreview(){
try {
//调整镜头对焦,提供连续清晰的图片
mPreviewBuilder.set(CaptureRequest.CONTROL_AF_MODE, CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_PICTURE);
//自动曝光
mPreviewBuilder.set(CaptureRequest.CONTROL_AE_MODE, CaptureRequest.CONTROL_AE_MODE_ON_AUTO_FLASH);
//设置反复捕获数据的请求,这样预览界面就会一直有数据显示
mState=STATE_PREVIEW;
mPreviewRequest=mPreviewBuilder.build();
mSession.setRepeatingRequest(mPreviewRequest, mCamPreviewCallback, mCamHandler);
} catch (Exception e) {
e.printStackTrace();
}
}
mPreviewBuilder在createCameraPreviewSession()中已创建,并授予了targer,现在继续设置请求属性,并最后创建CaptureRequest对象。
最后调用CameraCaputureSession.setRepeatingRequest()方法,向会发发起连续的请求。请求的内容即为mPreviewRequest对象设置的属性。第二个参数为图像被捕捉后的回调,第三个为执行线程的Handler。
注意,这里的mCamPreviewCallback类型为CameraCaptureSession.CaptureCallback,是demo中第一个出现的CameraCaptureSession.CaptureCallback对象,一共有2个该类型的对象,该对象用于处理预览的捕捉。
private CameraCaptureSession.CaptureCallback mCamPreviewCallback=new CameraCaptureSession.CaptureCallback(){
@Override
public void onCaptureCompleted(CameraCaptureSession session, CaptureRequest request, TotalCaptureResult result) {
super.onCaptureCompleted(session, request, result);
mSession=session;
process(result);
}
};
当有图像捕获时,直接调用process()方法。
4 预览中捕获图像后的处理逻辑
现在看一下process()方法:
private void process(TotalCaptureResult result){
switch (mState){
case STATE_PREVIEW:{
//do nothing
break;
}
case STATE_WAITING_LOCK:{
Integer afState = result.get(CaptureResult.CONTROL_AF_STATE);
if (afState == null) {
captureImage();
}else if (CaptureResult.CONTROL_AF_STATE_FOCUSED_LOCKED == afState || CaptureResult.CONTROL_AF_STATE_NOT_FOCUSED_LOCKED == afState){
Integer aeState = result.get(CaptureResult.CONTROL_AE_STATE);
if (aeState == null || aeState == CaptureResult.CONTROL_AE_STATE_CONVERGED) {
mState = STATE_PICTURE_TAKEN;
captureImage();
}else {
runPrecaptureSequence();
}
}
break;
}
case STATE_WAITING_PRECAPTURE:{
Integer aeState = result.get(CaptureResult.CONTROL_AE_STATE);
if (aeState == null || aeState == CaptureResult.CONTROL_AE_STATE_PRECAPTURE ||
aeState == CaptureRequest.CONTROL_AE_STATE_FLASH_REQUIRED) {
mState = STATE_WAITING_NON_PRECAPTURE;
}
break;
}
case STATE_WAITING_NON_PRECAPTURE:{
Integer aeState = result.get(CaptureResult.CONTROL_AE_STATE);
if (aeState == null || aeState != CaptureResult.CONTROL_AE_STATE_PRECAPTURE) {
mState = STATE_PICTURE_TAKEN;
captureImage();
}
break;
}
default:
break;
}
}
STATE_PREVIEW,表示图像预览,不需要我们做任何事情;
STATE_WAITING_LOCK,当你按下快门后,若相机之前没有对上焦,应该先对焦后再拍照。STATE_WAITING_LOCK这个case中,首先获取CaptureResult中key CONTROL_AF_STATE的值,即自动对焦的当前状态(完成到哪个步骤了)。若没有值,则调用拍照方法,若对焦成功或没有对焦成功,则执行之后的逻辑。
STATE_WAITING_PRECAPTURE是runPrecaptureSequence()中指定的状态,它表示在当前场景下,曝光不成功时,再在拍照前进行一次高质量的测光。
最后一个case表示高质量测光结束后,进行最终的拍照。
上述即为预览模式中,捕获照片后的处理逻辑。如果我们没有点击拍照键,上述process只会在STATE_PREVIEW中循环。
5 点击button进行拍照的处理逻辑
@Override
public void onClick(View v) {
switch (v.getId()){
case R.id.capture_btn:{
if (mIsCamOpen)
takePicture();
}
default:
break;
}
}
调用了takePicture()方法;
private void takePicture(){
lockFocus();
}
takePicture()中为lockFocus()。在快门生效前先对焦。
private void lockFocus(){
try {
// This is how to tell the camera to lock focus.
mPreviewBuilder.set(CaptureRequest.CONTROL_AF_TRIGGER,CameraMetadata.CONTROL_AF_TRIGGER_START);
// Tell #mCaptureCallback to wait for the lock.
mState = STATE_WAITING_LOCK;
mSession.capture(mPreviewBuilder.build(), mCamPreviewCallback, mCamHandler);
} catch (Exception e) {
e.printStackTrace();
}
}
在lockFocus中,将CaptureRequest的CONTROL_AF_TRIGGER属性设置为CONTROL_AF_TRIGGER_START,请求开启自动对焦。改变状态标签,并发送。
发送后,就进入process方法中STATE_WAITING_LOCK这个case下的程序逻辑。
若结果为null,则进行拍照captureImage();
private void captureImage(){
try {
CaptureRequest.Builder captureBuilder =mCurCam.createCaptureRequest(CameraDevice.TEMPLATE_STILL_CAPTURE);
captureBuilder.addTarget(mImgReader.getSurface());
captureBuilder.set(CaptureRequest.CONTROL_AF_MODE, CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_PICTURE);
captureBuilder.set(CaptureRequest.CONTROL_AE_MODE, CaptureRequest.CONTROL_AE_MODE_ON_AUTO_FLASH);
int rotation=mSelf.getWindowManager().getDefaultDisplay().getRotation();
//JPEG_ORIENTATION相对于传感器的方向,相片需要看上去竖直而应该顺时针旋转的角度;
captureBuilder.set(CaptureRequest.JPEG_ORIENTATION, getOrientation(rotation));
mSession.stopRepeating();
mSession.abortCaptures();
mSession.capture(captureBuilder.build(),mCamCaptureCallback,null);
}catch (Exception e){
e.printStackTrace();
}
}
该方法首先创建了以STILL_CAPTURE为模板的CaptureRequest对象,这与mPreviewBuilder对象功能不同。前者用来拍照,后者用于预览。
将ImageReader中的Surface作为数据输出目标,设置对焦模式为连续对焦,曝光模式为自动曝光。
获取rotation对象,该rotation对象指设备从默认的自然方向到当前方向的角度。若手机为竖屏手机,拍照时为横屏,那么该值自然为90°或270°,90°和270°的效果相同。JPEG_ORIENTATION表示,拍照后得到的照片相对于相机来说需要顺时针旋转多少角度才看上去竖直。若竖屏手机竖直拍摄,那么该值为0,若竖屏手机横向拍摄,那么该值为90°或270°。把rotation值,传入到getOrientation()方法中,就得到了这个旋转值。注意,在这里就决定了,拍摄后得到的照片是竖直存储的。接下来看getOrientation方法。
private int getOrientation(int rotation){
if (mSensorOrientation<0)
getSensorOrientation();
return (ORIENTATIONS.get(rotation) + mSensorOrientation + 270) % 360;
}
这里有个mSensorOrientation对象指若像片要在屏幕自然方向上竖直需要顺时针旋转的角度。通过getSensorOrientation对象为其赋值:
private void getSensorOrientation(){
try {
CameraCharacteristics characteristics=mCamManager.getCameraCharacteristics(mCamId);
mSensorOrientation = characteristics.get(CameraCharacteristics.SENSOR_ORIENTATION);
}catch (Exception e){
e.printStackTrace();
}
}
最后通过下述得到返回值;
(ORIENTATIONS.get(rotation) + mSensorOrientation + 270) % 360;
ORIENTATIONS的定义如下:
private static final SparseIntArray ORIENTATIONS = new SparseIntArray();
static {
ORIENTATIONS.append(Surface.ROTATION_0, 90);
ORIENTATIONS.append(Surface.ROTATION_90, 0);
ORIENTATIONS.append(Surface.ROTATION_180, 270);
ORIENTATIONS.append(Surface.ROTATION_270, 180);
}
通过上述逻辑,将我们拍到的照片以竖直的方向存储。
最后停止preview Session中不停的发送请求(即mPreviewBuilder创造的Request),放弃当前所有连续的捕获。最后在Session上发出捕获单张照片的请求。
注意,这里的第二个参数不再是之前的Callback了,它是本应用中2个CameraCaptureSession.CaptureCallback对象中的另一个,其定义如下:
private CameraCaptureSession.CaptureCallback mCamCaptureCallback=new CameraCaptureSession.CaptureCallback() {
@Override
public void onCaptureCompleted(@NonNull CameraCaptureSession session,@NonNull CaptureRequest request,@NonNull TotalCaptureResult result) {
Toast.makeText(mSelf,"capture image",Toast.LENGTH_SHORT).show();
unlockFocus();
}
};
当单张照片捕获后,调用unlockFocus方法;
private void unlockFocus(){
try {
// Reset the auto-focus trigger
mPreviewBuilder.set(CaptureRequest.CONTROL_AF_TRIGGER, CameraMetadata.CONTROL_AF_TRIGGER_CANCEL);
//setAutoFlash(mPreviewRequestBuilder);
mSession.capture(mPreviewBuilder.build(), mCamPreviewCallback, mCamHandler);
// After this, the camera will go back to the normal state of preview.
mState = STATE_PREVIEW;
mSession.setRepeatingRequest(mPreviewRequest, mCamPreviewCallback, mCamHandler);
} catch (Exception e) {
e.printStackTrace();
}
}
在unlockFocus作用是关闭对焦,并重新发起连续的捕获请求,让相机再次进入预览模式。
6 像片的存储
在captureImage方法中,我们将CaputureRequest.Builder的addTarger方法设置了值为mImageReader.getSurface()。当捕获单张像片后,数据将会被传送到ImageReader中。此时,会由ImageReader的监听器监听到此次操作。ImageReader的监听器定义如下: private ImageReader.OnImageAvailableListener mImgAvailableListener=new ImageReader.OnImageAvailableListener(){
@Override
public void onImageAvailable(ImageReader reader) {
Log.d(TAG, "onImageAvailable: capture still image.");
Image img=mImgReader.acquireLatestImage();
if (img.getFormat()==ImageFormat.JPEG && img!=null){
storeImageInSdCard(img);
}else{
Log.d(TAG, "onImageAvailable: image format isn't JPEG.");
}
// release data for space
img.close();
}
};
照片捕获且传输后会调用其onImageAvailable方法。调用Image.acquireLatestImage()来获取最近一张捕获(可有多张捕获的缓存),判断类型,并调用storeImageInSdCard方法。最后调用Image.close()方法清除缓存,不然缓存到达ImageReader的设置数量后,会无法接收照片。
storeImageInSdCard方法如下;
private void storeImageInSdCard(Image img){
Bitmap imgBitmap=unpackImage2Bitmap(img);
String path=new String(Environment.getExternalStorageDirectory()+ File.separator+getPackageName());
File rootDir=new File(path);
if (!rootDir.exists())
rootDir.mkdirs();
String fileName = getImageNameByTime();
File imgFile=new File(rootDir,fileName+".jpg");
try{
FileOutputStream output=new FileOutputStream(imgFile);
imgBitmap.compress(Bitmap.CompressFormat.JPEG,100,output);
output.flush();
output.close();
}catch (Exception e){
e.printStackTrace();
}
}
首先会调用unpackImage2Bitmap,将Image转换为Bitmap。接着创建并获取SD卡中存放照片的根目录。调用getImageNameByTime()根据当前时间为照片命名。最后将其压缩为jpeg格式存储在指定目录中。
unpackImage2Bitmap如下:
private Bitmap unpackImage2Bitmap(Image img){
ByteBuffer byteBuffer=img.getPlanes()[0].getBuffer();
byte[] imgBytes=new byte[byteBuffer.capacity()];
byteBuffer.get(imgBytes);
Bitmap imgBitmap= BitmapFactory.decodeByteArray(imgBytes,0,imgBytes.length);
return imgBitmap;
}
7 关闭相机,结束线程
当SurfaceTexture失效后即关闭相机,且结束线程。失效时机见TextureView的监听器。方法如下: private void closeCamera() {
try {
if (mIsCamOpen){
mIsCamOpen=false;
if (null != mSession) {
mSession.close();
mSession = null;
}
if (null != mCurCam) {
mCurCam.close();
mCurCam = null;
}
if (null != mImgReader) {
mImgReader.close();
mImgReader = null;
}
Log.d(TAG, "closeCamera:");
}
} catch ( Exception e) {
e.printStackTrace();
}
}
结束线程;
private void stopCameraThread(){
if (mCamThread!=null){
mCamThread.quitSafely();
try {
mCamThread.join();
mCamThread=null;
mCamHandler=null;
Log.d(TAG, "stopCameraThread:");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
结语
经过上述步骤,完成了camera2拍摄照片的简单功能。可以看到,虽然功能简单,但是代码却一大串。但是camera2对相机的管理是非常精确的,你可以通过camera2实现非常复杂、功能精致的相机。这就需要大家多多交流啦~~~~~
完整demo可见我的github:
https://github.com/lyzirving/androidcamera2demo.git