整理自
https://developer.android.google.cn/training/camera/cameradirect.html
https://developer.android.google.cn/guide/topics/media/camera.html#custom-camera
因为camera2需要Android 5.0(API 21),尚未有兼容包出现,所以5.0以下还是需要使用deprecated的Camera API
自定义相机的使用一般可以分为几个步骤:
1. 检测相机并要求使用
2. 创建继承SurfaceView和实现SurfaceHolder接口的Preview类
3. 创建预览布局
4. 设置拍照监听
5. 拍照以及保存图片
6. 释放相机
注意,相机使用完毕必须调用Camera.release()方法释放,否则后续所有使用相机的请求都会失败而且可能导致应用关闭。
使用相机必须添加权限:(如果是使用系统相机应用可以不添加)
<uses-permission android:name="android.permission.CAMERA" />
同时必须声明使用特性,具体可以指定特定特性,该项是用在Google Play过滤不符要求的设备。可以设置required属性为必要或不必要
<uses-feature android:name="android.hardware.camera" android:required="false"/>
如果需要保存到sd卡
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
如果需要录像
<uses-permission android:name="android.permission.RECORD_AUDIO" />
如果需要使用GPS定位信息
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
...
<uses-feature android:name="android.hardware.location.gps" />
如果在清单文件中没有要求必须有相机,则需要在运行时检测是否有相机。Android 2.3(API 9)开始可以调用Camera.getNumberOfCameras()获取相机数目。
/** Check if this device has a camera */
private boolean checkCameraHardware(Context context) {
if (context.getPackageManager().hasSystemFeature(PackageManager.FEATURE_CAMERA)){
// this device has a camera
return true;
} else {
// no camera on this device
return false;
}
}
正如系统相机所做的,打开相机的推荐方式是在onCreate()中开启一条子线程打开,因为打开相机的操作可能阻塞主线程。甚至打开相机的操作可以推迟到onResume()中进行,这样可以达到代码复用和简化操作的效果。
如果相机正在被其他应用使用,则调用Camera.open()会抛出异常,所以需要添加try-catch语句。以下两段实例代码显示如何获取相机实例,永远记得必须检测并捕获异常。
从API 9开始,android支持多个摄像头,所以如果无参调用open()会默认使用后置摄像头。可以调用Camera.open(int)方法指定摄像头。
/** A safe way to get an instance of the Camera object. */
public static Camera getCameraInstance(){
Camera c = null;
try {
c = Camera.open(); // attempt to get a Camera instance
}
catch (Exception e){
// Camera is not available (in use or does not exist)
}
return c; // returns null if camera is unavailable
}
private boolean safeCameraOpen(int id) {
boolean qOpened = false;
try {
releaseCameraAndPreview();
mCamera = Camera.open(id);
qOpened = (mCamera != null);
} catch (Exception e) {
Log.e(getString(R.string.app_name), "failed to open Camera");
e.printStackTrace();
}
return qOpened;
}
private void releaseCameraAndPreview() {
mPreview.setCamera(null);
if (mCamera != null) {
mCamera.release();
mCamera = null;
}
}
可以调用Camera.getParameters()方法获取相机参数,从API 9开始可以调用 Camera.getCameraInfo()查看相机是前置或者后置,以及图像方向。
使用SurfaceView绘制摄像头传递的图像。要开始图像预览,还需要一个实现android.view.SurfaceHolder.Callback接口的preview类,这个接口用来接收摄像头传递的图像信息。在开始预览之前,必须要把创建的这个preview类传递给Camera对象。
【两页示例代码中使用两种方式创建这个preview类,一种是使用继承自SurfaceView的View,然后在xml或代码中把这个view添加到FrameLayout中。另一种是ViewGroup中包含SurfaceView,两种都实现了SurfaceHolder的回调接口】
SurfaceHolder回调接口是用来捕捉创建和销毁view的过程,方便图片处理。
方式一:
/** A basic Camera preview class */
public class CameraPreview extends SurfaceView implements SurfaceHolder.Callback {
private SurfaceHolder mHolder;
private Camera mCamera;
public CameraPreview(Context context, Camera camera) {
super(context);
mCamera = camera;
// Install a SurfaceHolder.Callback so we get notified when the
// underlying surface is created and destroyed.
mHolder = getHolder();
mHolder.addCallback(this);
// deprecated setting, but required on Android versions prior to 3.0
mHolder.setType(SurfaceHolder.SURFACE_TYPE_PUSH_BUFFERS);
}
public void surfaceCreated(SurfaceHolder holder) {
// The Surface has been created, now tell the camera where to draw the preview.
try {
mCamera.setPreviewDisplay(holder);
mCamera.startPreview();
} catch (IOException e) {
Log.d(TAG, "Error setting camera preview: " + e.getMessage());
}
}
public void surfaceDestroyed(SurfaceHolder holder) {
// empty. Take care of releasing the Camera preview in your activity.
}
public void surfaceChanged(SurfaceHolder holder, int format, int w, int h) {
// If your preview can change or rotate, take care of those events here.
// Make sure to stop the preview before resizing or reformatting it.
if (mHolder.getSurface() == null){
// preview surface does not exist
return;
}
// stop preview before making changes
try {
mCamera.stopPreview();
} catch (Exception e){
// ignore: tried to stop a non-existent preview
}
// set preview size and make any resize, rotate or
// reformatting changes here
// start preview with new settings
try {
mCamera.setPreviewDisplay(mHolder);
mCamera.startPreview();
} catch (Exception e){
Log.d(TAG, "Error starting camera preview: " + e.getMessage());
}
}
}
class Preview extends ViewGroup implements SurfaceHolder.Callback {
SurfaceView mSurfaceView;
SurfaceHolder mHolder;
Preview(Context context) {
super(context);
mSurfaceView = new SurfaceView(context);
addView(mSurfaceView);
// Install a SurfaceHolder.Callback so we get notified when the
// underlying surface is created and destroyed.
mHolder = mSurfaceView.getHolder();
mHolder.addCallback(this);
mHolder.setType(SurfaceHolder.SURFACE_TYPE_PUSH_BUFFERS);
}
...
}
必须先创建Camera实例,然后才创建对应的preview类。下面这个方法以上一步创建好的Camera实例为参数,以调用相机的开始预览方法结束。该方法可以用于用户改变相机的时候,具体实现会先停止预览,释放相机资源,然后重新开始预览。注意,也必须在Preview类的surfaceChanged()方法中重启预览
public void setCamera(Camera camera) {
if (mCamera == camera) { return; }
stopPreviewAndFreeCamera();
mCamera = camera;
if (mCamera != null) {
List localSizes = mCamera.getParameters().getSupportedPreviewSizes();
mSupportedPreviewSizes = localSizes;
requestLayout();
try {
mCamera.setPreviewDisplay(mHolder);
} catch (IOException e) {
e.printStackTrace();
}
// Important: Call startPreview() to start updating the preview
// surface. Preview must be started before you can take a picture.
mCamera.startPreview();
}
}
修改预览效果在surfaceChanged()中进行,注意预览尺寸不可随意设置,必须使用Camera实例对象.getParameters().getSupportedPreviewSizes()中的值。(Android 7.0(API 24)开始支持多窗口特性,所以即使调用了 setDisplayOrientation()之后也不能肯定预览的纵横比和Activity的一致。取决于窗口大小和纵横比,可能需要在垂直布局中使用letterbox布局来适配宽相机预览,或者反之。)
下面的实例代码实现了修改预览尺寸的效果,更多相机设置的修改可以参照系统相机的源码。
public void surfaceChanged(SurfaceHolder holder, int format, int w, int h) {
// Now that the size is known, set up the camera parameters and begin
// the preview.
Camera.Parameters parameters = mCamera.getParameters();
parameters.setPreviewSize(mPreviewSize.width, mPreviewSize.height);
requestLayout();
mCamera.setParameters(parameters);
// Important: Call startPreview() to start updating the preview surface.
// Preview must be started before you can take a picture.
mCamera.startPreview();
}
大多数拍照应用把预览方向设为水平,因为这是相机传感器的自然方向。
可以在清单文件中对应Activity标签添加android:screenOrientation=”landscape”来简化预览的渲染。但是也可以实现垂直方向预览,因为手机的方向记录在了EXIF header当中。2.2(API 8)开始可以调用setCameraDisplayOrientation()方法可以在不影响图像形成的情况下旋转预览方向。但是在API 14之前必须先停止预览,然后改变方向,最后重启预览。
可以在FrameLayout中添加自定义的预览类
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="horizontal"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
>
<FrameLayout
android:id="@+id/camera_preview"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:layout_weight="1"
/>
<Button
android:id="@+id/button_capture"
android:text="Capture"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
/>
LinearLayout>
public class CameraActivity extends Activity {
private Camera mCamera;
private CameraPreview mPreview;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
// Create an instance of Camera
mCamera = getCameraInstance();
// Create our Preview view and set it as the content of our activity.
mPreview = new CameraPreview(this, mCamera);
FrameLayout preview = (FrameLayout) findViewById(R.id.camera_preview);
preview.addView(mPreview);
}
}
在开始预览之后,可以调用 Camera.takePicture()拍照。
// Add a listener to the Capture button
Button captureButton = (Button) findViewById(id.button_capture);
captureButton.setOnClickListener(
new View.OnClickListener() {
@Override
public void onClick(View v) {
// get an image from the camera
mCamera.takePicture(null, null, mPicture);
}
}
);
可以传递进去实现Camera.PictureCallback和Camera.ShutterCallback的对象。想要以JPEG格式接收数据的话,就必须要实现Camera.PictureCallback接口接收数据以及保存到文件中,以下是实现PictureCallBack的示例代码。
private PictureCallback mPicture = new PictureCallback() {
@Override
public void onPictureTaken(byte[] data, Camera camera) {
File pictureFile = getOutputMediaFile(MEDIA_TYPE_IMAGE);
if (pictureFile == null){
Log.d(TAG, "Error creating media file, check storage permissions: " +
e.getMessage());
return;
}
try {
FileOutputStream fos = new FileOutputStream(pictureFile);
fos.write(data);
fos.close();
} catch (FileNotFoundException e) {
Log.d(TAG, "File not found: " + e.getMessage());
} catch (IOException e) {
Log.d(TAG, "Error accessing file: " + e.getMessage());
}
}
};
如果想要持续拍照,可以创建实现了onPreviewFrame()的 Camera.PreviewCallback。也可以拍下选择的特定帧图片,或者延迟调用takePicture()
拍完一张照片之后,必须要重启预览才能继续拍照。以下实例代码是在快门的点击事件中判断预览状态,从而确定是拍照还是重启预览。
@Override
public void onClick(View v) {
switch(mPreviewState) {
case K_STATE_FROZEN:
mCamera.startPreview();
mPreviewState = K_STATE_PREVIEW;
break;
default:
mCamera.takePicture( null, rawCallback, null);
mPreviewState = K_STATE_BUSY;
} // switch
shutterBtnConfig();
}
相机使用完毕必须记得释放,否则会导致下一次使用相机失败,包括本应用和其他应用都会。
恰当时机是在预览destroyed的时候进行,下面的surfaceDestroyed()是preview类实现的方法。Activity.onPause()中也要记得释放。
public class CameraActivity extends Activity {
private Camera mCamera;
private SurfaceView mPreview;
private MediaRecorder mMediaRecorder;
...
@Override
protected void onPause() {
super.onPause();
releaseMediaRecorder(); // if you are using MediaRecorder, release it first
releaseCamera(); // release the camera immediately on pause event
}
private void releaseMediaRecorder(){
if (mMediaRecorder != null) {
mMediaRecorder.reset(); // clear recorder configuration
mMediaRecorder.release(); // release the recorder object
mMediaRecorder = null;
mCamera.lock(); // lock camera for later use
}
}
private void releaseCamera(){
if (mCamera != null){
mCamera.release(); // release the camera for other applications
mCamera = null;
}
}
}
stopPreviewAndFreeCamera()也在上面的setCamera()中调用了,所以初始化相机的时候必须记得先停止预览。【好像实例代码surfaceDestroyed()中没有释放相机】
public void surfaceDestroyed(SurfaceHolder holder) {
// Surface will be destroyed when we return, so stop the preview.
if (mCamera != null) {
// Call stopPreview() to stop updating the preview surface.
mCamera.stopPreview();
}
}
/**
* When this function returns, mCamera will be null.
*/
private void stopPreviewAndFreeCamera() {
if (mCamera != null) {
// Call stopPreview() to stop updating the preview surface.
mCamera.stopPreview();
// Important: Call release() to release the camera for use by other
// applications. Applications should release the camera immediately
// during onPause() and re-open() it during onResume()).
mCamera.release();
mCamera = null;
}
}
图片和录像必须保存在sd卡中以节省系统空间以及方便在没有手机的情况下使用。作为一名开发者,只有两个标准路径可以考虑作为保存目录
- Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES):该方法返回一个标准的公开的保存图片录像的路径,一般是/mnt/sdcard/Pictures。所有应用都可以修改该路径下的文件。即使某应用被删除,该应用保存到该路径的媒体文件仍然保持不变。为了避免与原有文件混淆,可以在这个目录下创建一个本应用的子目录来保存媒体文件
- Context.getExternalFilesDir(Environment.DIRECTORY_PICTURES) :
该方法返回一个与本应用相联系的保存媒体文件的路径,一般是/mnt/sdcard/Android/data/应用包名/files/Pictures。如果你的应用被删除了,该路径保存的所有文件也会被删除。4.4以后其他应用无法获取或修改该路径的文件。
以下示例代码展示了如何创建一个File或Uri用来作为相机intent的参数或者作为自定义相机的一部分。
public static final int MEDIA_TYPE_IMAGE = 1;
public static final int MEDIA_TYPE_VIDEO = 2;
/** Create a file Uri for saving an image or video */
private static Uri getOutputMediaFileUri(int type){
return Uri.fromFile(getOutputMediaFile(type));
}
/** Create a File for saving an image or video */
private static File getOutputMediaFile(int type){
// To be safe, you should check that the SDCard is mounted
// using Environment.getExternalStorageState() before doing this.
File mediaStorageDir = new File(Environment.getExternalStoragePublicDirectory(
Environment.DIRECTORY_PICTURES), "MyCameraApp");
// This location works best if you want the created images to be shared
// between applications and persist after your app has been uninstalled.
// Create the storage directory if it does not exist
if (! mediaStorageDir.exists()){
if (! mediaStorageDir.mkdirs()){
Log.d("MyCameraApp", "failed to create directory");
return null;
}
}
// Create a media file name
String timeStamp = new SimpleDateFormat("yyyyMMdd_HHmmss").format(new Date());
File mediaFile;
if (type == MEDIA_TYPE_IMAGE){
mediaFile = new File(mediaStorageDir.getPath() + File.separator +
"IMG_"+ timeStamp + ".jpg");
} else if(type == MEDIA_TYPE_VIDEO) {
mediaFile = new File(mediaStorageDir.getPath() + File.separator +
"VID_"+ timeStamp + ".mp4");
} else {
return null;
}
return mediaFile;
}
大部分相机设定可以通过Camera.parameters获取并修改。Android支持一系列相机特性设置,具体可到原文查看。
有三个特性的处理比较复杂:
Metering and focus areas 测量和对焦
Face detection 脸部识别
Time lapse video 延时录像
因为设备限制,不是所有的设备都支持所有的相机特性。即使支持,也可能只是有限度支持,所以需要检测特性是否可用。检测可以通过以下代码实现。同时parameters也支持getSupported…(), is…Supported()或者getMax…()判断是否支持以及支持程度。
// get Camera parameters
Camera.Parameters params = mCamera.getParameters();
List<String> focusModes = params.getSupportedFocusModes();
if (focusModes.contains(Camera.Parameters.FOCUS_MODE_AUTO)) {
// Autofocus mode is supported
}
可以在清单文件中声明需要的相机特性,Google Play会确保应用只安装在支持设备上。
可以通过获取camera的parameters,然后设置属性,再设置回camera。
// get Camera parameters
Camera.Parameters params = mCamera.getParameters();
// set the focus mode
params.setFocusMode(Camera.Parameters.FOCUS_MODE_AUTO);
// set Camera parameters
mCamera.setParameters(params);
几乎所有参数都支持这样的设置方式,设置效果也几乎实时展现,但是修改预览尺寸和相机方向需要先停止预览,然后修改再重启预览。Android 4.0(API 14) 开始修改相机方向不需要停止预览了。
Android 4.0(API 14)开始可以指定局部进行对焦或者轻微设置,并应用到摄像头上。以下是设置两个测光区域的示例代码:
// Create an instance of Camera
mCamera = getCameraInstance();
// set Camera parameters
Camera.Parameters params = mCamera.getParameters();
if (params.getMaxNumMeteringAreas() > 0){ // check that metering areas are supported
List<Camera.Area> meteringAreas = new ArrayList<Camera.Area>();
Rect areaRect1 = new Rect(-100, -100, 100, 100); // specify an area in center of image
meteringAreas.add(new Camera.Area(areaRect1, 600)); // set weight to 60%
Rect areaRect2 = new Rect(800, -1000, 1000, -800); // specify an area in upper right of image
meteringAreas.add(new Camera.Area(areaRect2, 400)); // set weight to 40%
params.setMeteringAreas(meteringAreas);
}
mCamera.setParameters(params);
The Camera.Area包含两个数据字段,一个Rect对象是用来指定相机视野内的一个区域,还有一个权重值用来确定该区域在测光或者对焦计算的重要程度。
上述代码确定了一个2000*2000的矩形区域,-1000,-1000分别是上坐标和左坐标,1000,1000分别是下坐标和右坐标。
图片红线表示用来确定Camera.area矩形区域的坐标系统。
坐标系统的边界总是对应预览界面的图像外边界,并不会因为图片缩放或者图片旋转而改变。