除了调用第三方APP进行拍照外,我们还可以自己使用系统API来控制相机设备进行拍照。这个相对来说比较复杂。下面只讲比较关键的步骤。
控制照相机我们需要声明使用照相设备的权限。
<uses-feature android:name="android.hardware.camera" android:required="true"/>
<uses-permission android:name="android.permission.CAMERA" />
<uses-feature android:name="android.hardware.camera.autofocus" android:required="false" />
我们声明feature的required
属性为true,这样没有相机设备的手机就没法安装APP。若设置为false,我们就需要在代码中检测是否具有相机设备,若有的话则开启拍照功能,否则需要禁用该功能。判断方法如下:
if(getPackageManager().hasSystemFeature(PackageManager.FEATURE_CAMERA)){
photoBtn.setOnClickListener(this);
}else{
photoBtn.setVisibility(View.GONE);
}
if(getPackageManager().hasSystemFeature(PackageManager.FEATURE_CAMERA_AUTOFOCUS)){
//开启自动聚焦模块
}
打开相机设备就是获得相机的一个实例,它是IO操作,所以可能比较耗时。为了避免ANR,我们需要在onCreate
或onResume
方法中另开一个线程去执行IO操作。相机设备可能会被其他APP占用,这时调用Camera.open()
方法会抛出异常。安全地打开相机设备的代码如下:
@Override
protected void onResume() {
super.onResume();
//另开一个线程
new Thread(new Runnable() {
@Override
public void run() {
if(safeCameraOpen(0)){
mHandler.post(new Runnable() {
@Override
public void run() {
if(mPreview != null){
//设置给预览视图,mPreview 为SurfaceView的子类
mPreview.setCamera(mCamera);
}
}
});
}
}
}).start();
}
/** *打开指定的相机设备,id为相机序号,<=相机设备数量-1 **/
private boolean safeCameraOpen(int id) {
boolean qOpened = false;
try {
//确保之前的实例已经被正确关闭并释放
releaseCameraAndPreview();
mCamera = Camera.open(id);
qOpened = (mCamera != null);
if(mCamera != null){
mCamera.setDisplayOrientation(90);
}
} catch (Exception e) {
Log.e(getString(R.string.app_name), "failed to open Camera");
e.printStackTrace();
}
return qOpened;
}
private void releaseCameraAndPreview() {
if(mPreview != null) {
mPreview.setCamera(null);
}
if (mCamera != null) {
mCamera.release();
mCamera = null;
}
}
上面的代码在onResume
方法中安全的打开设备。同样重要的是,我们要在onPause
方法中确保相机设备被正确的关闭和释放,这样就不会阻碍其他APP使用相机设备。
@Override
protected void onPause() {
releaseCameraAndPreview();
super.onPause();
}
要在拍照前预览效果,我们要构造预览视图,这个视图通常是继承自SurfaceView
的自定义视图。SurfaceView
能够非UI线程中绘制界面,因为预览效果需要频繁更新界面,使用它可以避免阻塞UI线程。下面是自定义的CameraPreview
的完整代码:
package com.github.znacloud.multimediademo.view;
import android.content.Context;
import android.hardware.Camera;
import android.util.AttributeSet;
import android.view.SurfaceHolder;
import android.view.SurfaceView;
import java.io.IOException;
import java.util.List;
/** * Created by Stephan on 2016/1/15. */
public class CameraPreview extends SurfaceView implements SurfaceHolder.Callback{
private SurfaceHolder mHolder;
private Camera mCamera;
private List<Camera.Size> mSupportedPreviewSizes;
public CameraPreview(Context context) {
super(context);
inits(context);
}
public CameraPreview(Context context, AttributeSet attrs) {
super(context, attrs);
inits(context);
}
public CameraPreview(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
inits(context);
}
private void inits(Context pContext){
mHolder = getHolder();
mHolder.addCallback(this);
}
@Override
public void surfaceCreated(SurfaceHolder holder) {
}
@Override
public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
if(mCamera == null) return;
Camera.Parameters parameters = mCamera.getParameters();
//根据视图的尺寸设置预览图的尺寸
parameters.setPreviewSize(width, height);
mCamera.setParameters(parameters);
requestLayout();
// 必须调用startPreview里面更新视图
mCamera.startPreview();
}
@Override
public void surfaceDestroyed(SurfaceHolder holder) {
if (mCamera != null) {
// 视图销毁时停止预览
mCamera.stopPreview();
}
}
public void setCamera(Camera camera) {
if (mCamera == camera) { return; }
//设置新的相机实例之前确保旧的相机实例被正确的释放和关闭
stopPreviewAndFreeCamera();
mCamera = camera;
if (mCamera != null) {
List<Camera.Size> localSizes = mCamera.getParameters().getSupportedPreviewSizes();
//TODO:根据设备支持的预览尺寸设置视图尺寸
try {
//关联到SurfaceView
mCamera.setPreviewDisplay(mHolder);
} catch (IOException e) {
e.printStackTrace();
}
//每次更新了相机实例后都要调用此方法开启预览功能
mCamera.startPreview();
}
}
private void stopPreviewAndFreeCamera() {
if (mCamera != null) {
// 停止预览
mCamera.stopPreview();
// 释放设备,这样其他APP就可以继续使用相机设备
mCamera.release();
mCamera = null;
}
}
}
接下来就是在成功打开相机设备后将相机实例关联到预览视图。
if(mPreview != null){
mPreview.setCamera(mCamera);
}
try {
mCamera.setPreviewDisplay(mHolder);
} catch (IOException e) {
e.printStackTrace();
}
上面的工作做完了之后我们就可以正式拍摄照片了。使用Camera.takePicture()
方法可以执行拍摄照片的动作,该方法可以传入多个回调函数。看API文档:
我们现在只做最简单的拍照,实现代码如下:
private static final int K_STATE_FROZEN = 0;
private static final int K_STATE_PREVIEW = 1;
private static final int K_STATE_BUSY = 2;
......
mCaptureBtn = (Button)findViewById(R.id.btn_shutter);
mCaptureBtn.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
if(mPreviewState == K_STATE_FROZEN){
mCamera.startPreview();
mPreviewState = K_STATE_PREVIEW;
}else if(mPreviewState == K_STATE_PREVIEW){
mCamera.takePicture(null,rawCallBack,null);
mPreviewState = K_STATE_BUSY;
}
}
});
由于在拍照完成后相机会自动进入冻结状态,我们需要根据状态来重新启动预览状态。上面的代码使用了拍照按钮来响应两种不同的操作,若当前为预览状态,点击按钮将立即拍照,此后相机进入冻结状态,再次点击按钮就只是恢复预览状态,不执行拍照。拍照时我们只传了一个回调函数rawCallBack
,该回调类定义如下:
private Camera.PictureCallback rawCallBack = new Camera.PictureCallback() {
@Override
public void onPictureTaken(byte[] data, Camera camera) {
mPreviewState = K_STATE_FROZEN;
//TODO:data中保存的是照片的原始数据,我们可以对其进行处理,保存为照片或者其他事情
}
};
到此为止,我们已经控制相机拍摄一张完整的照片了。当然,想要界面更加友好,更易使用,我们需要添加一些细节,比如添加快门声,添加聚焦效果,添加人脸检测功能等等。这些功能,系统都有提供API,我们只需要实现响应的界面反馈出来就行了,这里就不详细介绍了。