基础
相机开发中常用的API
- package android.hardware.camera2 这个包中包含了主要的相机控制的API,可以用于在你的项目中进行拍照或者录像功能的实现
- Camera 这个类是早些版本驱动相机的API
- SurfaceView 用于相机预览的界面
- MediaRecorder 用于录像的API
- 通过Intent调用系统的拍照功能,通过设置MediaStore.ACTION_IMAGE_CAPTURE 或者 MediaStore.ACTION_VIDEO_CAPTURE 动作调用系统的拍照功能
权限声明
- 相机开发权限声明
<uses-permission android:name="android.permission.CAMERA" />
- 其他功能,如GPS定位权限声明
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
调用本机相机功能
- 开启相机
private static final int CAPTURE_IMAGE_ACTIVITY_REQUEST_CODE = 100;
private Uri fileUri;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
// 创建Intent对象并传递相应的action参数
Intent intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
fileUri = getOutputMediaFileUri(MEDIA_TYPE_IMAGE); // 创建一个文件夹保存图片
intent.putExtra(MediaStore.EXTRA_OUTPUT, fileUri); // 设置保存路径
// 开启相机
startActivityForResult(intent, CAPTURE_IMAGE_ACTIVITY_REQUEST_CODE);
}
- 接收返回结果
private static final int CAPTURE_IMAGE_ACTIVITY_REQUEST_CODE = 100;
private static final int CAPTURE_VIDEO_ACTIVITY_REQUEST_CODE = 200;
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
if (requestCode == CAPTURE_IMAGE_ACTIVITY_REQUEST_CODE) {
if (resultCode == RESULT_OK) {
// Image captured and saved to fileUri specified in the Intent
Toast.makeText(this, "Image saved to:\n" +
data.getData(), Toast.LENGTH_LONG).show();
} else if (resultCode == RESULT_CANCELED) {
// User cancelled the image capture
} else {
// Image capture failed, advise user
}
}
if (requestCode == CAPTURE_VIDEO_ACTIVITY_REQUEST_CODE) {
if (resultCode == RESULT_OK) {
// Video captured and saved to fileUri specified in the Intent
Toast.makeText(this, "Video saved to:\n" +
data.getData(), Toast.LENGTH_LONG).show();
} else if (resultCode == RESULT_CANCELED) {
// User cancelled the video capture
} else {
// Video capture failed, advise user
}
}
}
构建一个相机APP
相机开发的基本步骤
- 检查是否能够使用相机
- 创建一个相机的预览类,该类继承自SurfaceView并且实现SurfaceHolder接口,用于接收并显示camera传递过来的数据
- 构建一个预览布局,该布局包含相机预览类以及用户自定义的控制接口
- 设置监听事件,捕获用户拍照或者录像动作,并作出相应的响应
- 将捕获到的照片或者录像保存
- 释放Camera资源,通过camera.release();方法
分步骤
1,检测相机功能是否可以使用
通过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;
}
}
此外,2.3版本以后,可以通过Camera.getNumberOfCameras()方法来获取设备中相机的数量,可以用来判断是否有前置摄像头
2,获取相机实例
使用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正在被使用或者不存在的时候,应用将会直接退出。此外,在2.3版本或者更高版本中,还可以使用open(int)方法获取Camera的实例,传入得参数int类似于优先级的作用
3,相机参数的获取
通过camera.getParameters()方法获取到相机相对应的参数,并通过Parameter对象进行设置修改,在2.3及之后的版本,通过camera.getCameraInfo()方法来确定摄像头是否为前置或者后置,以及Image的方向
4,创建相机预览类
/** 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);
// 不赞成设置,要求在3.0版本之前使用
mHolder.setType(SurfaceHolder.SURFACE_TYPE_PUSH_BUFFERS);
}
public void surfaceCreated(SurfaceHolder holder) {
// 通知Camera预览图片放哪里,即将SurfaceView与相机绑定
try {
mCamera.setPreviewDisplay(holder);
mCamera.startPreview();
} catch (IOException e) {
Log.d(TAG, "Error setting camera preview: " + e.getMessage());
}
}
public void surfaceDestroyed(SurfaceHolder holder) {
// empty.注意要在你的活动中释放相机预览
}
public void surfaceChanged(SurfaceHolder holder, int format, int w, int h) {
// If your preview can change or rotate, take care of those events here.
// 确保在重新设置大小或者格式化之前停止预览
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
//重新设置代码编写
//设置预览大小尺寸应该从方法getSupportPreviewSizes()方法中获取
// start preview with new settings
//开始预览
try {
mCamera.setPreviewDisplay(mHolder);
mCamera.startPreview();
} catch (Exception e){
Log.d(TAG, "Error starting camera preview: " + e.getMessage());
}
}
}
5,创建简单的相机预览界面布局
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="horizontal"
android:layout_width="match_parent"
android:layout_height="match_parent"
>
<FrameLayout
android:id="@+id/camera_preview"
android:layout_width="match_parent"
android:layout_height="match_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>
当中的FramLayout相当于是相机预览的容器,而Button则是实现点击拍照,通常情况,相机画面默认的方向是水平方向的,所以需要将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>
或者通过Parameter的setRotation()方法对方向进行调整
//根据当前朝向修改保存图片的旋转角度
private void updateCameraOrientation(){
if(mCamera!=null){
Camera.Parameters parameters = mCamera.getParameters();
//rotation参数为 0、90、180、270。水平方向为0。
int rotation=90+mOrientation==360?0:90+mOrientation;
//前置摄像头需要对垂直方向做变换,否则照片是颠倒的
if(mIsFrontCamera){
if(rotation==90) rotation=270;
else if (rotation==270) rotation=90;
}
parameters.setRotation(rotation);//生成的图片转90°
//预览图片旋转90°
mCamera.setDisplayOrientation(90);//预览转90°
mCamera.setParameters(parameters);
}
}
构建出相机的布局后,将相机预览类放到布局的FramLayout中,具体代码如下:
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);
}
}
6-1 实现拍照功能
- 拍照动作的实现,主要是通过Camera.takePicture()方法完成,该方法接收三个参数,分别为三个回调事件,ShutterCallback以及两个PictureCallback,为了接收图片,第三个PictureCallback必须实现,用来保存camera当中拍到的图片,简单的实现如下:
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设置监听事件,当按钮被点击时触发拍照功能,即调用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);
}
}
);
通过以上代码即可简单实现拍照功能
6-2 实现录像功能
- 相比较于拍照,录像功能的实现需要更加细致的管理,需要Camera同另一个类MediaRecorder协同配合完成。在使用MediaRecorder之前,还需要调用Camera.lock()和Camera.unlock()方法来允许MediaRecorder对象调用camera硬件。从4.0版本开始,这两个方法将会自动被调用,而不需要手动调用它们。
- 与拍照不同,录像功能的实现必须按照指定的顺序执行才能成功,详细步骤如下
- 打开相机,获取到Camera的实例
- 绑定对应的预览SurfaceView,通过Camera.setPreviewDisplay()方法
- 调用Camera.startPreview()方法开启预览
- 开启录像功能,一下步骤必须全部实现
- 解锁相机,通过Camera.unlock()方法
- 配置MediaRecorder
- setCamera(),设置相机为录像功能,
释放Camera资源
Camera在内润是单例的,所有的程序都可以访问和获取该实例,而一旦程序在使用完Camera对象后没有及时的释放该实例,将会导致该资源不能被其他的程序所访问,所有访问Camera类都会被系统关闭。释放Camera的方法为Camera.release();实例代码如下:
public class CameraActivity extends Activity {
private Camera mCamera;
private SurfaceView mPreview;
private MediaRecorder mMediaRecorder;
...
//在Activity的onPause方法中释放资源,如果同时调用了MediaRecorder,也需要将MediaRecorder一同释放
@Override
protected void onPause() {
super.onPause();
// if you are using MediaRecorder, release it first
releaseMediaRecorder();
// release the camera immediately on pause event
releaseCamera();
}
private void releaseMediaRecorder(){
if (mMediaRecorder != null) {
// clear recorder configuration
//释放前先清除MediaRecorder中的配置
mMediaRecorder.reset();
// release the recorder object
//释放MediaRecorder实例
mMediaRecorder.release();
mMediaRecorder = null;
// lock camera for later use
//将Camera锁上,后面释放和调用时使用
mCamera.lock();
}
}
private void releaseCamera(){
if (mCamera != null){
// release the camera for other applications
mCamera.release();
mCamera = null;
}
}
}
文件存储
- 使用后生成的Image或者Video都应该保存在sd卡中,保存的路径有以下两个:
- Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES),该方法中的路径是公有的,共享的,所有的应用都可以通过该路径得到路径下的图片资源文件,同时对其进行修改、删除等操作,也是推荐的保存路径。此外,当应用程序被卸载时,保存在该路径下的图片文件并不会删除,而会一直保存下来。
- context.getExternalFileDir(Enviroment.DIRECTORY_PICTURES);该路径下保存的Image或者Video资源将会和你的Application关联,其他的程序不能对其中的资源进行访问、修改以及删除等操作;但当应用程序被卸载时,这些资源也将会被删除。
//示例代码
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 */
//获取Uri地址
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.
//在使用之前最好先检查sd卡是否挂载
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()){
//如果创建失败,返回null
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(Environment.DIRECTORY_PICTURES)方法是在2.2(APILevel 8)版本之后的,更早的版本应该使用Environment.getExternalStorageDirectory()替换。
Camera属性
- camera中大部分的属性都可以通过Camera.Paramter中进行设置,详细属性说明看官方文档,以下三个属性还需要进行其他的设置
- Metering and focus area
- Face detection
- Time lapse video
- 不是所有的camera属性都是可以使用的,要根据手机硬件是否支持以及相对应的软件是否实现该功能。
检测Camera中的属性是否可用
camera中的一些属性的使用是取决于手机的硬件以及软件是否支持,所以在考虑使用这些属性的时候,就需要对手机是否支持进行检测判断,例如:
// 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
}
Camera.Paramters中的大部分属性是否支持都可以通过getSupport...()或者is...Supported()或者getMax()方法获取,需要注意的是,当你检测和设置了这些属性之后,Google Play将会约束你的应用不能在不支持这些功能的手机上进行安装。
- 属性设置
// 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);
相机中大部分属性在修改过后就能立刻在Preview当中显示出来,不过图片的尺寸以及方向是不能立刻显示的,需要先停止预览,设置大小及方向,最后再重新开始预览才能显示出来,从4.0版本(APILevel 14)开始,预览的可以不需要重新启动就被改变。
设置感光和聚焦区域
在4.0版本(APILevel 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
// set weight to 60%
//设置权重
meteringAreas.add(new Camera.Area(areaRect1, 600));
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);
注意:通过Camera.setDisplayOrientation()改变屏幕方向,原坐标系统不会重绘。
人脸识别
- 从4.0版本(APILevel 14)之后,提供了可以进行人脸识别的API,需要注意的是,在进行人脸识别的时候,setWhiteBalance(String), setFocusAreas(List)和setMeteringAreas(List) 将不会有效。
- 人脸识别功能的开发步骤:
- 检查设备当中是否支持该功能,通过getMaxNumDetectedFaces()方法进行获取
- 创建相应的人脸识别监听并绑定到相机上
- 在预览后开启人脸识别
//人脸识别监听器
class MyFaceDetectionListener implements Camera.FaceDetectionListener {
@Override
public 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 Detection
Camera.Parameters params = mCamera.getParameters();
// start face detection only *after* preview has started
//确保开启该功能之前开启了预览
if (params.getMaxNumDetectedFaces() > 0){
// camera supports face detection, so can start it:
mCamera.startFaceDetection();
}
}
- 开启的完整代码
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 exist
Log.d(TAG, "mHolder.getSurface() == null");
return;
}
try {
mCamera.stopPreview();
} catch (Exception e){
// ignore: tried to stop a non-existent preview
Log.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 preview
Log.d(TAG, "Error starting camera preview: " + e.getMessage());
}
}