Android相机基础基于camera2API
前言
最近,在使用Android做一个照相机的开发。因为不能使用系统提供的相机应用,所以只能自己写一个。Android以前提供相机的api叫camera,不过在level 21被Google抛弃了。网上的教程,还有很多都是使用camera的,为了好好学习一下camera2,就去扒了Google提供的官方示例。下面给一个github的连接,可以找到全部的源码。
源码分析
下述的例子主要提供的功能是:相机预览、拍照、照片保存。具体如下:进入该应用后,可以看到相机的预览画面,提供一个按钮用于拍照,拍完照片后照片会保存在SD卡的根目录下。
架构分析
首先来了解一下camera2的整体结构:
如上所示,整个camera2由一个CameraManager来进行统一管理,通过Context的getSystemService方法可以实例化CameraManager,然后该类主要通过三个类来对Camera进行操作。下面分别介绍一下:
- CameraDevice:描述一个照相机设备,一个Android设备可能会有多个摄像头,通过CameraId可以进行区别。它最主要有一个相机状态的回调函数,当下达打开相机的命令后,若相机正确的打开便会回调该函数。
- CameraCharacteristic:某个照相机设备的具体参数。本例主要用到它提供的输出格式(即输出数据的格式)。
- CameraCaptureSession:相机捕获会话,通过这个类可以和相机进行对话(预览还是单张拍照还是录像等)。这里有两个回调函数,捕获状态的回调,和捕获数据的回调(后文会有详述)。
上图左上部分所示的时Android设备和camera设备的通信情况,两者之间通过pipeline(管道)进行数据交换。当需要尽心不同的操作时,将CameraCaptureRequest通过管道传给camera,接收到请求后,camera做出相应的反应,将获取到得数据CmaeraMetadata通过管道传回给Android设备。
注意事项
本例,需要使用比较多的权限,请参看源码AndroidManifest.xml。
Android设备的屏幕方向,与摄像头的原始方向并不一致,需要做方向转换。一般而言,当Android设备横着放时,与摄像头的方向是一致的。
为了避免照片失真(照片被拉长或者压扁),需要保证预览的长宽比例、照片的长宽比例和相机输出格式的长宽比例三者保持一致。
代码流转
本例当中,用一个activity承载一个fragment。所有的代码都写在fragment里面,重写了fragment的几个生命周期函数:
- onCreateView:加载fragment的布局文件;
- onViewCreated:实例化布局控件;
- onActivityCreated:在SD卡的目录下建立jpg文件等待待将拍到的照片写进去;
- onResume:开始照相机线程,执行一些逻辑判断;
- onPause:关闭照相机,停止照相机线程;
正常来说,代码的整体流程如图2所示,activity将需要的fragment加载进来后,开始加载显示预览的控件texture,当控件加载完毕会执行一个回调函数onSurfaceTextureAvailable()
,在这个回调函数里面,打开摄像头(即执行openCamera()
)。
openCamera()
里面,首先要配置相机的输出,预览图像和拍照的图片要作不同的处理,然后根据当前的设备屏幕环境,判断是否需要进行数据的转换,最后通过cameraManager打开摄像头(调用cameraManager.openCamera()
方法)。
更详细的方法请看后面的源码,图2当中,中间是判断屏幕方向的逻辑,右边是自动选择最合适的显示逻辑。
源码
// 代码比较长,请耐心查看,注释可能有不正确的地方,请提出
// 注意,下面代码为了配合我的使用,已经去掉了按钮,但是拍照的方法仍然保留,通过调用方法可以完成拍照
package com.eric_lai.weeding_robot.fragment;
/**
* Created by ERIC_LAI on 16/3/18.
*/
import android.app.Activity;
import android.app.AlertDialog;
import android.app.Dialog;
import android.app.DialogFragment;
import android.app.Fragment;
import android.content.Context;
import android.content.DialogInterface;
import android.content.res.Configuration;
import android.graphics.ImageFormat;
import android.graphics.Matrix;
import android.graphics.Point;
import android.graphics.RectF;
import android.graphics.SurfaceTexture;
import android.hardware.camera2.CameraAccessException;
import android.hardware.camera2.CameraCaptureSession;
import android.hardware.camera2.CameraCharacteristics;
import android.hardware.camera2.CameraDevice;
import android.hardware.camera2.CameraManager;
import android.hardware.camera2.CameraMetadata;
import android.hardware.camera2.CaptureRequest;
import android.hardware.camera2.CaptureResult;
import android.hardware.camera2.TotalCaptureResult;
import android.hardware.camera2.params.StreamConfigurationMap;
import android.media.Image;
import android.media.ImageReader;
import android.os.Bundle;
import android.os.Handler;
import android.os.HandlerThread;
import android.support.annotation.NonNull;
import android.util.Log;
import android.util.Size;
import android.util.SparseIntArray;
import android.view.LayoutInflater;
import android.view.Surface;
import android.view.TextureView;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Toast;
import com.eric_lai.weeding_robot.R;
import com.eric_lai.weeding_robot.view.AutoFitTextureView;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.concurrent.Semaphore;
import java.util.concurrent.TimeUnit;
public class CameraFragment extends Fragment {
/**
* Conversion from screen rotation to JPEG orientation.
*/
private static final SparseIntArray ORIENTATIONS = new SparseIntArray();
private static final String FRAGMENT_DIALOG = "dialog";
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);
}
/**
* 调试用TAG
*/
private static final String TAG = "CameraFragment";
/**
* 相机状态:
* 0: 预览
* 1: 等待上锁(拍照片前将预览锁上保证图像不在变化)
* 2: 等待预拍照(对焦, 曝光等操作)
* 3: 等待非预拍照(闪光灯等操作)
* 4: 已经获取照片
*/
private static final int STATE_PREVIEW = 0;
private static final int STATE_WAITING_LOCK = 1;
private static final int STATE_WAITING_PRECAPTURE = 2;
private static final int STATE_WAITING_NON_PRECAPTURE = 3;
private static final int STATE_PICTURE_TAKEN = 4;
/**
* Camera2 API提供的最大预览宽度和高度
*/
private static final int MAX_PREVIEW_WIDTH = 1920;
private static final int MAX_PREVIEW_HEIGHT = 1080;
/**
* SurfaceTexture监听器
*/
private final TextureView.SurfaceTextureListener mSurfaceTextureListener
= new TextureView.SurfaceTextureListener() {
@Override
public void onSurfaceTextureAvailable(SurfaceTexture texture, int width, int height) {
// SurfaceTexture就绪后回调执行打开相机操作
openCamera(width, height);
}
@Override
public void onSurfaceTextureSizeChanged(SurfaceTexture texture, int width, int height) {
// 预览方向改变时, 执行转换操作
configureTransform(width, height);
}
@Override
public boolean onSurfaceTextureDestroyed(SurfaceTexture texture) {
return true;
}
@Override
public void onSurfaceTextureUpdated(SurfaceTexture texture) {
}
};
/**
* 正在使用的相机id
*/
private String mCameraId;
/**
* 预览使用的自定义TextureView控件
*/
private AutoFitTextureView mTextureView;
/**
* 预览用的获取会话
*/
private CameraCaptureSession mCaptureSession;
/**
* 正在使用的相机
*/
private CameraDevice mCameraDevice;
/**
* 预览数据的尺寸
*/
private Size mPreviewSize;
/**
* 相机状态改变的回调函数
*/
private final CameraDevice.StateCallback mStateCallback = new CameraDevice.StateCallback() {
@Override
public void onOpened(@NonNull CameraDevice cameraDevice) {
// 当相机打开执行以下操作:
// 1. 释放访问许可
// 2. 将正在使用的相机指向将打开的相机
// 3. 创建相机预览会话
mCameraOpenCloseLock.release();
mCameraDevice = cameraDevice;
createCameraPreviewSession();
}
@Override
public void onDisconnected(@NonNull CameraDevice cameraDevice) {
// 当相机失去连接时执行以下操作:
// 1. 释放访问许可
// 2. 关闭相机
// 3. 将正在使用的相机指向null
mCameraOpenCloseLock.release();
cameraDevice.close();
mCameraDevice = null;
}
@Override
public void onError(@NonNull CameraDevice cameraDevice, int error) {
// 当相机发生错误时执行以下操作:
// 1. 释放访问许可
// 2. 关闭相机
// 3, 将正在使用的相机指向null
// 4. 获取当前的活动, 并结束它
mCameraOpenCloseLock.release();
cameraDevice.close();
mCameraDevice = null;
Activity activity = getActivity();
if (null != activity) {
activity.finish();
}
}
};
/**
* 处理拍照等工作的子线程
*/
private HandlerThread mBackgroundThread;
/**
* 上面定义的子线程的处理器
*/
private Handler mBackgroundHandler;
/**
* 静止页面捕获(拍照)处理器
*/
private ImageReader mImageReader;
/**
* 输出照片的文件
*/
private File mFile;
/**
* ImageReader的回调函数, 其中的onImageAvailable会在照片准备好可以被保存时调用
*/
private final ImageReader.OnImageAvailableListener mOnImageAvailableListener
= new ImageReader.OnImageAvailableListener() {
@Override
public void onImageAvailable(ImageReader reader) {
mBackgroundHandler.post(new ImageSaver(reader.acquireNextImage(), mFile));
}
};
/**
* 预览请求构建器, 用来构建"预览请求"(下面定义的)通过pipeline发送到Camera device
*/
private CaptureRequest.Builder mPreviewRequestBuilder;
/**
* 预览请求, 由上面的构建器构建出来
*/
private CaptureRequest mPreviewRequest;
/**
* 当前的相机状态, 这里初始化为预览, 因为刚载入这个fragment时应显示预览
*/
private int mState = STATE_PREVIEW;
/**
* 信号量控制器, 防止相机没有关闭时退出本应用(若没有关闭就退出, 会造成其他应用无法调用相机)
* 当某处获得这个许可时, 其他需要许可才能执行的代码需要等待许可被释放才能获取
*/
private Semaphore mCameraOpenCloseLock = new Semaphore(1);
/**
* 捕获会话回调函数
*
*/
private CameraCaptureSession.CaptureCallback mCaptureCallback
= new CameraCaptureSession.CaptureCallback() {
@Override
public void onCaptureProgressed(@NonNull CameraCaptureSession session,
@NonNull CaptureRequest request,
@NonNull CaptureResult partialResult) {
process(partialResult);
}
@Override