Androd自定义控件(五)打造自己的Camera

使用surfaceview自定义相机,同时把自己踩过的坑分享给大家,希望大家有所收获。

写在前面的话

前一阵子负责一个自定义相机进行拍照并在另一个页面进行人脸识别的模块,相机部分需求并不怎么复杂,可以切换前后摄像头,可以拍照并把照片返回上一个页面。由于没有怎么接触过自定义相机的部分,而网上的一些资料又不全,踩了不少坑。故在这里总结一下,希望对大家有所帮助,同时把自定义控件系列的最后一个坑填上(surfaceview)。效果图如下:

这里写图片描述

Android中开发相机的两种方式

Android系统提供了两种使用手机相机资源实现拍摄功能的方法,一种是直接通过Intent调用系统相机组件,这种方法快速方便,适用于直接获得照片的场景,如上传相册,微博、朋友圈发照片等。另一种是使用相机API来定制自定义相机,这种方法适用于需要定制相机界面或者开发特殊相机功能的场景,如需要对照片做裁剪、滤镜处理,添加贴纸,表情,地点标签等。

调用系统自带相机

关于系统自带相机的调用非常简单,这里我就不过多叙述了,具体可以参考谷歌的Training。我只说容易被大家忽视的几个点:

  • 如果我们的应用使用相机,但相机并不是应用的正常运行所必不可少的组件,可以将权限声明中的android:required设置为”false”。这样的话,Google Play 也会允许没有相机的设备下载该应用。当然我们有必要在使用相机之前通过调用hasSystemFeature(PackageManager.FEATURE_CAMERA)方法来检查设备上是否有相机。如果没有,我们应该禁用和相机相关的功能!

  • 在调用startActivityForResult()方法之前,先调用resolveActivity(),这个方法会返回能处理该Intent的第一个Activity(译注:即检查有没有能处理这个Intent的Activity)。执行这个检查非常重要,因为如果在调用startActivityForResult()时,没有应用能处理你的Intent,应用将会崩溃。所以只要返回结果不为null,使用该Intent就是安全的。

使用Android框架所提供的API来直接控制相机硬件

使用API来控制相机我们需要用到关键类和接口:

  • 使用Camera对象来控制相机
  • 使用SurfaceView来展现照相机采集的图像
  • 通过surfaceholder来控制surfac的尺寸和格式,修改surface的像素,监视surface的变化等等
  • 通过SurfaceHolder.Callback 接口,监听surface状态变化

接下来我们分为以下三部分来介绍:关键类以及接口的作用和方法,Camera控制拍照步骤,自定义相机容易踩到的坑以及解决办法。

API说明

Camera :最主要的类,用于管理和操作camera资源。它提供了完整的相机底层接口,支持相机资源切换,设置预览/拍摄尺寸,设定光圈、曝光、聚焦等相关参数,获取预览/拍摄帧数据等功能,主要方法有以下这些:

  • open():获取camera实例。
  • setPreviewDisplay(SurfaceHolder):绑定绘制预览图像的surface。surface是指向屏幕窗口原始图像缓冲区(raw buffer)的一个句柄,通过它可以获得这块屏幕上对应的canvas,进而完成在屏幕上绘制View的工作。通过surfaceHolder可以将Camera和surface连接起来,当camera和surface连接后,camera获得的预览帧数据就可以通过surface显示在屏幕上了。
  • setPrameters设置相机参数,包括前后摄像头,闪光灯模式、聚焦模式、预览和拍照尺寸等。
  • startPreview():开始预览,将camera底层硬件传来的预览帧数据显示在绑定的surface上。
  • stopPreview():停止预览,关闭camra底层的帧数据传递以及surface上的绘制。
  • release():释放Camera实例
  • takePicture(Camera.ShutterCallback shutter, Camera.PictureCallback raw, Camera.PictureCallback jpeg):这个是实现相机拍照的主要方法,包含了三个回调参数。shutter是快门按下时的回调,raw是获取拍照原始数据的回调,jpeg是获取经过压缩成jpg格式的图像数据的回调。

