文章翻译自谷歌API指南,摘取了自定义相机部分。文中连接直接调到官方文档,请自备梯子。也可以下载一个离线API文档,直接搜索相关API查看API使用说明。
原文链接:Camera
一、相关权限声明
以下权限及特性根据情况添加。
<uses-permission android:name="android.permission.CAMERA" /><uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" /><uses-permission android:name="android.permission.RECORD_AUDIO" /><uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" /><uses-feature android:name="android.hardware.camera" /><uses-feature android:name="android.hardware.camera.autofocus" /><uses-feature android:name="android.hardware.camera.flash" /><uses-feature android:name="android.hardware.camera.front" />
二、通常步骤
- 检测相机硬件是否存在
- 访问相机
- 检查照相机特性
- 创建预览类(Preview Class)
- 在布局中放置预览
- 捕获图片
- 释放相机
三、检测相机硬件是否存在
如果没有在配置清单明确声明要使用相机,则应该在代码中动态检测相机是否存在。示例如下:
/** Check if this device has a camera */private boolean checkCameraHardware(Context context) {if (context.getPackageManager().hasSystemFeature(PackageManager.FEATURE_CAMERA)){// this device has a camerareturn true;} else {// no camera on this devicereturn false;}}
Android 2.3 及其之后允许检查摄像头数量,使用 Camera.getNumberOfCameras() 方法。
四、访问相机
确定相机硬件存在之后便要取得Camera对象
要取得camera,使用 Camera.open() 方法并且确保捕获所有异常。示例如下:
/** 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}
Android 2.3及其之后可以访问指定的相机通过 Camera.open(int)方法。上面代码中的方法默认返回多摄像头设备的第一个背置摄像头。
五、检查照相机特性
一旦获得照相机对象,你可以通过 Camera.getParameters() 得到关于照相机功能的进一步信息并且检查返回的 Camera.Parameters 对象来支持这些功能。当使用 API 9 及以上,使用 Camera.getCameraInfo()来确定照相机是前置还是后置及图像方向。
六、创建预览类(Preview Class)
为了用户能有效的照相及摄像,他们必须能看见照相机设备的图像。照相机预览类是一个 SurfaceView ,它能显示照相机当前图像信息。所以使用者可以截取或捕获照片及视频。
下面的示例代码演示了如何创建一个能被放置到视图布局中的preview class。这个类继承了 SurfaceHolder.Callback接口以便捕获的布局视图的创建及销毁事件,这将用于决定照相机预览输入。
/** 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.0mHolder.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 existreturn;}// stop preview before making changestry {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 settingstry {mCamera.setPreviewDisplay(mHolder);mCamera.startPreview();} catch (Exception e){Log.d(TAG, "Error starting camera preview: " + e.getMessage());}}}
如果你想为你的相机预览设置一个具体的尺寸,把它写在surfaceChanged() 方法中,即上面示例代码中注释的位置。但你设置了预览尺寸,你必须使用从getSupportedPreviewSizes()中获得的值。不要在setPreviewSize()方法中随意设置值。
七、在布局中放置预览
一个相机预览类,例如上一章节中展示的那样,必须放置在一个Activity的布局中以便其它用户接口控制拍照和摄像。这个章节展示如何建立一个基本的布局和Activity来预览。
下面的布局代码提供了一个基本视图用来展示相机预览,在这个示例中,这个 FrameLayout 节点作为相机预览界面的容器。这个布局类型被使用所以额外的照片信息或者控制能被附加在当前相加预览上。示例如下:
<?xml version="1.0" encoding="utf-8"?><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>
在大多数的设备上,默认的相机预览方向是横向。这个示例布局指定一个横向布局并且代码中更改应用方法为横向。为了简单解释相机预览,你应该更改通过以下配置应用预览Activity方向为横向。
<activity android:name=".CameraActivity"android:label="@string/app_name"android:screenOrientation="landscape"><!-- configure this activity to use landscape orientation --><intent-filter><action android:name="android.intent.action.MAIN" /><category android:name="android.intent.category.LAUNCHER" /></intent-filter></activity>
一个相机预览不是一定在横向模式。从Android 2.2开始,你可以使用 setDisplayOrientation() 方法来设置预览图像的旋转。为了在用户改变手机方向时改变预览方向,应该在预览类中surfaceChanged()方法中设置,首先使用Camera.stopPreview()停止预览,改变方向,然后通过Camera.startPreview()再次启动预览。
在你的相机视图Activity中,添加预览类到示例布局中的FrameLayout节点。你的相机Activity一定也要确保在它暂停或者关闭时释放相机。一下示例代码展示了如何更改相机Activity来关联预览类。
public class CameraActivity extends Activity {private Camera mCamera;private CameraPreview mPreview;@Overridepublic void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.main);// Create an instance of CameramCamera = 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);}}
上面示例代码中的getCameraInstance()方法是在步骤四中定义的。
八、捕获图片
当你建立了一个预览类在你显示的布局中。你已经准备好通过你的应用捕获图片了。在你的应用代码中,你必须设置接口来响应用户拍照动作。
为了取得图像,使用Camera.takePicture()方法。这个方法需要三个参数接收相机返回的数据。为了接收JPEG格式的数据,你必须继承一个Camera.PictureCallback借口来接收图像数据并且写入文件。以下代码展示了一个基本的Camera.PictureCallback接口的实现来保存从相机接收到的数据。
private PictureCallback mPicture = new PictureCallback() {@Overridepublic 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());}}};
触发捕获图像是通过调用Camera.takePicture()方法,以下示例代码展示了如何通过Button监听器调用这个方法。
// Add a listener to the Capture button
Button captureButton = (Button) findViewById(id.button_capture);
captureButton.setOnClickListener(new View.OnClickListener() {@Overridepublic void onClick(View v) {// get an image from the cameramCamera.takePicture(null, null, mPicture);}}
);
九、释放相机
相机是一个设备上的应用共享资源。你的应用可以使用相机在得到相机实例之后,并且当你的应用停止使用它,一定要格外注意的释放照相机对象,哪怕是你的应用暂停了。如果你的应用没有正确释放照相机,所有随后尝试访问照相机的应用,包括你的应用,都将失败,这可能导致你的或者其他应用被关闭。
为了释放照相机实例,使用 Camera.release() 方法,示例代码如下:
public class CameraActivity extends Activity {private Camera mCamera;private SurfaceView mPreview;private MediaRecorder mMediaRecorder;...@Overrideprotected void onPause() {super.onPause();releaseMediaRecorder(); // if you are using MediaRecorder, release it firstreleaseCamera(); // release the camera immediately on pause event}private void releaseMediaRecorder(){if (mMediaRecorder != null) {mMediaRecorder.reset(); // clear recorder configurationmMediaRecorder.release(); // release the recorder objectmMediaRecorder = null;mCamera.lock(); // lock camera for later use}}private void releaseCamera(){if (mCamera != null){mCamera.release(); // release the camera for other applicationsmCamera = null;}}}
注意:如果你的应用没有正确释放照相机,所有随后尝试访问照相机的应用,包括你的应用,都将失败,这可能导致你的或者其他应用被关闭。
十、保存媒体文件
用户创建的媒体文件比如照片和视频应该保存在设备外部存储目录来养护系统空间并允许用户在没有他们设备的情况下来访问他们的文件。有许多可以用来在设备上保存媒体文件的目录,但是作为开发者,这里只有两个标准地方你应该考虑。
- Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES) - 这个方法返回一个标准的,共享的和推荐的位置来存储图片和视频。这个目录是共享的,所以其他应用可以容易的发现,读取,改变和删除在这个位置上的文件。如果你的应用被用户卸载,存储在这个位置的媒体文件将不会被移除。为避免干扰用户已经存在的相片和视频,你应该为你的应用在这个目录中创建一个多媒体文件夹。正如示例代码中那样。这个方法在Andorid 2.2中被提供。想要在之前版本中访问,参考Saving Shared Files.
- Context.getExternalFilesDir(Environment.DIRECTORY_PICTURES)-这个方法返回一个与应用关联的存储图片和视频的标准位置。如果你的应用被移除,在这个位置存储的文件将被移除。安全的是不要强制在这个位置的文件并且其他应用能读取、改变和删除他们。
下面的示例代码演示了当通过Itent或者建立的相机APP调用了设备相机时,如何为媒体文件创建一个File或者Uri位置。
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 existif (! mediaStorageDir.exists()){if (! mediaStorageDir.mkdirs()){Log.d("MyCameraApp", "failed to create directory");return null;}}// Create a media file nameString 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;}
注意:Environment.getExternalStoragePublicDirectory()在Android 2.2及以上版本才可用。如果你要针对更早的版本,使用Environment.getExternalStorageDirectory()。
十一、相机特性
Androd为你的相机应用提供各式各样你可以控制的的相机特性,比如照片格式,闪光灯模式,焦点模式等。这个章节将列出常见相机特性,并且简要的讨论如何使用他们。大多数详解特性能通过Camera.Parameters被访问和访问。然而,这里有几个比其他简单设置要注意的特性。这几个特性在下面被列出了:
- Metering and focus areas
- Face detection
- Time lapse video
想要获得通过Camera.Parameters控制特性的大概信息,阅读Using camera features这章。想要获得通过相机参数控制相机属性的详细信息,按照下面特性列表中的API参考文档链接。
表格不好打,大家调到官网看一下吧,往下一点的表格就是:
我是表格
检查特性可用性
在Android设备上设置相机特性要了解的第一件事情是不是所有的相机特性在所有设备上都支持。此外,支持特定属性的设备可能在不同程度上支持他们或者不同选项。因此,当你开发相机应用时一个部分是决定支持什么样的相机特性,支持到什么级别。做完这个决定,你应该计划在你的相机应用中加入代码来检出设备硬件是否支持这些特性,并且在一个特性不支持的时候优雅的处理它。
你可以检查相机特性可用性通过得到一个相机参数对象。并且检查响应的方法。一下代码暂时了如何创建一个Camera.Parameters对象,并且检查相机是否支持自动对焦特性。
// get Camera parametersCamera.Parameters params = mCamera.getParameters();List<String> focusModes = params.getSupportedFocusModes();if (focusModes.contains(Camera.Parameters.FOCUS_MODE_AUTO)) {// Autofocus mode is supported}
你可以使用技术显示所有相机特性。这个* Camera.Parameters*对象提供了一个
getSupported…(), is…Supported() or getMax…()方法来确定一个特性时候以及什么程度上被支持。
如果你的用用需要一些特性以使功能正确。你可以在应用配置文件中添加他们。当你声明了使用这些特定相加属性,比如闪光灯和自动对焦,Google Play限制你的应用在不支持这些特性的手机上安装。要查看应用配置文件中可以声明的特性,查看* Features Reference.*
使用相机特性
大多数特性活跃的闭关且能被Camera.Parameters对象控制。创建这个对象首先要得到相机对象的实例。通过getParameters()方法,改变返回的参数对象之后再设置给相机对象,正如下面展示的那样。
// get Camera parametersCamera.Parameters params = mCamera.getParameters();// set the focus modeparams.setFocusMode(Camera.Parameters.FOCUS_MODE_AUTO);// set Camera parametersmCamera.setParameters(params);
这种方法几乎对所有相机特性都有用,并且大多着参数设置之后对对用户立即可见的。在软件方面,参数改变之后,照相机硬件处理新的参数然后发送更新数据,期间可能花费一些帧来生效。
重要:一些照相机特性不能随意改变。尤其是,改变照相机预览的尺寸或者照相机方向的请求,首先应该停止预览,然后改变尺寸,之后重启预览。Android 4.0之后预览方向可以在不重启的情况下改变。
其他照相机特性需要更多代码来实现,包括:
- Metering and focus areas
- Face detection
- Time lapse video
在下面的章节中提供了如何实现这些功能的快速概述。
白平衡和焦点区域
在一些摄影场景,自动对焦和亮度调节可能不会达到预期效果。从Android 4.0开始,你的相机应用可以提供额外的控制来允许你的应用或者用户在相片中的指定区域来决定焦点或者亮度并且将这些数值提交给相机硬件用来捕获图片或视频。
计量或者焦点的区域和其他你通过方法控制的相机特性的作用方式十分相似。下面代码演示了为照相机实例设置两个亮度计量区域。
// set Camera parametersCamera.Parameters params = mCamera.getParameters();if (params.getMaxNumMeteringAreas() > 0){ // check that metering areas are supportedList<Camera.Area> meteringAreas = new ArrayList<Camera.Area>();Rect areaRect1 = new Rect(-100, -100, 100, 100); // specify an area in center of imagemeteringAreas.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 imagemeteringAreas.add(new Camera.Area(areaRect2, 400)); // set weight to 40%params.setMeteringAreas(meteringAreas);}mCamera.setParameters(params);
这个Camera.Area对象包括两个数据参数:一个 Rect 对象用来指定一个在相机作用视图区域和一个权重值,权重值告诉相机这个应该给予亮度计量或焦点计算的区域重要性等级。
在一个Camera.Area对象的Rect 成员域描述了一个矩形形状映射到一个2000 X 2000的单元格。这个坐标系中(-1000,-1000)代表相机图片的左上角,坐标系中(1000,1000)代表相机图片的右下角,如下插图所示。
图1. 红色的线说明了坐标系系统在相机预览中指定了一个Camera.Area。这个蓝色的框展示了Rect位置和形状,值为333,333,667,667
这个坐标系统的边界总是对应可见的相机预览图中的外边缘,并且随着缩放级别伸缩或者扩展。同样,使用Camera.setDisplayOrientation() 旋转相机预览图像不会重新映射坐标系统。
人脸检测
对于包含人脸的照片,面部总是总是照片中最重要的地方,并且应该在被用来在捕获图像时决定焦点和白平衡。Andorid 4.0框架提供了APIs来鉴定脸部并且计算相片设置使用脸部检测技术。
注意:当脸部检测运行中,setWhiteBalance(String), setFocusAreas(List) and setMeteringAreas(List)不起作用。
在你的相机中使用脸部检测特性需要以下几步:
- 检查脸部检测是否被设备支持
- 创建一个脸部识别监听器
- 添加脸部识别监听器到相机类
- 在预览开始后开始脸部识别
脸部识别特性不是被所有设备支持。你可以检查这个特性是否被设备支持通过getMaxNumDetectedFaces()。一个检查实例将在下面的startFaceDetection()示例中提供。
为了能被通知和被脸部识别响应,你的相机应用必须为脸部识别事件设置监听器。为了达到这个,你必须创建一个实现了Camera.FaceDetectionListener接口的监听器类,示例如下:
class MyFaceDetectionListener implements Camera.FaceDetectionListener {@Overridepublic void onFaceDetection(Face[] faces, Camera camera) {if (faces.length > 0){Log.d("FaceDetection", "face detected: "+ faces.length +" Face 1 Location X: " + faces[0].rect.centerX() +"Y: " + faces[0].rect.centerY() );}}}
创建完这个监听器之后,你可以设置给你的相机对象,如下代码所示:
mCamera.setFaceDetectionListener(new MyFaceDetectionListener());
你的应用一定要在每次你启动或重新启动相机预览之后启动脸部识别。创建一个方法来开始脸部识别这样你可以在需要的时候调用它,如下代码所示:
public void startFaceDetection(){// Try starting Face DetectionCamera.Parameters params = mCamera.getParameters();// start face detection only *after* preview has startedif (params.getMaxNumDetectedFaces() > 0){// camera supports face detection, so can start it:mCamera.startFaceDetection();}}
你一定要在每次启动或重新启动相机预览之后启动人脸识别。如果你使用了创建预览类章节中的类的话,添加* startFaceDetection()方法到surfaceCreated()和surfaceChanged()*方法中,如下代码所示:
public void surfaceCreated(SurfaceHolder holder) {try {mCamera.setPreviewDisplay(holder);mCamera.startPreview();startFaceDetection(); // start face detection feature} catch (IOException e) {Log.d(TAG, "Error setting camera preview: " + e.getMessage());}}public void surfaceChanged(SurfaceHolder holder, int format, int w, int h) {if (mHolder.getSurface() == null){// preview surface does not existLog.d(TAG, "mHolder.getSurface() == null");return;}try {mCamera.stopPreview();} catch (Exception e){// ignore: tried to stop a non-existent previewLog.d(TAG, "Error stopping camera preview: " + e.getMessage());}try {mCamera.setPreviewDisplay(mHolder);mCamera.startPreview();startFaceDetection(); // re-start face detection feature} catch (Exception e){// ignore: tried to stop a non-existent previewLog.d(TAG, "Error starting camera preview: " + e.getMessage());}}
注意:记住在startPreview()调用之后这个方法。不要尝试在你相机Activity的onCreate()方法中启动脸部识别,这个预览在你应用的这个执行点事不被提供的。