前言
在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