SurfaceView :用于绘制相机预览图像的类,提供给用户实时的预览图像。普通的view以及派生类都是共享同一个surface的,所有的绘制都必须在UI线程中进行。而surfaceview是一种比较特殊的view,它并不与其他普通view共享surface,而是在内部持有了一个独立的surface,surfaceview负责管理这个surface的格式、尺寸以及显示位置。由于UI线程还要同时处理其他交互逻辑,因此对view的更新速度和帧率无法保证,而surfaceview由于持有一个独立的surface,因而可以在独立的线程中进行绘制,因此可以提供更高的帧率。自定义相机的预览图像由于对更新速度和帧率要求比较高,所以比较适合用surfaceview来显示。

SurfaceHolder :surfaceholder是控制surface的一个抽象接口,它能够控制surface的尺寸和格式,修改surface的像素,监视surface的变化等等,surfaceholder的典型应用就是用于surfaceview中。surfaceview通过getHolder()方法获得surfaceholder 实例,通过后者管理监听surface 的状态。

SurfaceHolder.Callback 接口 :负责监听surface状态变化的接口,有三个方法:

  • surfaceCreated(SurfaceHolder holder):在surface创建后立即被调用。在开发自定义相机时,可以通过重载这个函数调用camera.open()、camera.setPreviewDisplay(),来实现获取相机资源、连接camera和surface等操作。
  • surfaceChanged(SurfaceHolder holder, int format, int width, int height):在surface发生format或size变化时调用。在开发自定义相机时,可以通过重载这个函数调用camera.startPreview来开启相机预览,使得camera预览帧数据可以传递给surface,从而实时显示相机预览图像。
  • surfaceDestroyed(SurfaceHolder holder):在surface销毁之前被调用。在开发自定义相机时,可以通过重载这个函数调用camera.stopPreview(),camera.release()来实现停止相机预览及释放相机资源等操作。

Camera控制拍照的过程

  1. 调用Camera的open()方法打开相机。
  2. 调用Camera的getParameters()获取拍照参数,该方法返回一个Cmera.Parameters对象。
  3. 调用Camera.Parameters对象对照相的参数进行设置。
  4. 调用Camera的setParameters(),并将Camera.Parameters对象作为参数传入,这样就可以对拍照进行参数控制,Android2.3.3以后不用设置。
  5. 调用Camerade的startPreview()的方法开始预览取景,在之前需要调用Camera的setPreviewDisplay(SurfaceHolder holder)设置使用哪个SurfaceView来显示取得的图片。
  6. 调用Camera的takePicture()方法进行拍照。
  7. 程序结束时,要调用Camera的stopPreview()方法停止预览,并且通过Camera.release()来释放资源。

具体实现代码戳后面链接

踩坑与填坑

预览方向

先看下官方文档的说明

Most camera applications lock the display into landscape mode because that is the natural orientation of the camera sensor. This setting does not prevent you from taking portrait-mode photos, because the orientation of the device is recorded in the EXIF header. The setCameraDisplayOrientation() method lets you change how the preview is displayed without affecting how the image is recorded. However, in Android prior to API level 14, you must stop your preview before changing the orientation and then restart it.

大多数相机程序会锁定预览为横屏状态,因为该方向是相机传感器的自然方向。当然这一设定并不会阻止我们去拍竖屏的照片,因为设备的方向信息会被记录在EXIF头中。setCameraDisplayOrientation()方法可以让你在不影响照片拍摄过程的情况下,改变预览的方向。然而,对于Android API Level 14及以下版本的系统,在改变方向之前,我们必须先停止预览,然后再去重启它。

SurfaceView预览图像拉伸变形,拍摄照片尺寸不对

