
原文出自王艳涛的专栏转载请注明出处!
简介
android framework包括对设备上可用的各种相机及相机功能的支持,在应用中实现拍照和录制视频,本文对简单的拍照和视频录制进行介绍,并对自定义创建相机体验的高级方法进行概述。
Camera类已经deprecated,在Android5.0后推荐使用Camera2类。
注意事项
在使用相机之前,首先要考虑以下关于怎样使用相机的问题:
- 相机要求:如果应用要使用相机,不允许应用安装在没有相机的设备上时,应该在manifest中声明要求。
- 快速拍照/自定义相机:应用中是否要提供相机新的方法,如果仅仅拍照或者录像,推荐使用系统自带的相机应用,如果需要开发定制的相机功能,请看后面 构建相机应用 章节。
- 存储:是否想让应用中生成的照片/视频仅本应用可见,或者共享使其他应用(相册、其他社交应用等)可见,是否想在应用被卸载后,图片/视频依旧可用,请看后面 保存媒体文件 章节。
基础
android framework是通过 android.hardware.camera2 的API或者相机Intent实现拍照/录屏的功能。相关的类有:
- android.hardware.camera2:该包下是用于控制设备摄相机的主要API。当构建相机应用时,可用于拍摄照片或视频。
- Camera:该类是用于控制相机的旧版本API。
- SurfaceView:该类用来提供相机的预览。
- MediaRecorder:该类用来通过相机录制视频。
- Intent:MediaStore.ACTION_IMAGE_CAPTURE或MediaStore.ACTION_VIDEO_CAPTURE类型的intent action,调用本机相机应用进行拍照/录像。
Manifest 声明
相机权限:应用必须拥有使用相机设备的权限
<uses-permission android:name="android.permission.CAMERA" />
如果调用本地的相机应用,可以不申请此权限
相机功能:应用必须声明一些与相机的一些功能。
<uses-feature android:name="android.hardware.camera" />
了解更多的相机功能,移步至相机功能清单,通过在manifest中声明相机功能,可以防止应用被安装在没有相机或者不支持某些特定相机功能的的设备上。
如果应用可以在没有相机或者某些特定相机功能的设备上也能正常运行,则修改为如下代码:
<uses-feature android:name="android.hardware.camera" android:required="false" />
存储权限:如果要讲照片/视频存到内存卡中,需要声明此权限
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
录音权限:录制视频时,必须声明次权限
<uses-permission android:name="android.permission.RECORD_AUDIO" />
定位权限:如果要让照片带有位置信息,需要声明此权限,另外在android5.0以后,还需要声明GPS的使用
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
...
<!-- Needed only if your app targets Android 5.0 (API level 21) or higher. -->
<uses-feature android:name="android.hardware.location.gps" />
使用本地相机应用
最简单的拍照/录像的方式就是直接使用Inten启动本地相机应用,具体使用查看拍照、录像。
构建相机应用
如果想开发自定义拥有自定义相机界面、功能的相机应用,并使用自定义的编码格式,本地的相机应用就不能胜任了,需要自己构建一个相机应用。
这里使用的是旧版本的Camera API,已经deprecated,查看comera2移步至android.hardware.camera2 API
构建自定义的相机应用流程如下:
- 检测获取相机:检测相机是否存在,并获取访问。
- 创建预览类:创建实现了SurfaceHolder接口的SurfaceView预览类,可以预览相机的实时图像。
- 创建预览布局:有了预览类之后,创建一个视图布局,包含预览界面和用户交互控件。
- 创建捕获监听:响应用户操作,对图像或者视频进行捕获。
- 捕捉和保存:捕获图片/视频的代码,并保存输出。
- 释放相机:相机使用完毕后,必须合适的释放相机,以供其他应用使用。
相机硬件是一种共享资源,确保自定义的应用不会与其他使用相机的应用相冲突。以下部分将讨论如何检测相机,如何请求访问相机,如何捕获图片或视频以及如何在应用完成时释放相机。
检测相机硬件
如果应用没有在mainfest中声明需要相机,在运行时就要使用PackageManager.hasSystemFeature()方法检测相机是否可用。
/** 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;
}
}
Android设备可以有多个摄像头,例如用于摄影的后置摄像头和用于视频通话的前置摄像头。 Android 2.3(API等级9)及更高版本允许使用Camera.getNumberOfCameras()方法检查设备上可用的摄像机数量。
访问相机
如果应用正在运行的设备有相机,则必须通过获取Camera的实例来请求访问相机(使用intent访问除外)。
使用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
}
注意:使用Camera.open()时,请始终检查异常。 如果相机使用或不存在,不检查异常会导致系统关闭应用。
在运行Android 2.3(API Level 9)或更高版本的设备上,可以使用Camera.open(int)访问特定摄像头。上面的示例代码将访问具有多个摄像头的设备上的第一个后置摄像头。
检查相机功能
获取相机访问权限后,可以通过Camera.getParameters() 方法获取相机的更多信息。当使用API Level 9或更高版本时,应该使用Camera.getCameraInfo()来确定相机是否在设备的正面或背面,以及图像的方向。
创建预览类
预览类SurfaceView可以显示相机的实时图像,用户可以根据预览捕获照片/视频。
以下示例代码演示如何创建可以包含在View布局中的基本相机预览类。 该类实现了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.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());
}
}
}
如果要为相机预览设置特定尺寸,请在上述注释中的surfaceChanged()方法中进行设置。 设置预览大小时,您必须使用getSupportedPreviewSizes()中的值。 不要在setPreviewSize()方法中设置任意值。
注意:随着Android 7.0(API级别24)及更高版本中引入了多窗口功能,即使在调用setDisplayOrientation()之后,也不能再假设预览的宽高比与当前的活动相同。 根据窗口大小和宽高比,需要使用 letterbox layout将宽大的相机预览适用于portrait-orientated layout,反之亦然
设置预览布局
预览类必须和其他控件一起放在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 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(API 8级)开始,您可以使用setDisplayOrientation()方法设置预览图像的旋转。为了在用户旋转手机时更改预览方向,在预览类的surfaceChanged()方法中,首先使用Camera.stopPreview()更改方向来停止预览,然后再次使用Camera.startPreview()。
在相机View的activity中,将预览类添加到上述代码的FrameLayout元素中,并在暂停或者关闭时释放相机,代码如下:
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()方法,实现一个Camera.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());
}
}
};
通过拍照button的点击事件触发拍照,代码如下:
// 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);
}
}
);
录像
使用android framework录像时,要认真管理Camera对象,并与MediaRecord类进行交互。
注意:从Android 4.0(API级别14)开始,调用Camera.lock()和Camera.unlock()方法即可自动对Camera进行管理。
与拍照不同,录像需要特定的函数调用顺序,以做好录像的准备工作。流程如下:
- 打开相机:通过Camera.open()获取相机实例。
- 连接预览:通过Camera.setPreviewDisplay()连接SurfaceView,为预览相机实时图像做准备。
- 开始预览:调用Camera.startPreview()显示相机实时图像。
- 开始录制:步骤如下:
- 解锁相机:调用Camera.ulock()解锁相机,以供MediaRecordr使用。
- 配置MediaRecorder:调用下列方法完成MediaRecorder的配置,更多信息参看MediaRecorder
- setCamera():通过Camera实例中的setCamera()方法设置用来录像的相机。
- setAudioSource():通过MediaRecorder.AudioSource.CAMCORDER,设置声源。
- setVideoSource():通过MediaRecorder.VideoSource.CAMERA,设置视频源。
- 设置输出视频编码格式,对于Android 2.2(API 8级)及更高版本,请使用MediaRecorder.setProfile()方法,并使用CamcorderProfile.get()获取配置文件实例。对于2.2之前的Android版本,必须设置如下视频输出格式和编码参数:
- setOutputFormat():指定输出格式,默认为MediaRecorder.OutputFormat.MPEG_4。
- setAudioEncoder():指定声音编码类型,默认为MediaRecorder.AudioEncoder.AMR_NB。
- setVideoEncoder() :指定视频编码类型,默认为MediaRecorder.VideoEncoder.MPEG_4_SP。
- setOutputFile():设置输出文件。
- setPreviewDisplay():用前面指定的连接预览的类,设置SurfaceView预览布局元素。
- 准备MediaRecorde:根据上述配置,调用MediaRecorder.prepare()。
- 开始录制:调用MediaRecorder.start()开始录制视频。
- 停止录制:按顺序调用如下方法:
- 停止MediaReorder:调用MediaRecorder.stop()。
- 重置MediaRecorder(可选):清除MediaRecorder中的配置。
- 释放MediaRecorder:调用MediaRecorder.release()释放。
- 锁定相机:锁定相机,以便后续MediaRecorder通过Camera.lock()使用相机,从Android 4.0(API级别14)开始,除非MediaRecorder.prepare()调用失败,否则不需要此方法。
- 停止预览:当使用完相机后,通过调用Camera.stopPreview()停止预览。
- 释放相机:释放相机,以确保其他应用可以使用相机,调用Camera.release()。
注意:可以先使用MediaRecorder创建相机预览,并跳过此过程的前几个步骤。 然而,由于用户通常喜欢在开始录制之前看到预览,所以这里不讨论该过程。
提示:如果应用通常用于录制视频,在启动预览之前将setRecordingHint(boolean)设置为true。 此设置可以帮助减少开始录制所需的时间
配置MediaRecorder
private boolean prepareVideoRecorder(){
mCamera = getCameraInstance();
mMediaRecorder = new MediaRecorder();
// Step 1: Unlock and set camera to MediaRecorder
mCamera.unlock();
mMediaRecorder.setCamera(mCamera);
// Step 2: Set sources
mMediaRecorder.setAudioSource(MediaRecorder.AudioSource.CAMCORDER);
mMediaRecorder.setVideoSource(MediaRecorder.VideoSource.CAMERA);
// Step 3: Set a CamcorderProfile (requires API Level 8 or higher)
mMediaRecorder.setProfile(CamcorderProfile.get(CamcorderProfile.QUALITY_HIGH));
// Step 4: Set output file
mMediaRecorder.setOutputFile(getOutputMediaFile(MEDIA_TYPE_VIDEO).toString());
// Step 5: Set the preview output
mMediaRecorder.setPreviewDisplay(mPreview.getHolder().getSurface());
// Step 6: Prepare configured MediaRecorder
try {
mMediaRecorder.prepare();
} catch (IllegalStateException e) {
Log.d(TAG, "IllegalStateException preparing MediaRecorder: " + e.getMessage());
releaseMediaRecorder();
return false;
} catch (IOException e) {
Log.d(TAG, "IOException preparing MediaRecorder: " + e.getMessage());
releaseMediaRecorder();
return false;
}
return true;
}
在Android 2.2(API 8级)之前,必须直接设置输出格式和编码格式参数,而不是使用CamcorderProfile。 此方法在以下代码中进行了说明:
// Step 3: Set output format and encoding (for versions prior to API Level 8)
mMediaRecorder.setOutputFormat(MediaRecorder.OutputFormat.MPEG_4);
mMediaRecorder.setAudioEncoder(MediaRecorder.AudioEncoder.DEFAULT);
mMediaRecorder.setVideoEncoder(MediaRecorder.VideoEncoder.DEFAULT);
MediaRecorder的以下视频录制参数为默认设置,可能需要调整时可以调用下面的方法:
- setVideoEncodingBitRate()
- setVideoSize()
- setVideoFrameRate()
- setAudioEncodingBitRate()
- setAudioChannels()
- setAudioSamplingRate()
开始/停止MediaRecorder
当使用MediaRecorder类开始和停止视频录制时,必须遵循以下列方法的调用顺序。
- 解锁相机
- 配置MediaRecorder
- 开始MediaRecorder
- 录制视频
- 停止MediaRecorder
- 释放MediaRecorder
- 锁定相机
当完成录制后,不要立即释放相机,否则预览也会停止。
private boolean isRecording = false;
// 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) {
if (isRecording) {
// stop recording and release camera
mMediaRecorder.stop(); // stop the recording
releaseMediaRecorder(); // release the MediaRecorder object
mCamera.lock(); // take camera access back from MediaRecorder
// inform the user that recording has stopped
setCaptureButtonText("Capture");
isRecording = false;
} else {
// initialize video camera
if (prepareVideoRecorder()) {
// Camera is available and unlocked, MediaRecorder is prepared,
// now you can start recording
mMediaRecorder.start();
// inform the user that recording has started
setCaptureButtonText("Stop");
isRecording = true;
} else {
// prepare didn't work, release the camera
releaseMediaRecorder();
// inform user
}
}
}
}
);
释放相机
相机是由设备上的应用程序共享的资源。 获取相机实例后,应用可以使用相机,当应用停止使用时,或者一旦应用程序暂停(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;
}
}
}
存储媒体文件
照片/视频媒体文件应该存放在外部存储,以节省系统空间。下面是两种标准方式:
- Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES) :方法返回照片/视频的标准、共享、推荐的存储位置。其他应用可以访问,应用卸载后,文件不会被删除。为了避免干扰用户现有的图片和视频,应该在此目录中创建一个子目录,如下代码所示。
- Context.getExternalFilesDir(Environment.DIRECTORY_PICTURES) :方法返回用于保存于应用相关联的图片/视频位置,如果应用卸载,这些文件将会被删除。
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;
}
注意:Environment.getExternalStoragePublicDirectory()在Android 2.2(API级别8)或更高版本中可用。 如果您使用早期版本的Android定位设备,请改用Environment.getExternalStorageDirectory()。
其他
相机功能配置、使用,如人脸识别、自动对焦等,请自行百度。
网友评论