通常情况下,调用android系统相机基本上可以满足拍照的需求,而自定义相机的需求一般来自于开发自己的相机应用,今天我们来简单聊聊android自定义相机的实现,限于篇幅,我们上篇只讨论android.hardware.Camera,下篇我会和大家一起讨论一下android.hardware.Camera2
demo代码
首先简单看一下用到的类有哪些:
类名 | 介绍 | 注释 |
---|---|---|
Camera | android 5.0 之前控制相机硬件是通过这个Camera类实现的 | deprecated in API level 21. |
Camera.Parameters | 控制相机的参数,如对焦,闪光灯等 | deprecated in API level 21. |
SurfaceView | 专注于绘制应用层内嵌浮层的类,这里我们用它来预览镜头的取景 | \ |
SurfaceHolder | 持有一个展示的surface的抽象接口。允许你控制surface的尺寸和格式,编辑surface中的像素和监听surface的改变。 | \ |
SurfaceHolder.Callback | 控制该接口的客户端能够接收surface改变的信息。 | \ |
<uses-permission android:name="android.permission.CAMERA" />
<uses-feature android:name="android.hardware.camera" />
<uses-feature android:name="android.hardware.camera.autofocus" />
在camera的官方接口文档中,给出了控制camera的10个步骤,很详细,我们这边侧重于实现。
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/camera_container"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".Camera.CameraActivity">
<FrameLayout
android:id="@+id/camera_preview"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_alignParentLeft="true"
android:layout_alignParentTop="true"/>
<LinearLayout
android:id="@+id/control"
android:layout_width="match_parent"
android:layout_height="112dp"
android:layout_alignParentBottom="true"
android:background="@color/control_background"
android:gravity="center"
android:orientation="horizontal">
<Button
android:id="@+id/camera_take_photo"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:text="@string/picture" />
LinearLayout>
RelativeLayout>
/**
* 初始化相机
*/
private void initCamera() {
if(!getPackageManager().hasSystemFeature(
PackageManager.FEATURE_CAMERA)) {
//TODO 未检测到系统的相机
} else {
//获取cameraId ,在api8及以前,直接调用Camera.open()方法会打开后置摄像头
//获取系统后置摄像头的id(工具方法,后面附代码)
int cameraId = CameraUtil.findBackFacingCamera();
if (!CameraUtil.isCameraIdValid(cameraId)) {
//TODO 检测camera id无效
} else {
//是否可以安全打开相机
if (safeCameraOpen(cameraId)) {
mCamera.startPreview(); //开启相机预览
mPreview.setCamera(mCamera);
} else {
//TODO 无法安全打开相机
}
}
}
//打开相机的操作延迟到onResume()方法里面去执行,这样可以使得代码更容易重用,还能保持控制流程更为简单。当然也可以另起线程处理
@Override
public void onResume() {
super.onResume();
initCamera();
}
附上获取相机id的工具方法:
public final class CameraUtil {
public static final int INVALID_CAMERA_ID = -1;
/**
* 获取前置摄像头id
* @return
*/
public static int findFrontFacingCamera() {
int cameraId = INVALID_CAMERA_ID;
// Search for the front facing camera
int numberOfCameras = Camera.getNumberOfCameras();
for (int i = 0; i < numberOfCameras; i++) {
Camera.CameraInfo info = new Camera.CameraInfo();
Camera.getCameraInfo(i, info);
if (info.facing == Camera.CameraInfo.CAMERA_FACING_FRONT) {
Log.d("CameraUtil", "Camera found");
cameraId = i;
break;
}
}
return cameraId;
}
/**
* 获取后置摄像头id
* @return
*/
public static int findBackFacingCamera() {
int cameraId = INVALID_CAMERA_ID;
// Search for the front facing camera
int numberOfCameras = Camera.getNumberOfCameras();
for (int i = 0; i < numberOfCameras; i++) {
Camera.CameraInfo info = new Camera.CameraInfo();
Camera.getCameraInfo(i, info);
if (info.facing == Camera.CameraInfo.CAMERA_FACING_BACK) {
Log.d("CameraUtil", "Camera found");
cameraId = i;
break;
}
}
return cameraId;
}
public static boolean isCameraIdValid(int cameraId) {
return cameraId != INVALID_CAMERA_ID;
}
}
处理打开相机时可能出现的异常:
/**
* 获取Camera,并加入开启检测
*
* @param id 相机id
* @return
*/
private boolean safeCameraOpen(int id) {
boolean qOpened = false;
try {
releaseCameraAndPreview();
mCamera = Camera.open(id);
qOpened = (mCamera != null);
} catch (Exception e) {
e.printStackTrace();
}
return qOpened;
}
这样不出意外,我们成功的打开了相机 —
这里包括了设置相机参数,设置相机方向,初始化SurfaceHolder从而来setPreviewDisplay(SurfaceHolder) ,完成这一步,即可实现取景的预览。
一般的,我们可以在布局中放置一个默认的SurfaceView,也可以通过addView()方法将SurfaceView实例添加到指定的容器中。
这里我们通过继承SurfaceView,派生一个自定义的CameraPreview,同时实现SurfaceHolder.Callback接口便于我们对surface进行控制。
public class CameraPreview extends SurfaceView implements SurfaceHolder.Callback {
private static final String TAG = "TAG";
/**
* 控制相机方向
*/
private static final SparseIntArray ORIENTATIONS = new SparseIntArray();
static {
ORIENTATIONS.append(Surface.ROTATION_0, 90);
ORIENTATIONS.append(Surface.ROTATION_90, 0);
ORIENTATIONS.append(Surface.ROTATION_180, 270);
ORIENTATIONS.append(Surface.ROTATION_270, 180);
}
private SurfaceHolder mHolder;
private Camera mCamera;
//持有Activity引用,为了获取屏幕方向,改成内部类会比较好
private Activity mActivity;
public CameraPreview(Activity activity) {
super(activity);
mActivity = activity;
mHolder = getHolder();
mHolder.addCallback(this);
if(Build.VERSION.SDK_INT < Build.VERSION_CODES.HONEYCOMB) {
//API 11及以后废弃,需要时自动配置
mHolder.setType(SurfaceHolder.SURFACE_TYPE_PUSH_BUFFERS);
}
}
public void setCamera(Camera camera) {
mCamera = camera;
}
/**
* 刷新相机
*/
private void refreshCamera() {
if (mCamera != null) {
requestLayout();
//获取当前手机屏幕方向
int rotation = mActivity.getWindowManager()
.getDefaultDisplay().getRotation();
//调整相机方向
mCamera.setDisplayOrientation(
ORIENTATIONS.get(rotation));
// 设置相机参数
mCamera.setParameters(settingParameters());
}
}
/**
* 配置相机参数
* @return
*/
private Camera.Parameters settingParameters() {
// 获取相机参数
Camera.Parameters params = mCamera.getParameters();
List focusModes = params.getSupportedFocusModes();
//设置持续的对焦模式
if (focusModes.contains(
Camera.Parameters.FOCUS_MODE_CONTINUOUS_PICTURE)) {
params.setFocusMode(Camera.Parameters.FOCUS_MODE_CONTINUOUS_PICTURE);
}
//设置闪光灯自动开启
if (focusModes.contains(Camera.Parameters.FLASH_MODE_AUTO)) {
params.setFocusMode(Camera.Parameters.FLASH_MODE_AUTO);
}
return params;
}
@Override
public void surfaceCreated(SurfaceHolder holder) {
// The Surface has been created, now tell the camera where to draw the preview.
try {
if(mCamera != null) {
//surface创建,设置预览SurfaceHolder
mCamera.setPreviewDisplay(holder);
//开启预览
mCamera.startPreview();
}
} catch (IOException e) {
Log.d(TAG, "Error setting camera preview: " + e.getMessage());
}
}
@Override
public void surfaceDestroyed(SurfaceHolder holder) {
// empty. Take care of releasing the Camera preview in your activity.
}
@Override
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
refreshCamera();
// start preview with new settings
try {
mCamera.setPreviewDisplay(mHolder);
mCamera.startPreview();
} catch (Exception e) {
Log.d(TAG, "Error starting camera preview: " + e.getMessage());
}
}
}
CameraPreview里面涉及到预览主要的逻辑,一些如对焦模式或闪光灯模式等相机参数,大家可以自己参照API文档。接下来只要在onCreate()方法里,将我们的SurfaceView加入我们设置好的布局容器中即可。
mPreview = new CameraPreview(this);
FrameLayout vPreview = (FrameLayout) findViewById(R.id.camera_preview);
vPreview.addView(mPreview);
拍照很简单,只需要调用takePicture (Camera.ShutterCallback shutter, Camera.PictureCallback raw, Camera.PictureCallback jpeg)方法
Camera.ShutterCallback 发生在照片刚被捕获的一瞬间
后面两个回调分别发生在原始的照片数据/压缩的jpeg照片数据可用时,api5之后,额外提供了一个4个回调参数的方法,大家自己看文档就好。这里我们简化处理,只提供最后一个回调:
//触发一个异步的图片捕获回调
mCamera.takePicture(null, null, this);
...
@Override
public void onPictureTaken(final byte[] data, Camera camera) {
File pictureFile = ...; //自行创建一个file文件用于保存照片
OutputStream output = null;
try {
output = new FileOutputStream(pictureFile);
output.write(data);
} catch (IOException e) {
e.printStackTrace();
} finally {
if (output != null) {
try {
output.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
//保存成功后的处理
resetCamera();
}
其实很简单,只需要将字节数据写进文件里即可,如果保存完图片后想继续拍照,这时候就需要重置一下相机,因为拍照完,取景预览默认定格在取景页,提供一下resetCamera()方法,供大家参考
/**
* 重置相机
*/
private void resetCamera() {
mCamera.startPreview();
mPreview.setCamera(mCamera);
}
/**
* 释放相机和预览
*/
private void releaseCameraAndPreview() {
mPreview.setCamera(null);
if (mCamera != null) {
mCamera.release();
mCamera = null;
}
}
如果我们开启相机是activity的onResume()方法中,那么我们可以在onPause()方法中释放相机和预览资源
@Override
protected void onPause() {
super.onPause();
releaseCameraAndPreview();
}
大家看,这时图片还是以字节的形式存在,只有当用户点击保存,系统才会保存图片。所以文件的创建时间其实并不是拍照的那一刻(虽然也差不多)。所以自己控制camera拍照的话就可以拿到相对精准的照片拍摄时间。
留个坑:android中如何获取照片(文件)的创建时间?
目测:基于linux的android也只能拿到lastModified time
(2). SurfaceView 生命周期的控制
还是上一个图,例如我们拍照后进入图中状态,这时锁屏或者进入后台(触发Activity的onPause()方法),然后我们再返回应用,此时我们会发现,SurfaceView原本固定的取景完全恢复了。首先我们想到的处理方法是设置状态机,
定义整个拍照过程的不同状态,如:
/**
* 预览中
*/
private static final int STATE_PREVIEW = 0;
/**
* 等待保存
*/
private static final int STATE_WAITING_SAVE = 1;
/**
* 保存中
*/
private static final int STATE_SAVING = 2;
在onResume()和onPause()方法中,针对不同的状态,控制初始化/释放的过程。
但是,SurfaceView默认的生命周期和activity也是相关的,所以如果希望在上述操作中保持SurfaceView的状态,需要额外一些操作。
再留一个坑:SurfaceView生命周期与Activity的生命周期
(3). 拍照方向
android相机传感器默认是横向的,当竖屏拍照时,照片默认是旋转90度的
第三个坑:照片旋转了如何处理?