说明这个问题之前,同样先说一下几个跟相机有关的尺寸。

  • SurfaceView尺寸 :即自定义相机应用中用于显示相机预览图像的View的尺寸,当它铺满全屏时就是屏幕的大小。这里surfaceview显示的预览图像暂且称作手机预览图像。

  • Previewsize :相机硬件提供的预览帧数据尺寸。预览帧数据传递给SurfaceView,实现预览图像的显示。这里预览帧数据对应的预览图像暂且称作相机预览图像。

  • Picturesize :相机硬件提供的拍摄帧数据尺寸。拍摄帧数据可以生成位图文件,最终保存成.jpg或者.png等格式的图片。这里拍摄帧数据对应的图像称作相机拍摄图像。图4说明了以上几种图像及照片之间的关系。手机预览图像是直接提供给用户看的图像,它由相机预览图像生成,拍摄照片的数据则来自于相机拍摄图像。

原因是没有正确设置比例 parameter.setPictureSize(width,height),这个比例不是你决定的,要先通过camera.getParameters().getSupportedPictureSizes()获得手机支持的尺寸。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
/**
* 设置照片格式
*/
private void setParameter() {
Camera.Parameters parameters = camera.getParameters(); // 获取各项参数
parameters.setPictureFormat(PixelFormat.JPEG); // 设置图片格式
parameters.setJpegQuality(100); // 设置照片质量
//获得相机支持的照片尺寸,选择合适的尺寸
List<Camera.Size> sizes = parameters.getSupportedPictureSizes();
int maxSize = Math.max(display.getWidth(), display.getHeight());
int length = sizes.size();
if (maxSize > 0) {
for (int i = 0; i < length; i++) {
if (maxSize <= Math.max(sizes.get(i).width, sizes.get(i).height)) {
parameters.setPictureSize(sizes.get(i).width, sizes.get(i).height);
break;
}
}
}
List<Camera.Size> ShowSizes = parameters.getSupportedPreviewSizes();
int showLength = ShowSizes.size();
if (maxSize > 0) {
for (int i = 0; i < showLength; i++) {
if (maxSize <= Math.max(ShowSizes.get(i).width, ShowSizes.get(i).height)) {
parameters.setPreviewSize(ShowSizes.get(i).width, ShowSizes.get(i).height);
break;
}
}
}
camera.setParameters(parameters);
}

前置摄像头的镜像效果

  • Android 相机硬件有个特殊设定,就是对于前置摄像头,在展示预览视图时采用类似镜面的效果,显示的是摄像头成像的镜像。而拍摄出的照片则仍采用摄像头成像。看到这里,大家可能会有些怀疑,不妨现在就试试自己 Android 手机上的前置摄像头,对比下预览图像和拍摄出照片的区别。这是由于底层相机在传递前置摄像头预览数据时做了水平翻转变换,即将x方向镜像翻转180度。这个变化对之前竖屏预览的方向也会造成影响,本来对于后置摄像头旋转90度即可使预览视图正确,而对前置摄像头,如果也旋转90度的话,看到的预览图像则是上下颠倒的(因为x方向翻转了180度),因此必须再旋转180度,才能显示正确。
    解决方案,在保存图片的时候根据选择的摄像头做对应的翻转。
1
2
3
4
5
6
7
8
9
10
11
12
//将照片改为竖直方向
Bitmap bitmap = BitmapFactory.decodeByteArray(data, 0, data.length);
Matrix matrix = new Matrix();
switch (cameraPosition) {
case 0://前
matrix.preRotate(270);
break;
case 1:
matrix.preRotate(90);
break;
}
bitmap = Bitmap.createBitmap(bitmap, 0, 0, bitmap.getWidth(), bitmap.getHeight(), matrix, true);
  • 同时在开发的过程中发现了一个有趣的东西,我们用前置摄像头拍出来的照片其实是左右翻转的。但我用小米自带的相机测试发现,当摄像头中有人脸出现的时候,相机会做左右翻转的操作,以给用户更好的体验。

源码戳这里。

任磊_Coder wechat
关注博主是一种态度,评论博主是一种欣赏。
坚持原创技术分享,您的支持将鼓励我继续创作!