前言
在Android SurfaceView解析中我介绍了下如何使用SurfaceView,不过在我们日常的开发中,除非是游戏相关,一般都不会直接在SurfaceView上面绘制图像,而是让系统"绘制"在我们的SurfaceView中,这篇博客我就来介绍下如何让相机绘制在我们的SurfaceView上面,并实现如下功能。
目标:前后置相机预览、前后置相机拍照并保存为图片以及一张加了水印的图片。
知识点概括
1、相机的基本使用
2、相机前后置判断以及相机支持尺寸等获取
3、相机预览画面的比例适配
4、相机的拍照处理以及照片加水印
相机的基本使用
使用系统相机主要是android.hardware.Camera类,首先需要声明权限
<uses-permission android:name="android.permission.CAMERA" /> <uses-feature android:name="android.hardware.camera" /> <uses-feature android:name="android.hardware.camera.autofocus" />
1、获取相机,通过Camera.open()即可获取一个相机对象。
// open() 无参数 默认后置摄像头,没有后置则返回null // open(int cameraId) 可选开启摄像头 // 后置摄像头:CameraInfo.CAMERA_FACING_BACK = 0 // 前置摄像头:CameraInfo.CAMERA_FACING_FRONT = 1 mCamera = Camera.open(CameraInfo.CAMERA_FACING_FRONT);
2、设置相机的参数,使用mCamera.getParameters();方法可以获取默认的相机参数,然后根据我们的需要进行设置,最后再使用mCamera.setParameters(param);方法设置参数
Camera.Parameters param = mCamera.getParameters(); //在这里进行设置 mCamera.setParameters(param);
3、设置相机的预览窗口,使用 mCamera.setPreviewDisplay(SurfaceHolder surfaceHolder);方法告诉系统相机将画面绘制到我们的SurfaceView上面。关于SurfaceView的用法请看这里。
4、开启/关闭相机预览,使用mCamera.startPreview(); / mCamera.stopPreview();函数即可开始预览和关闭预览。
5、相机的销毁,由于在同一时刻,系统只能允许一个进程打开相机,所以在我们不需要使用相机的时候,我们需要将其销毁,销毁只需要调用mCamera.release();方法即可。关于销毁相机的时机,一般是在onPause()时销毁,在onResume()时重新打开。
下面贴出的代码分别对应相机的开启/关闭。其中用到的方法在后面介绍。
/**
* 打开摄像头并开始预览
*/
public void startCamera() {
if (!mIsCreated) {
Log.e(TAG, "surfaceview not create!!!");
return;
}
// 如果没有摄像头
if (Camera.getNumberOfCameras() == 0) {
Toast.makeText(getContext(), "没有发现摄像头设备", Toast.LENGTH_SHORT).show();
return;
}
// 只有一个摄像头,那么就不切换摄像头了,
if (Camera.getNumberOfCameras() == 1) {
CameraInfo cameraInfo = new CameraInfo();
Camera.getCameraInfo(0, cameraInfo);
FACING_MODE = cameraInfo.facing;
}
// open() 无参数 默认后置摄像头,没有后置则返回null
// open(int cameraId) 可选开启摄像头
// 后置摄像头:CameraInfo.CAMERA_FACING_BACK = 0
// 前置摄像头:CameraInfo.CAMERA_FACING_FRONT = 1
mCamera = Camera.open(FACING_MODE);
if (mCamera != null) {
try {
mCamera.setPreviewDisplay(mSurfaceHolder);
Camera.Parameters param = mCamera.getParameters();
outputDeviceInfo(param);
param.setPictureFormat(ImageFormat.JPEG);
// 设置大小和方向等参数
Size bestPerviewSize = getBestSupportedSize(param.getSupportedPreviewSizes(), getWidth(), getHeight());
param.setPreviewSize(bestPerviewSize.width, bestPerviewSize.height);
Size bestPictureSize = getBestSupportedSize(param.getSupportedPictureSizes(), getWidth(), getHeight());
param.setPictureSize(bestPictureSize.width, bestPictureSize.height);
param.setFocusMode(Camera.Parameters.FOCUS_MODE_CONTINUOUS_VIDEO);
// 后拍照的结果需要旋转90度
if (FACING_MODE == CameraInfo.CAMERA_FACING_BACK) {
param.setRotation(90);
}
// 前置拍照的结果需要旋转270度
if (FACING_MODE == CameraInfo.CAMERA_FACING_FRONT) {
param.setRotation(270);
}
// 预览需要旋转90度
mCamera.setDisplayOrientation(90);
mCamera.setParameters(param);
mCamera.startPreview();
} catch (IOException e) {
e.printStackTrace();
Toast.makeText(getContext(), "打开摄像异常", Toast.LENGTH_SHORT).show();
releaseCamera();
}
}
}
/**
* 销毁摄像头
*/
public void releaseCamera() {
if (mCamera != null) {
mCamera.stopPreview();
mCamera.release();
mCamera = null;
}
}
6、拍照、拍照只需要调用 mCamera.takePicture(Camera.ShutterCallback, Camera.PictureCallback, Camera.PictureCallback, Camera.PictureCallback)即可。需要注意的是,调用这个函数会让预览停止,所以在我们拍照完毕以后,需要再次调用mCamera.startPreview();方法开启预览。关于这个方法的介绍,将会在本文最后面详细介绍。
7、切换摄像头,切换摄像头只需要销毁原来的然后重新open一个即可。注意需要判断下摄像头的个数,万一只有一个就悲剧了。
相机前后置判断以及相机支持尺寸等获取
1、判断相机的个数以及前后置判断
//相机个数
int numberOfCameras = Camera.getNumberOfCameras()
//通过Camera.getCameraInfo方法获取摄像头信息
for (int i = 0; i < numberOfCameras; i++) {
CameraInfo cameraInfo = new CameraInfo();
Camera.getCameraInfo(i, cameraInfo);
// 前置摄像头 = 1
if (cameraInfo.facing == CameraInfo.CAMERA_FACING_FRONT) {
Log.i(TAG, "cameraId = " + i + ",Camera is CameraInfo.CAMERA_FACING_FRONT," + "Camera orientation = "
+ cameraInfo.orientation);
}
// 后置摄像头 = 0
else if (cameraInfo.facing == CameraInfo.CAMERA_FACING_BACK) {
Log.i(TAG, "cameraId = " + i + ",Camera is CameraInfo.CAMERA_FACING_FRONT," + "Camera orientation = "
+ cameraInfo.orientation);
} else {
Log.e(TAG, "unkonw Camera");
}
}
2、摄像头的尺寸
我们可以通过如下代码获取摄像头支持的预览尺寸和相片尺寸,这些尺寸非常重要,因为在我们开启摄像头的时候,需要使用param.setPreviewSize(int width, int height)和param.setPictureSize(int width, int height)方法来设置宽高,如果随便设置,那么会造成黑屏或者直接崩溃。
Camera.Parameters param = mCamera.getParameters(); //设备支持的图片尺寸 List<Size> pictureSizes = param.getSupportedPictureSizes(); //设备支持的预览尺寸 List<Size> previewSizes = param.getSupportedPreviewSizes();
相机预览画面的比例适配
代码如下,首先对比所有支持的尺寸是否有和SurfaceView相同的,如果没有,则选择比例最接近SurfaceView的尺寸。
/**
* 通过对比得到与宽高比最接近的尺寸(如果有相同尺寸,优先选择)
*
* @param supportedSizeList 需要对比的预览尺寸列表
* @param surfaceWidth 需要被进行对比的原宽
* @param surfaceHeight 需要被进行对比的原高
* @return 得到与原宽高比例最接近的尺寸
*/
protected Camera.Size getBestSupportedSize(List<Size> supportedSizeList, int surfaceWidth, int surfaceHeight) {
Log.i(TAG, "surfaceWidth = " + surfaceWidth + ",surfaceHeight = " + surfaceHeight);
int reqWidth = surfaceWidth;
int reqHeight = surfaceHeight;
boolean isPortrait = getResources().getConfiguration().orientation == Configuration.ORIENTATION_PORTRAIT;
// 当屏幕为垂直的时候需要把宽高值进行调换,保证宽大于高
if (isPortrait) {
reqWidth = surfaceHeight;
reqHeight = surfaceWidth;
}
// 先查找preview中是否存在与surfaceview相同宽高的尺寸
for (Camera.Size size : supportedSizeList) {
if ((size.width == reqWidth) && (size.height == reqHeight)) {
return size;
}
}
// 如果没有尺寸相同的,则找与传入的宽高比最接近的size
float reqRatio = ((float) reqWidth) / reqHeight;
float curRatio, deltaRatio;
float deltaRatioMin = Float.MAX_VALUE;
Camera.Size retSize = null;
for (Camera.Size size : supportedSizeList) {
curRatio = ((float) size.width) / size.height;
deltaRatio = Math.abs(reqRatio - curRatio);
if (deltaRatio < deltaRatioMin) {
deltaRatioMin = deltaRatio;
retSize = size;
}
}
Log.i(TAG, "retSize.width = " + retSize.width + ",retSize.height = " + retSize.height);
return retSize;
}
相机的拍照处理以及照片加水印
在上面说到了,拍照只需要调用mCamera.takePicture(Camera.ShutterCallback, Camera.PictureCallback, Camera.PictureCallback, Camera.PictureCallback)即可,三个参数分别对应对应相机拍照前,相机的原始数据回调,相机的拍摄结果数据回调,我们在拍摄结果回调中,将数据直接存文件即可实现照片的保存,当然,我们也可以对这个数据进行处理再保存,下面的代码分别保存了原始图片以及加水印后的图片。
mCamera.takePicture(new ShutterCallback() {
// 拍照之前的工作
@Override
public void onShutter() {
Log.i(TAG, "before takePicture");
}
}, new PictureCallback() {
// 照片的二进制数据
@Override
public void onPictureTaken(byte[] data, Camera camera) {
Log.i(TAG, "doing onPictureTaken");
}
}, new PictureCallback() {
// 最终的照片数据
@Override
public void onPictureTaken(byte[] data, Camera camera) {
Log.i(TAG, "done onPictureTaken");
outOriginPicture(data);
outWatermarkPicture(data);
Toast.makeText(getContext(), "保存原始图片和水印图片成功", Toast.LENGTH_SHORT).show();
// 拍照以后会停止预览,所以继续预览
mCamera.startPreview();
}
});
}
/**
* 保存添加了水印的图片
*
* @param data 图片的元素数据
*/
private void outWatermarkPicture(byte[] data) {
File externalStorageDirectory = Environment.getExternalStorageDirectory();
File picPath = new File(externalStorageDirectory, "img_watermake.jpg");
Bitmap bitmap = BitmapFactory.decodeByteArray(data, 0, data.length);
Bitmap createBitmap = Bitmap.createBitmap(bitmap.getWidth(), bitmap.getHeight(), Bitmap.Config.RGB_565);
Canvas canvas = new Canvas(createBitmap);
canvas.drawBitmap(bitmap, 0, 0, null);
Paint paint = new Paint();
paint.setStyle(Paint.Style.FILL);
paint.setColor(Color.BLUE);
paint.setDither(true);
paint.setAntiAlias(true);
paint.setTextSize(150);
canvas.drawText("水印文字", 0, 150, paint);
try {
FileOutputStream out = new FileOutputStream(picPath);
createBitmap.compress(CompressFormat.JPEG, 100, out);
out.close();
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
/**
* 保存原始图片
*
* @param data 原始图片的二进制数据
*/
private void outOriginPicture(byte[] data) {
File externalStorageDirectory = Environment.getExternalStorageDirectory();
File picPath = new File(externalStorageDirectory, "img.jpg");
try {
FileOutputStream out = new FileOutputStream(picPath);
out.write(data);
out.close();
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
那些坑
1、如果你的android:targetSdkVersion="23"或者更高,那么相机权限需要动态申请
2、预览尺寸设置很重要,解决方法参考上面的相机预览画面的比例适配
3、在上面的代码中,你一定看到了很多90度,270度,之所以要设置旋转这个度数,是因为Android默认横屏是0度,而我们一般是竖着拍,所以后置需要旋转90度,由于前置是后置的180度,所以前置需要旋转270度!!!
4、将相机所在的Activity设置为只能竖屏会让你觉得这个世界更美好!!!不然横竖屏切换会让你崩溃。
Demo下载地址:https://github.com/CB2Git/BlogDemoRepository/tree/master/TestSurfaceViewCamera