此项目仅是demo,宗旨在于学习,若用在生产环境需谨慎
现在很多app都会有拍照功能,一般调用系统进行拍照裁剪就能满足平时的需求,但有些场景或者特殊情况下如:持续不间断拍多张照片或者是进行人脸识别的时候,这时候之间调用系统原生相机拍照时不能满足自己的开发需求,就需要使用原生Camera来进行自定义开发,本文会采用android.hardware.CameraAPI来进行开发。在Android生态中,Camera是碎片化较为严重的一块,因为现在Android本身有三套API:
- Camera:Android 5.0以下
- Camera2:Android 5.0以上
- CameraX:基于Camera2API实现,极大简化在minsdk21及以上版本的实现过程
另外各家厂商(华为,OPPO,VIVO,小米)都对Camera2支持程度各不相同,从而导致需要花很大功夫来做适配工作。 做过相机的同学都知道,相机开发一般分为五个步骤:
- 检测相机资源,如果存在相机资源,就请求访问相机资源,否则就结束
- 创建预览界面,一般是继承SurfaceView并且实现SurfaceHolder接口的拍摄预览类,并且创建布局文件,将预览界面和用户界面绑定,进行实时显示相机预览图像
- 创建拍照监听器来响应用户的不同操作,如开始拍照,停止拍照等
- 拍照成功后保存文件,将拍摄获得的图像文件转成位图文件,并且保存输出需要的格式图片
- 释放相机资源,当相机不再使用时,进行释放
了解完开发步骤后,因为本文是针对Camera来进行开发,那下面先了解一些具体的类和方法。
Surface根据英文直译是表面的意思,在源码中有这样描述的:
/** * Handle onto a raw buffer that is being managed by the screen compositor. * * <p>A Surface is generally created by or from a consumer of image buffers (such as a *{@link android.graphics.SurfaceTexture},{@link android.media.MediaRecorder}, or *{@link android.renderscript.Allocation}), and is handed to some kind of producer (such as *{@link android.opengl.EGL14#eglCreateWindowSurface(android.opengl.EGLDisplay,android.opengl.EGLConfig,java.lang.Object,int[],int) OpenGL}, *{@link android.media.MediaPlayer#setSurface MediaPlayer}, or *{@link android.hardware.camera2.CameraDevice#createCaptureSession CameraDevice}) to draw * into.</p> */上面的意思:Surface是用来处理屏幕显示内容合成器所管理的原始缓存区工具,它通常由图像缓冲区的消费者来创建如(SurfaceTexture,MediaRecorder),然后被移交给生产者(如:MediaPlayer)或者显示到其上(如:CameraDevice),从上面可以得知:
- Surface通常由SurfaceTexture或者MediaRecorder来创建
- Surface最后显示在MediaPlayer或者CameraDevice上
- 通过Surface就可以获得管理原始缓存区的数据
- 原始缓冲区(rawbuffer)是用来保存当前窗口的像素数据
在Surface内有一个Canvas成员:
privatefinalCanvasmCanvas = newCompatibleCanvas();我们知道,画图都是在Canvas对象上来画的,因为Suface持有Canvas,那么我们可以这样认为,Surface是一个句柄,而Canvas是开发者画图的场所,就像黑板,而原生缓冲器(rawbuffer)是用来保存数据的地方,所有得到Surface就能得到其中的Canvas和原生缓冲器等其他内容。
SurfaceView简单理解就是Surface的View。
/** * Provides a dedicated drawing surface embedded inside of a view hierarchy. * You can control the format of this surface and, if you like, its size; the * SurfaceView takes care of placing the surface at the correct location on the * screen */意思就是
SurfaceView提供了嵌入视图层级中的专用surface,你可以控制surface的格式或者大小(通过SurfaceView就可以看到Surface部分或者全部内容),SurfaceView负责把surface显示在屏幕的正确位置。
publicclassSurfaceViewextendsViewimplementsViewRootImpl.WindowStoppedCallback{.... finalSurfacemSurface = newSurface(); // Current surface in use .... privatefinalSurfaceHoldermSurfaceHolder = newSurfaceHolder(){..... } }SurfaceView继承自View,并且其中有两个成员变量,一个是Surface对象,一个是SurfaceHolder对象,SurfaceView将Surface显示在屏幕上,SurfaceView通过SurfaceHolder得知Surface的状态(创建、变化、销毁),可以通过getHolder()方法获得当前SurfaceView的SurfaceHolder对象,然后就可以对SurfaceHolder对象添加回调来监听Surface的状态。
Surface是从Object派生而来,实现了Parcelable接口,看到Parcelable很容易让人想到数据,而SurfaceView就是用来展示Surface数据的,两者的关系可以用下面一张图来描述:
Surface是通过SurfaceView才能展示其中内容。
到这里也许大家会有一个疑问,SurfaceView和普通的View有什么区别?相机开发就一定要用SurfaceView吗?
首先普通的View和其派生类都是共享同一个Surface,所有的绘制必须在主线程(UI线程)进行,通过Surface获得对应的Canvas,完成绘制View的工作。
SurfaceView是特殊的View,它不与其他普通的view共享Surface,在自己内部持有Surface可以在独立的线程中进行绘制,在自定义相机预览图像这块,更新速度比较快和帧率要求比较高,如果用普通的View去更新,极大可能会阻塞UI线程,SurfaceView是在一个新起的线程去更新画面并不会阻塞UI线程。还有SurfaceView底层实现了双缓冲机制,双缓冲技术主要是为了解决反复局部刷新带来的闪烁问题,对于像游戏,视频这些画面变化特别频繁,如果前面没有显示完,程序又重新绘制,这样会导致屏幕不停得闪烁,而双缓冲及时会把要处理的图片在内存中处理后,把要画的东西先画到一个内存区域里,然后整体一次行画处理,显示在屏幕上。举例说明: 在Android中,如果自定义View大多数都会重写onDraw方法,onDraw方法并不是绘制一点显示一点,而是绘制完成后一次性显示到屏幕上。因为CPU访问内存的速度远远大于访问屏幕的速度,如果需要绘制大量复杂的图形时,每次都一个个从内存读取图形然后绘制到屏幕就会造成多次访问屏幕,这些效率会很低。为了解决这个问题,我们可以创建一个临时的Canvas对象,将图像都绘制到这个临时的Canvas对象中,绘制完成后通过drawBitmap方法绘制到onDraw方法中的Canvas对象中,这样就相对于Bitmap的拷贝过程,比直接绘制效率要高。
所以相机开发中最适合用SurfaceView来绘制。
/** * Abstract interface to someone holding a display surface. Allows you to * control the surface size and format, edit the pixels in the surface, and * monitor changes to the surface. This interface is typically available * through the{@link SurfaceView} class. * * <p>When using this interface from a thread other than the one running * its{@link SurfaceView}, you will want to carefully read the * methods *{@link #lockCanvas} and{@link Callback#surfaceCreated Callback.surfaceCreated()}. */publicinterfaceSurfaceHolder{.... publicinterfaceCallback{publicvoidsurfaceCreated(SurfaceHolderholder); publicvoidsurfaceChanged(SurfaceHolderholder, intformat, intwidth, intheight); publicvoidsurfaceDestroyed(SurfaceHolderholder); ... } }这是一个抽象的接口给持有
surface对象使用,允许你控制surface的大小和格式,编辑surface中的像素和监听surface的变化,这个接口通常通过SurfaceView这个类来获得。
另外SurfaceHolder中有一个Callback接口,这个接口有三个方法:
public void surfaceCreated(SurfaceHolder holder)
surface第一次创建回调
public void surfaceChanged(SurfaceHolder,int format,int width,int height)
surface变化的时候会回调
public void surfaceDestroyed(SurfaceHolder holder)
surface销毁的时候回调
除了上面Callback接口比较重要外,另外还有以下几个方法也比较重要:
public void addCallback(Callback callback)
为SurfaceHolder添加回调接口
public void removeCallback(Callback callback)
对SurfaceHolder移除回调接口
public Canvas lockCanvas()
获取Canvas对象并且对它上锁
public Canvas lockCanvas(Rect dirty)
获取一个Canvas对象,并且对它上锁,但是所动的内容是dirty所指定的矩形区域
public void unlockCanvasAndPost(Canvas canvas)
当修改Surface中的数据完成后,释放同步锁,并且提交改变,将新的数据进行展示,同时Surface中的数据会丢失,加锁的目的就是为了在绘制的过程中,Surface数据不会被改变。
public void setType(int type)
设置Surface的类型,类型有以下几种:
SURFACE_TYPE_NORMAL:用RAM缓存原生数据的普通Surface
SURFACE_TYPE_HARDWARE:适用于DMA(Direct memory access)引擎和硬件加速的Surface
SURFACE_TYPE_GPU:适用于GPU加速的Surface
SURFACE_TYPE_PUSH_BUFFERS:表明该Surface不包含原生数据,Surface用到的数据由其他对象提供,在Camera图像预览中就使用该类型的Surface,有Camera负责提供给预览Surface数据,这样图像预览会比较流畅,如果设置这种类型就不能调用lockCanvas来获取Canvas对象。
到这里,会发现
Surface、SurfaceView和SurfaceHolder就是典型的MVC模型。- Surface:原始数据缓冲区,MVC中的M
- SurfaceView:用来绘制Surface的数据,MVC中的V
- SurfaceHolder:控制Surface尺寸格式,并且监听Surface的更改,MVC中的C
上面三者的关系可以用下面一张图来表示:
查看源码时,发现android.hardware.cameragoogle不推荐使用了:
下面讲讲Camera最主要的成员和一些接口:
在Camera类里,CameraInfo是静态内部类:
/** * Information about a camera * 用来描述相机信息 * @deprecated We recommend using the new{@link android.hardware.camera2} API for new * applications. * 推荐在新的应用使用{android.hardware.camera2}API */@DeprecatedpublicstaticclassCameraInfo{/** * The facing of the camera is opposite to that of the screen. * 相机正面和屏幕正面相反,意思是后置摄像头 */publicstaticfinalintCAMERA_FACING_BACK = 0; /** * The facing of the camera is the same as that of the screen. * 相机正面和屏幕正面一致,意思是前置摄像头 */publicstaticfinalintCAMERA_FACING_FRONT = 1; /** * The direction that the camera faces. It should be * CAMERA_FACING_BACK or CAMERA_FACING_FRONT. * 摄像机面对的方向,它只能是CAMERA_FACING_BACK或者CAMERA_FACING_FRONT * */publicintfacing; /** * <p>The orientation of the camera image. The value is the angle that the * camera image needs to be rotated clockwise so it shows correctly on * the display in its natural orientation. It should be 0, 90, 180, or 270.</p> * orientation是相机收集图片的角度,这个值是相机采集的图片需要顺时针旋转才能正确显示自 * 然方向的图像,它必须是0,90,180,270中 * * * <p>For example, suppose a device has a naturally tall screen. The * back-facing camera sensor is mounted in landscape. You are looking at * the screen. If the top side of the camera sensor is aligned with the * right edge of the screen in natural orientation, the value should be * 90. If the top side of a front-facing camera sensor is aligned with * the right of the screen, the value should be 270.</p> * 举个例子:假设现在竖着拿着手机,后面摄像头传感器是横向(水平方向)的,你现在正在看屏幕 * 如果摄像机传感器的顶部在自然方向上右边,那么这个值是90度(手机是竖屏,传感器是横屏的)* * 如果前置摄像头的传感器顶部在手机屏幕的右边,那么这个值就是270度,也就是说这个值是相机图像顺时针 * 旋转到设备自然方向一致时的角度。 * */publicintorientation; /** * <p>Whether the shutter sound can be disabled.</p> * 是否禁用开门声音 */publicbooleancanDisableShutterSound};可能很多人对上面orientation解释有点懵,这里重点讲一下orientation,首先先知道四个方向:屏幕坐标方向,自然方向,图像传感器方向,相机预览方向。
在Android系统中,以屏幕左上角为坐标系统的原点(0,0)坐标,向右延伸是X轴的正方向,向下延伸是y轴的正方向,如上图所示。
每个设备都有一个自然方向,手机和平板自然方向不一样,在Android应用程序中,android:screenOrientation来控制activity启动时的方向,默认值unspecified即为自然方向,当然可以取值为:
- unspecified,默认值,自然方向
- landscape,强制横屏显示,正常拿设备的时候,宽比高长,这是平板的自然方向
- portrait,正常拿着设备的时候,宽比高短,这是手机的自然方向
- behind:和前一个Activity方向相同
- sensor:根据物理传感器方向转动,用户90度,180度,270度旋转手机方向
- sensorLandScape:横屏选择,一般横屏游戏会这样设置
- sensorPortait:竖屏旋转
- nosensor:旋转设备的时候,界面不会跟着旋转,初始化界面方向由系统控制
- user:用户当前设置的方向
默认的话:平板的自然方向是横屏,而手机的自然方向是竖屏方向。
手机相机的图像数据都是来自于摄像头硬件的图像传感器,这个传感器在被固定到手机上后有一个默认的取景方向,方向一般是和手机横屏方向一致,如下图:
和竖屏应用方向呈90度。
将图像传感器捕获的图像,显示在屏幕上的方向。在默认情况下,和图像传感器方向一致,在相机API中可以通过setDisplayOrientation(int degrees)设置预览方向(顺时针设置,不是逆时针)。默认情况下,这个值是0,在注释文档中:
/** * Set the clockwise rotation of preview display in degrees. This affects * the preview frames and the picture displayed after snapshot. This method * is useful for portrait mode applications. Note that preview display of * front-facing cameras is flipped horizontally before the rotation, that * is, the image is reflected along the central vertical axis of the camera * sensor. So the users can see themselves as looking into a mirror. * * <p>This does not affect the order of byte array passed in{@link * PreviewCallback#onPreviewFrame}, JPEG pictures, or recorded videos. This * method is not allowed to be called during preview. * * 设置预览显示的顺时针旋转角度,会影响预览帧和拍拍照后显示的图片,这个方法对竖屏模式的应用 * 很有用,前置摄像头进行角度旋转之前,图像会进行一个水平的镜像翻转,用户在看预览图像的时候* 就像镜子一样了,这个不影响PreviewCallback的回调,生成JPEG图片和录像文件的方向。 * */注意,对于手机来说:
- 横屏下:因为屏幕方向和相机预览方向一致,所以预览图像和看到的实物方向一致
- 竖屏下:屏幕方向和预览方向垂直,会造成旋转90度现象,无论怎么旋转手机,UI预览界面和实物始终是90度,为了得到一致的预览界面需要将相机预览方向旋转90度
(setDisplayOrientation(90)),这样预览界面和实物方向一致。
下面举个简单例子:
这里重点讲解一下竖屏下:
需要结合上下两张图来看:
- 当图像传感器获得图像后,就会知道这幅图像每个坐标的像素值,但是要显示到屏幕上就要根据屏幕自然方向的坐标来显示(竖屏下屏幕自然方向坐标系和后置相机图像传感器方向呈90度),所以图像会逆时针旋转旋转90度,显示到屏幕坐标系上。
- 那么收集的图像时逆时针旋转了90度,那么这时候需要顺时针旋转90度才能和收集的自然方向保持一致,也就是和实物图方向一样。
在Android中,对于前置摄像头,有以下规定:
- 在预览图像是真实物体的镜像
- 拍出的照片和真实场景一样
同理这里重点讲一下,前置竖屏:
在前置相机中,预览图像和相机收集图像是镜像关系,上面图中Android图标中前置收集图像和预览图像时相反的,前置相机图像传感器方向和前置相机预览图像方向是左右相反的,上图也有体现。
- 前置摄像头收集到图像后(没有经过镜像处理),但是要显示到屏幕上,就要按照屏幕自然方向的坐标系来进行显示,需要顺时针旋转270度(API没有提供逆时针90度的方法),才能和手机自然方向一致。
- 在预览的时候,做了镜像处理,所以只需要顺时针旋转90度,就能和自然方向一致,因为摄像图像没有做水平翻转,所以前置摄像头拍出来的图片,你会发现跟预览的时候是左右翻转的,自己可以根据需求做处理。 上面把角度知识梳理了,后面会通过代码一步一步验证,下面按照最开始的思维导图继续看
Camera内的方法:
facing代表相机方向,可取值有二:
- CAMREA_FACING_BACK,值为0,表示是后置摄像头
- CAMERA_FACING_FRONT,值为1,表示是前置摄像头
是否禁用快门声音
PreviewCallback是一个接口,可以给Camera设置Camrea.PreviewCallback,并且实现这个onPreviewFrame(byte[] data, Camera camera)这个方法,就可以去Camera预览图片时的数据,如果设置Camera.setPreviewCallback(callback),onPreviewFrame这个方法会被一直调用,可以在摄像头对焦成功后设置camera.setOneShotPreviewCallback(previewCallback),这样设置onPreviewFrame这个方法就会被调用异常,处理data数据,data是相机预览到的原始数据,可以保存下来当做一张照片。
AutoFocusCallback是一个接口,用于在相机自动对焦完成后时通知回调,第一个参数是相机是否自动对焦成功,第二个参数是相机对象。
作为静态内部类,用来描述通过相机人脸检测识别的人脸信息。
是Rect对象,表示检测到人脸的区域,这个Rect对象中的坐标并不是安卓屏幕中的坐标,需要进行转换才能使用。
人脸检测的置信度,范围是1到100。100是最高的信度
是一个Point对象,表示的是检测到左眼的位置坐标
是一个Point对象,表示的是检测到右眼的位置坐标
同时一个Point对象,表示的是检测到嘴的位置坐标 leftEye,rightEye,mouth有可能获得不到,并不是所有相机支持,不支持情况下,获取为空
代表拍照图片的大小。
拍照图片的宽
拍照图片的高
这是一个接口,当开始预览(人脸识别)的时候开始回调
通知监听器预览帧检测到的人脸,Face[]是一个数组,用来存放检测的人脸(存放多张人脸),第二个参数是识别人脸的相机。
在Camera作为内部类存在,是相机配置设置类,不同设备可能具有不同的照相机功能,如图片大小或者闪光模式。
设置预览相机图片的大小,width是图片的宽,height是图片的高
设置预览图片的格式,有以下格式:
- ImageFormat.NV16
- ImageFormat.NV21
- ImageFormat.YUY2
- ImageFormat.YV12
- ImgaeFormat.RGB_565
- ImageFormat.JPEG 如果不设置返回的数据,会默认返回NV21编码数据。
设置保存图片的大小,width图片的宽度,以像素为单位,height是图片的高度,以像素为单位。
设置保存图片的格式,取值和setPreviewFormat格式一样。
上面已经讲过,设置相机采集照片的角度,这个值是相机所采集的图片需要顺时针选择到自然方向的角度值,它必须是0,90,180或者270中的一个。
设置相机对焦模式,对焦模式有以下:
- AUTO
- INFINITY
- MACRO
- FIXED
- EDOF
- CONTINUOUS_VIDEO
设置缩放系数,也就是平常所说的变焦。
返回相机支持的预览图片大小,返回值是一个List<Size>数组,至少有一个元素。
返回获取相机支持的视频帧大小,可以通过MediaRecorder来使用。
返回相机支持的图片预览格式,所有相机都支持ImageFormat.NV21,返回是集合类型并且返回至少包含一个元素。
以集合的形式返回相机支持采集的图片大小,至少返回一个元素。
以集合的形式返回相机支持的图片(拍照后)格式,至少返回一个元素。
以集合的形式返回相机支持的对焦模式,至少返回一个元素。
返回相机所支持的最多人脸检测数,如果返回0,则说明制定类型的不支持人脸识别。如果手机摄像头支持最多的人脸检测个数是5个,当画面超出5个人脸数,还是检测到5个人脸数。
返回当前缩放值,这个值的范围在0到getMaxZoom()之间。
返回当前设备可用的摄像头个数。
返回指定id所表示的摄像头信息,如果getNumberOfCameras()返回N,那么有效的id值为0~(N-1),一般手机至少有前后两个摄像头。
使用传入的id所表示的摄像头来创建Camera对象,如果这个id所表示的摄像头被其他应用程序打开调用此方法会跑出异常,当使用完相机后,必须调用release()来释放资源,否则它会保持锁定状态,不可用其他应用程序。
根据所传入的SurfaceHolder对象来设置实时预览。
根据传入的PreviewCallback对象来监听相机预览数据的回调,PreviewCallback再上面已经讲过。
根据传入的Parameters对象来设置当前相机的参数信息。
根据传入的Parameters对象来返回当前相机的参数信息
开始预览,在屏幕上绘制预览帧,如果没有调用setPreviewDisplay(SurfaceHolder)或者setPreviewTexture(SurfaceTexture)直接调用这个方法是没有任何效果的,如果启动预览失败,则会引发RuntimeException。
停止预览,停止绘制预览帧到屏幕,如果停止失败,会引发RuntimeException。
开始人脸识别,这个要调用startPreview之后调用,也就是在预览之后才能进行人脸识别,如果不支持人脸识别,调用此方法会抛出IllegalArgumentException。
停止人脸识别。
给人脸检测设置监听,以便提供预览帧。
断开并且释放相机对象资源。
设置相机预览画面旋转的角度,在刚开始讲述orientation的时候讲述角度问题,查看源码时,有以下注释:
publicstaticvoidsetCameraDisplayOrientation(Activityactivity, intcameraId, android.hardware.Cameracamera){android.hardware.Camera.CameraInfoinfo = newandroid.hardware.Camera.CameraInfo(); android.hardware.Camera.getCameraInfo(cameraId, info); //获取window(Activity)旋转方向introtation = activity.getWindowManager().getDefaultDisplay().getRotation(); intdegrees = 0; switch (rotation){caseSurface.ROTATION_0: degrees = 0; break; caseSurface.ROTATION_90: degrees = 90; break; caseSurface.ROTATION_180: degrees = 180; break; caseSurface.ROTATION_270: degrees = 270; break} intresult; //计算图像所要旋转的角度if (info.facing == Camera.CameraInfo.CAMERA_FACING_FRONT){result = (info.orientation + degrees) % 360; result = (360 - result) % 360; // compensate the mirror } else{// back-facingresult = (info.orientation - degrees + 360) % 360} //调整图像旋转角度camera.setDisplayOrientation(result)}上面已经描述过在竖屏下,对于后置相机来讲:
只需要旋转后置相机的orientation也就是90度即可和屏幕方向保持一致;
对于前置相机预览方向,相机预览的图像是相机采集到的图像镜像,所以旋转orientation 270-180=90度才和屏幕方向一致。 CameraInfo是实例化的相机类,info.orientation是相机对于屏幕自然方向(左上角坐标系)的旋转角度数。 那下面跟着官方适配方法走:
int rotation = activity.getWindowManager().getDefaultDisplay().getRotation(); rotation是预览Window的旋转方向,对于手机而言,当在清单文件设置Activity的screenOrientation="portait"时,rotation=0,这时候没有旋转,当screenOrientation="landScape"时,rotation=1。
对于后置摄像头,手机竖屏显示时,预览图像旋转的角度:result=(90-0+360)%360=90;手机横屏显示时,预览图像旋转:result = (90-0+360)%360 = 0;
camera.setDisplayOrientation(int param)这个方法是图片输出后所旋转的角度数,旋转值可以是0,90,180,270。
注意:camera.setDisplayOrientation(int param)这个方法仅仅是修改相机的预览方向,不会影响到PreviewCallback回调、生成的JPEG图片和录像视频的方向,这些数据的方向会和图像Sensor方向一致。
需要申请拍照权限和外部存储权限:
<!--权限申请 相机--> <uses-permissionandroid:name="android.permission.CAMERA"/> <!--使用uses-feature指定需要相机资源--> <uses-featureandroid:name="android.hardware.Camera"/> <!--需要自动聚焦 --> <uses-featureandroid:name="android.hardware.camera.autofocus"/> <!--存储图片或者视频--> <uses-permissionandroid:name="android.permission.WRITE_EXTERNAL_STORAGE"/>在onCreate检查权限:
@OverrideprotectedvoidonCreate(BundlesavedInstanceState){super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); initBind(); initListener(); checkNeedPermissions()}/** * 检测需要申请的权限 * */privatevoidcheckNeedPermissions(){//6.0以上需要动态申请权限 动态权限校验 Android 6.0 的 oppo & vivo 手机时,始终返回 权限已被允许 但是当真正用到该权限时,却又弹出权限申请框。if (Build.VERSION.SDK_INT >= 23){if (ContextCompat.checkSelfPermission(this, Manifest.permission.CAMERA) != PackageManager.PERMISSION_GRANTED || ContextCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED){//多个权限一起申请ActivityCompat.requestPermissions(this, newString[]{Manifest.permission.CAMERA, Manifest.permission.WRITE_EXTERNAL_STORAGE }, 1)} else{//已经全部申请 初始化相机资源initCamera()} }else{//6.0以下不用动态申请initCamera()} }在onRequestPermissionsResult处理回调:
/** * 动态处理申请权限的结果 * 用户点击同意或者拒绝后触发 * * @param requestCode 请求码 * @param permissions 权限 * @param grantResults 结果码 */@OverridepublicvoidonRequestPermissionsResult(intrequestCode, String[] permissions, int[] grantResults){super.onRequestPermissionsResult(requestCode, permissions, grantResults); switch (requestCode){case1: //获取权限一一验证if (grantResults.length > 1){if (grantResults[0] == PackageManager.PERMISSION_GRANTED){if (grantResults[1] == PackageManager.PERMISSION_GRANTED){initCamera()} else{//拒绝就要强行跳转设置界面Permissions.showPermissionsSettingDialog(this, permissions[1])} } else{//拒绝就要强行跳转设置界面Permissions.showPermissionsSettingDialog(this, permissions[0])} } else{ToastUtil.showShortToast(this, "请重新尝试~")} break} }/** * 调用系统相机 * */privatevoidgoSystemCamera(){//在根目录创建jpg文件cameraSavePath = newFile(Environment.getExternalStorageDirectory().getPath() + "/" + System.currentTimeMillis() +".jpg"); //指定跳到系统拍照Intentintent = newIntent(MediaStore.ACTION_IMAGE_CAPTURE); //适配Android 7.0以上版本应用私有目录限制被访问if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.N){uri = FileProvider.getUriForFile(this, SystemUtil.getPackageName(getApplicationContext()) + ".fileprovider",cameraSavePath); intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)}else{//7.0以下uri = Uri.fromFile(cameraSavePath)} //指定ACTION为MediaStore.EXTRA_OUTPUTintent.putExtra(MediaStore.EXTRA_OUTPUT,uri); //请求码赋值为1startActivityForResult(intent,1)}在OnActivityResult(int requestCode,int resultCode,Intent data)方法做处理:
@OverrideprotectedvoidonActivityResult(intrequestCode,intresultCode,Intentdata){StringphotoPath; //处理拍照后返回的图片路径if(requestCode == 1 && resultCode == RESULT_OK){if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.N){photoPath = String.valueOf(cameraSavePath)}else{photoPath = uri.getEncodedPath()} Log.d("拍照返回图片的路径:",photoPath); Glide.with(this).load(photoPath).apply(RequestOptions.noTransformation() .override(iv_photo.getWidth(),iv_photo.getHeight()) .error(R.drawable.default_person_icon)) .into(iv_photo)}elseif(requestCode == 2 && resultCode == RESULT_OK){//处理调用相册返回的路径photoPath = PhotoAlbumUtil.getRealPathFromUri(this,data.getData()); Glide.with(this).load(photoPath).apply(RequestOptions.noTransformation() .override(iv_photo.getWidth(),iv_photo.getHeight()) .error(R.drawable.default_person_icon)) .into(iv_photo)} super.onActivityResult(requestCode, resultCode, data)}上面是调用系统相机拍照后的效果,另外照片存储到了外部存储的根目录位置:
下面按照以下步骤来实现自定义相机开发:
- 在布局xml文件中定义SurfaceView用于预览,通过SurfaceView.getHolder获取SurfaceHolder对象
- 给SurfaceHolder对象设置监听回调,实现三个方法surfaceCreated(SurfaceHolder holder)、surfaceChanged(SurfaceHolder holder, int format, int width, int height)、surfaceDestroyed(SurfaceHolder holder)
- 在surfaceCreated(SurfaceHolder holder)方法里通过传入的相机id来Camera.open(int cameraId)打开相机
- 给相机设置具体参数,如:预览格式,对焦模式
- 通过Camera.setPreviewDisplay(SurfaceHolder holder)设置实时预览
- 根据官方方法来设置正确的照片预览方向
- 调用Camera.startPreview()开始预览
- 同时可以调用Camera.startFaceDetection来人脸检测,并设置回调,重写onFaceDetection(Camera.Face[] faces, Camera camera)得到检测人脸数量
- 调用Camera.takePicture来进行拍照
- 处理保存的照片,旋转或者压缩
- 当相机不再调用时,释放相机资源
<?xml version="1.0" encoding="utf-8"?> <android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"xmlns:app="http://schemas.android.com/apk/res-auto"android:layout_width="match_parent"android:layout_height="match_parent"> <SurfaceViewandroid:id="@+id/sf_camera"android:layout_width="match_parent"android:layout_height="match_parent"/> <android.support.constraint.ConstraintLayout android:id="@+id/cl_bottom"android:layout_width="match_parent"android:layout_height="80dp"app:layout_constraintBottom_toBottomOf="parent" > <!-- 拍照后显示的图片--> <ImageViewandroid:id="@+id/iv_photo"android:layout_width="40dp"android:layout_height="40dp"android:layout_marginLeft="20dp"app:layout_constraintBottom_toBottomOf="parent"app:layout_constraintLeft_toLeftOf="parent"app:layout_constraintTop_toTopOf="parent" /> <!-- 拍照按钮--> <TextViewandroid:id="@+id/tv_takephoto"android:layout_width="wrap_content"android:layout_height="wrap_content"android:background="@drawable/icon_take_photo_selector"app:layout_constraintBottom_toBottomOf="parent"app:layout_constraintHorizontal_bias="0.5"app:layout_constraintLeft_toLeftOf="parent"app:layout_constraintRight_toRightOf="parent"app:layout_constraintTop_toTopOf="parent"/> </android.support.constraint.ConstraintLayout> </android.support.constraint.ConstraintLayout>布局文件主要有拍照预览控件SurfaceView、拍照后显示的图片Imageview、拍照按钮Textview组成。
新增相机业务逻辑类CameraPresenter,目的是将业务和界面显示分开,Activity负责UI的显示,业务逻辑在CameraPresenter,新增构造函数,构造函数有两个参数,分别是持有手机界面的Activity和SurfaceView对象,并根据传入的SurfaceView对象通过SurfaceView.getHolder方法获取SurfaceHolder对象:
publicCameraPresenter(AppCompatActivitymAppCompatActivity, SurfaceViewmSurfaceView){this.mAppCompatActivity = mAppCompatActivity; this.mSurfaceView = mSurfaceView; mSurfaceHolder = mSurfaceView.getHolder()}SurfaceHolder对象设置监听回调:
publicCameraPresenter(AppCompatActivitymAppCompatActivity, SurfaceViewmSurfaceView){this.mAppCompatActivity = mAppCompatActivity; this.mSurfaceView = mSurfaceView; mSurfaceHolder = mSurfaceView.getHolder(); init()} /** * 初始化增加回调 */privatevoidinit(){mSurfaceHolder.addCallback(newSurfaceHolder.Callback(){@OverridepublicvoidsurfaceCreated(SurfaceHolderholder){//surface创建时执行 } @OverridepublicvoidsurfaceChanged(SurfaceHolderholder, intformat, intwidth, intheight){//surface绘制时执行 } @OverridepublicvoidsurfaceDestroyed(SurfaceHolderholder){//surface销毁时执行 } })}在surfaceCreated(SurfaceHolder holder)方法里调用打开相机:
//摄像头Id 默认后置 0,前置的值是1privateintmCameraId = Camera.CameraInfo.CAMERA_FACING_BACK; @OverridepublicvoidsurfaceCreated(SurfaceHolderholder){//surface创建时执行if (mCamera == null){// mCameraId是后置还是前置 0是后置 1是前置openCamera(mCameraId)} } /** * 打开相机 并且判断是否支持该摄像头 * * @param FaceOrBack 前置还是后置 * @return */privatebooleanopenCamera(intFaceOrBack){//是否支持前后摄像头booleanisSupportCamera = isSupport(FaceOrBack); //如果支持if (isSupportCamera){try{mCamera = Camera.open(FaceOrBack)} catch (Exceptione){e.printStackTrace(); ToastUtil.showShortToast(mAppCompatActivity, "打开相机失败~"); returnfalse} } returnisSupportCamera}调用Camera.open(int cameraId)后返回具体的Camera对象后,还需要设置相机一些参数,如预览模式,对焦模式等:
/** * 打开相机 并且判断是否支持该摄像头 * * @param FaceOrBack 前置还是后置 * @return */privatebooleanopenCamera(intFaceOrBack){//是否支持前后摄像头booleanisSupportCamera = isSupport(FaceOrBack); //如果支持if (isSupportCamera){try{mCamera = Camera.open(FaceOrBack); initParameters(mCamera); //设置预览回调if (mCamera != null){mCamera.setPreviewCallback(this)} } catch (Exceptione){e.printStackTrace(); ToastUtil.showShortToast(mAppCompatActivity, "打开相机失败~"); returnfalse} } returnisSupportCamera} /** * 设置相机参数 * * @param camera */privatevoidinitParameters(Cameracamera){try{//获取Parameters对象mParameters = camera.getParameters(); //设置预览格式mParameters.setPreviewFormat(ImageFormat.NV21); //判断是否支持连续自动对焦图像if (isSupportFocus(Camera.Parameters.FOCUS_MODE_CONTINUOUS_PICTURE)){mParameters.setFocusMode(Camera.Parameters.FOCUS_MODE_CONTINUOUS_PICTURE); //判断是否支持单次自动对焦 } elseif (isSupportFocus(Camera.Parameters.FOCUS_MODE_AUTO)){//自动对焦(单次)mParameters.setFocusMode(Camera.Parameters.FOCUS_MODE_AUTO)} //给相机设置参数mCamera.setParameters(mParameters)} catch (Exceptione){e.printStackTrace(); ToastUtil.showShortToast(mAppCompatActivity, "初始化相机失败")}设置完相机参数之后,就可以需要相机调用Camera.setPreviewDisplay(SurfaceHolder holder)和Camera.startPreview()开启预览:
/** * 开始预览 */privatevoidstartPreview(){try{//根据所传入的SurfaceHolder对象来设置实时预览mCamera.setPreviewDisplay(mSurfaceHolder); mCamera.startPreview(); //这里同时开启人脸检测startFaceDetect()} catch (IOExceptione){e.printStackTrace()} } /** * 人脸检测 */privatevoidstartFaceDetect(){//开始人脸识别,这个要调用startPreview之后调用mCamera.startFaceDetection(); //添加回调mCamera.setFaceDetectionListener(newCamera.FaceDetectionListener(){@OverridepublicvoidonFaceDetection(Camera.Face[] faces, Cameracamera){mCameraCallBack.onFaceDetect(transForm(faces), camera); Log.d("sssd", "检测到" + faces.length + "人脸")} })}在surfaceCreated(SurfaceHolder holder)回调方法调用:
... @OverridepublicvoidsurfaceCreated(SurfaceHolderholder){//surface创建时执行if (mCamera == null){//mCameraId是后置还是前置 0是后置 1是前置openCamera(mCameraId)} //并设置预览startPreview()} ...当相机不再调用的时候,需要调用Camera.release()来释放相机资源
/** * 释放相机资源 */publicvoidreleaseCamera(){if (mCamera != null){//停止预览mCamera.stopPreview(); mCamera.setPreviewCallback(null); //释放相机资源mCamera.release(); mCamera = null} }在surfaceDestroyed(SurfaceHolder holder)调用:
/** * 初始化增加回调 */privatevoidinit(){mSurfaceHolder.addCallback(newSurfaceHolder.Callback(){@OverridepublicvoidsurfaceCreated(SurfaceHolderholder){//surface创建时执行 mCameraId是后置还是前置 0是后置 1是前置if (mCamera == null){openCamera(mCameraId)} //并设置预览startPreview()} @OverridepublicvoidsurfaceChanged(SurfaceHolderholder, intformat, intwidth, intheight){//surface绘制时执行 } @OverridepublicvoidsurfaceDestroyed(SurfaceHolderholder){//surface销毁时执行releaseCamera()} })} /** * 设置前置还是后置 * * @param mCameraId 前置还是后置 */publicvoidsetFrontOrBack(intmCameraId){this.mCameraId = mCameraId}在自定义相机的Activity界面进行调用:
@OverrideprotectedvoidonCreate(BundlesavedInstanceState){super.onCreate(savedInstanceState); setContentView(R.layout.activity_customcamera); //绑定ViewinitBind(); //添加点击,触摸事件等监听initListener(); //初始化CameraPresentermCameraPresenter = newCameraPresenter(this,sf_camera); //设置后置摄像头mCameraPresenter.setFrontOrBack(Camera.CameraInfo.CAMERA_FACING_BACK)}在onDestroy()方法调用releaseCamera():
/** * Activity 销毁回调方法 释放各种资源 */@OverrideprotectedvoidonDestroy(){super.onDestroy(); if(mCameraPresenter != null){mCameraPresenter.releaseCamera()} }现在先看看效果:
发现预览效果图逆时针旋转了90度,当你把手机横屏摆放也是,上面已经说过,因为屏幕自然方向和图像传感器方向不一致造成的,需要重新设置预览时的角度,采用官方的推荐方法:
/** * 保证预览方向正确 * * @param appCompatActivity Activity * @param cameraId 相机Id * @param camera 相机 */privatevoidsetCameraDisplayOrientation(AppCompatActivityappCompatActivity, intcameraId, Cameracamera){Camera.CameraInfoinfo = newCamera.CameraInfo(); Camera.getCameraInfo(cameraId, info); //rotation是预览Window的旋转方向,对于手机而言,当在清单文件设置Activity的screenOrientation="portait"时,//rotation=0,这时候没有旋转,当screenOrientation="landScape"时,rotation=1。introtation = appCompatActivity.getWindowManager().getDefaultDisplay() .getRotation(); intdegrees = 0; switch (rotation){caseSurface.ROTATION_0: degrees = 0; break; caseSurface.ROTATION_90: degrees = 90; break; caseSurface.ROTATION_180: degrees = 180; break; caseSurface.ROTATION_270: degrees = 270; break} intresult; //计算图像所要旋转的角度if (info.facing == Camera.CameraInfo.CAMERA_FACING_FRONT){result = (info.orientation + degrees) % 360; result = (360 - result) % 360; // compensate the mirror } else{// back-facingresult = (info.orientation - degrees + 360) % 360} orientation = result; //调整预览图像旋转角度camera.setDisplayOrientation(result)}并在startPreview()方法里调用:
/** * 开始预览 */privatevoidstartPreview(){try{//根据所传入的SurfaceHolder对象来设置实时预览mCamera.setPreviewDisplay(mSurfaceHolder); //调整预览角度setCameraDisplayOrientation(mAppCompatActivity,mCameraId,mCamera); mCamera.startPreview(); startFaceDetect()} catch (IOExceptione){e.printStackTrace()} }再次看下运行效果:
上面调整了预览角度的问题后,因为在市面上安卓机型五花八门,屏幕分辨率也很多,为了避免图像变形,需要调整预览图像和保存的图像尺寸:
//获取屏幕宽和高privateintscreenWidth, screenHeight; publicCameraPresenter(AppCompatActivitymAppCompatActivity, SurfaceViewmSurfaceView){this.mAppCompatActivity = mAppCompatActivity; this.mSurfaceView = mSurfaceView; mSurfaceHolder = mSurfaceView.getHolder(); DisplayMetricsdm = newDisplayMetrics(); mAppCompatActivity.getWindowManager().getDefaultDisplay().getMetrics(dm); //获取宽高像素screenWidth = dm.widthPixels; screenHeight = dm.heightPixels; Log.d("sssd-手机宽高尺寸:",screenWidth +"*"+screenHeight); init()} /** * * 设置保存图片的尺寸 */privatevoidsetPictureSize(){List<Camera.Size> localSizes = mParameters.getSupportedPictureSizes(); Camera.SizebiggestSize = null; Camera.SizefitSize = null;// 优先选预览界面的尺寸Camera.SizepreviewSize = mParameters.getPreviewSize();//获取预览界面尺寸floatpreviewSizeScale = 0; if (previewSize != null){previewSizeScale = previewSize.width / (float) previewSize.height} if (localSizes != null){intcameraSizeLength = localSizes.size(); for (intn = 0; n < cameraSizeLength; n++){Camera.Sizesize = localSizes.get(n); if (biggestSize == null){biggestSize = size} elseif (size.width >= biggestSize.width && size.height >= biggestSize.height){biggestSize = size} // 选出与预览界面等比的最高分辨率if (previewSizeScale > 0 && size.width >= previewSize.width && size.height >= previewSize.height){floatsizeScale = size.width / (float) size.height; if (sizeScale == previewSizeScale){if (fitSize == null){fitSize = size} elseif (size.width >= fitSize.width && size.height >= fitSize.height){fitSize = size} } } } // 如果没有选出fitSize, 那么最大的Size就是FitSizeif (fitSize == null){fitSize = biggestSize} mParameters.setPictureSize(fitSize.width, fitSize.height)} } /** * 设置预览界面尺寸 */privatevoidsetPreviewSize(){//获取系统支持预览大小List<Camera.Size> localSizes = mParameters.getSupportedPreviewSizes(); Camera.SizebiggestSize = null;//最大分辨率Camera.SizefitSize = null;// 优先选屏幕分辨率Camera.SizetargetSize = null;// 没有屏幕分辨率就取跟屏幕分辨率相近(大)的sizeCamera.SizetargetSiz2 = null;// 没有屏幕分辨率就取跟屏幕分辨率相近(小)的sizeif (localSizes != null){intcameraSizeLength = localSizes.size(); for (intn = 0; n < cameraSizeLength; n++){Camera.Sizesize = localSizes.get(n); Log.d("sssd-系统支持的尺寸:",size.width + "*" +size.height); if (biggestSize == null || (size.width >= biggestSize.width && size.height >= biggestSize.height)){biggestSize = size} //如果支持的比例都等于所获取到的宽高if (size.width == screenHeight && size.height == screenWidth){fitSize = size; //如果任一宽或者高等于所支持的尺寸 } elseif (size.width == screenHeight || size.height == screenWidth){if (targetSize == null){targetSize = size; //如果上面条件都不成立 如果任一宽高小于所支持的尺寸 } elseif (size.width < screenHeight || size.height < screenWidth){targetSiz2 = size} } } if (fitSize == null){fitSize = targetSize} if (fitSize == null){fitSize = targetSiz2} if (fitSize == null){fitSize = biggestSize} Log.d("sssd-最佳预览尺寸:",fitSize.width + "*" + fitSize.height); mParameters.setPreviewSize(fitSize.width, fitSize.height)} }这里额外要注意:对于相机来说,都是width是长边,也就是width > height,在上面setPreviewSize()方法里,获取所支持的size.width要和screenHeight比较,size.height要和screenWidth,最后在设置相机里调用即可:
/** * 设置相机参数 * * @param camera */privatevoidinitParameters(Cameracamera){try{//获取Parameters对象mParameters = camera.getParameters(); //设置预览格式mParameters.setPreviewFormat(ImageFormat.NV21); setPreviewSize(); setPictureSize(); //.....mCamera.setParameters(mParameters)} catch (Exceptione){e.printStackTrace(); ToastUtil.showShortToast(mAppCompatActivity, "初始化相机失败")} }下面看看在vivo x9所支持的尺寸:
下面进行拍照处理,拍照保存图片有两种方式:
- 直接调用
Camera.takePicture(ShutterCallback shutter,PictureCallback raw,PictureCallback jpeg)
/** * Equivalent to <pre>takePicture(Shutter, raw, null, jpeg)</pre>. * * @see #takePicture(ShutterCallback, PictureCallback, PictureCallback, PictureCallback) */publicfinalvoidtakePicture(ShutterCallbackshutter, PictureCallbackraw, PictureCallbackjpeg){takePicture(shutter, raw, null, jpeg)} /** * @param shutter the callback for image capture moment, or null * @param raw the callback for raw (uncompressed) image data, or null * @param postview callback with postview image data, may be null * @param jpeg the callback for JPEG image data, or null * @throws RuntimeException if starting picture capture fails; usually this * would be because of a hardware or other low-level error, or because * release() has been called on this Camera instance. */publicfinalvoidtakePicture(ShutterCallbackshutter, PictureCallbackraw, PictureCallbackpostview, PictureCallbackjpeg){... }三个参数的takePicture实际调用四个参数的takePicture,只是带有postview图像数据的回调,设置为空了。
- 在相机预览的回调中直接保存:
mCamera.setPreviewCallback(newCamera.PreviewCallback(){@OverridepublicvoidonPreviewFrame(byte[] data, Cameracamera){} });在onPreviewFrame以字节数组形式返回具体照片数据,这个方法会不停的回调,这里不演示这个方法,保存图片的方法和第一个方法是一样的。 首先先自定义回调:
//自定义回调privateCameraCallBackmCameraCallBack; publicinterfaceCameraCallBack{//预览帧回调voidonPreviewFrame(byte[] data, Cameracamera); //拍照回调voidonTakePicture(byte[] data, CameraCamera); //人脸检测回调voidonFaceDetect(ArrayList<RectF> rectFArrayList, Cameracamera); //拍照路径返回voidgetPhotoFile(StringimagePath)}调用Camera.takePicture方法:
/** * 拍照 */publicvoidtakePicture(){if (mCamera != null){//拍照回调 点击拍照时回调 写一个空实现mCamera.takePicture(newCamera.ShutterCallback(){@OverridepublicvoidonShutter(){} }, newCamera.PictureCallback(){//回调没压缩的原始数据@OverridepublicvoidonPictureTaken(byte[] data, Cameracamera){} }, newCamera.PictureCallback(){//回调图片数据 点击拍照后相机返回的照片byte数组,照片数据@OverridepublicvoidonPictureTaken(byte[] data, Cameracamera){//拍照后记得调用预览方法,不然会停在拍照图像的界面mCamera.startPreview(); //回调mCameraCallBack.onTakePicture(data, camera); //保存图片getPhotoPath(data)} })} }保存图片目录先放在app内:
publicclassConfiguration{//这是app内部存储 格式如下 /data/data/包名/xxx/publicstaticStringinsidePath = "/data/data/com.knight.cameraone/pic/"; //外部路径publicstaticStringOUTPATH = Environment.getExternalStorageDirectory() + "/拍照-相册/"}创建目录具体方法:
/** * 创建拍照照片文件夹 */privatevoidsetUpFile(){photosFile = newFile(Configuration.insidePath); if (!photosFile.exists() || !photosFile.isDirectory()){booleanisSuccess = false; try{isSuccess = photosFile.mkdirs()} catch (Exceptione){ToastUtil.showShortToast(mAppCompatActivity, "创建存放目录失败,请检查磁盘空间~"); mAppCompatActivity.finish()} finally{if (!isSuccess){ToastUtil.showShortToast(mAppCompatActivity, "创建存放目录失败,请检查磁盘空间~"); mAppCompatActivity.finish()} } } }在初始化相机时先调用创建文件:
publicCameraPresenter(AppCompatActivitymAppCompatActivity, SurfaceViewmSurfaceView){//...screenWidth = dm.widthPixels; screenHeight = dm.heightPixels; Log.d("sssd-手机宽高尺寸:",screenWidth +"*"+screenHeight); //创建文件夹目录setUpFile(); init()}拍照后保存图片这种输出耗时操作应该用线程来处理,新建线程池类:
publicclassThreadPoolUtil{privatestaticExecutorServicethreadPool = Executors.newCachedThreadPool(); /** * 在线程池执行一个任务 * @param runnable 任务 */publicstaticvoidexecute(Runnablerunnable){threadPool.execute(runnable)} }getPhotoPath(byte[] data)方法:
/** * @return 返回路径 */privatevoidgetPhotoPath(finalbyte[] data){ThreadPoolUtil.execute(newRunnable(){@Overridepublicvoidrun(){longtimeMillis = System.currentTimeMillis(); Stringtime = SystemUtil.formatTime(timeMillis); //拍照数量+1photoNum++; //图片名字Stringname = SystemUtil.formatTime(timeMillis, SystemUtil.formatTime(photoNum) + ".jpg"); //创建具体文件Filefile = newFile(photosFile, name); if (!file.exists()){try{file.createNewFile()} catch (Exceptione){e.printStackTrace(); return} } try{FileOutputStreamfos = newFileOutputStream(file); try{//将数据写入文件fos.write(data)} catch (IOExceptione){e.printStackTrace()} finally{try{fos.close()} catch (IOExceptione){e.printStackTrace()} } //将图片保存到手机相册中SystemUtil.saveAlbum(Configuration.insidePath + file.getName(), file.getName(), mAppCompatActivity); //将图片复制到外部SystemUtil.coptPicture(Configuration.insidePath + file.getName(),Configuration.OUTPATH,file.getName()); //发消息给主线程Messagemessage = newMessage(); message.what = 1; //文件路径message.obj = Configuration.insidePath + file.getName(); mHandler.sendMessage(message)} catch (FileNotFoundExceptione){e.printStackTrace()} } })}上面代码先把照片存到app包内,再将照片复制到app包外,当图片保存处理完后,回调主线程进行显示图片:
@SuppressLint("HandlerLeak") HandlermHandler = newHandler(){@SuppressLint("NewApi") @OverridepublicvoidhandleMessage(Messagemsg){switch (msg.what){case1: mCameraCallBack.getPhotoFile(msg.obj.toString()); break; default: break} } };在Activity中设置回调:
//添加监听mCameraPresenter.setCameraCallBack(this);拍照后保存图片后显示在界面上,Activity实现照片显示:
/** * 返回拍照后的照片 * @param imagePath */@OverridepublicvoidgetPhotoFile(StringimagePath){//设置头像Glide.with(this).load(imagePath) .apply(RequestOptions.bitmapTransform(newCircleCrop()) .override(iv_photo.getWidth(), iv_photo.getHeight()) .error(R.drawable.default_person_icon)) .into(iv_photo)}布局文件增加ImageView来显示拍照存储后的图片:
<android.support.constraint.ConstraintLayout android:id="@+id/cl_bottom"android:layout_width="match_parent"android:layout_height="80dp"app:layout_constraintBottom_toBottomOf="parent" > <!-- 拍照后显示的图片--> <ImageViewandroid:id="@+id/iv_photo"android:layout_width="40dp"android:layout_height="40dp"android:layout_marginLeft="20dp"app:layout_constraintBottom_toBottomOf="parent"app:layout_constraintLeft_toLeftOf="parent"app:layout_constraintTop_toTopOf="parent" /> <!-- 拍照按钮--> <TextViewandroid:id="@+id/tv_takephoto"android:layout_width="wrap_content"android:layout_height="wrap_content"android:background="@drawable/icon_take_photo_selector"app:layout_constraintBottom_toBottomOf="parent"app:layout_constraintHorizontal_bias="0.5"app:layout_constraintLeft_toLeftOf="parent"app:layout_constraintRight_toRightOf="parent"app:layout_constraintTop_toTopOf="parent"/> </android.support.constraint.ConstraintLayout>效果如下:
看看拍照后存储的照片:
发现拍照后存储的照片经过逆时针90度旋转,需要将顺时针90度,原因在上面分析orientation的时候讲述过,虽然调整来预览图像角度,但是并不能调整图片传感器的图片方向,所以只能保存图片后再将图片旋转:
/** * 旋转图片 * @param cameraId 前置还是后置 * @param orientation 拍照时传感器方向 * @param path 图片路径 */privatevoidrotateImageView(intcameraId,intorientation,Stringpath){Bitmapbitmap = BitmapFactory.decodeFile(path); Matrixmatrix = newMatrix(); //0是后置if(cameraId == 0){if(orientation == 90){matrix.postRotate(90)} } //1是前置if(cameraId == 1){//顺时针旋转270度 matrix.postRotate(270)} // 创建新的图片BitmapresizedBitmap = Bitmap.createBitmap(bitmap, 0, 0, bitmap.getWidth(), bitmap.getHeight(), matrix, true); Filefile = newFile(path); //重新写入文件try{// 写入文件FileOutputStreamfos; fos = newFileOutputStream(file); //默认jpgresizedBitmap.compress(Bitmap.CompressFormat.JPEG, 100, fos); fos.flush(); fos.close(); resizedBitmap.recycle()}catch (Exceptione){e.printStackTrace(); return} }在保存图像后调用:
/** * * 返回图片路径 * @param data */privatevoidgetPhotoPath(finalbyte[] data){... //将图片旋转rotateImageView(mCameraId,orientation,Configuration.insidePath + file.getName()); //将图片保存到手机相册SystemUtil.saveAlbum(Configuration.insidePath + file.getName(), file.getName(), mAppCompatActivity); ... }在布局文件添加TextView作为前后摄像头转换:
<SurfaceViewandroid:id="@+id/sf_camera"android:layout_width="match_parent"android:layout_height="match_parent"/> <TextViewandroid:id="@+id/tv_change_camera"android:layout_width="40dp"android:layout_height="40dp"android:layout_marginRight="15dp"android:layout_marginTop="15dp"android:background="@drawable/icon_change_camera_default"app:layout_constraintRight_toRightOf="parent"app:layout_constraintTop_toTopOf="parent" />在CameraPersenter中,添加改变摄像头方法:
/** * 前后摄像切换 */publicvoidswitchCamera(){//先释放资源releaseCamera(); //在Android P之前 Android设备仍然最多只有前后两个摄像头,在Android p后支持多个摄像头 用户想打开哪个就打开哪个mCameraId = (mCameraId + 1) % Camera.getNumberOfCameras(); //打开摄像头openCamera(mCameraId); //切换摄像头之后开启预览startPreview()}具体调用:
caseR.id.tv_change_camera: mCameraPresenter.switchCamera(); break;效果如下图:
在看看拍照后存储的照片:
这里可以发现,在预览的时候只是顺时针调用setDisplayOrientation()设置预览方向,并没有做镜面翻转,为什么切换前置时,预览效果跟实物一样呢,原来是在调用setDisplayOrientation()做了水平镜面的翻转,但是拍照后保存下来的照片是没有水平翻转的,所以同时要对拍照后的照片做水平方向镜面翻转,那就在旋转图片里的方法加上翻转处理:
/** * 旋转图片 * @param cameraId 前置还是后置 * @param orientation 拍照时传感器方向 * @param path 图片路径 */privatevoidrotateImageView(intcameraId,intorientation,Stringpath){Bitmapbitmap = BitmapFactory.decodeFile(path); Matrixmatrix = newMatrix(); // 创建新的图片BitmapresizedBitmap; //0是后置if(cameraId == 0){if(orientation == 90){matrix.postRotate(90)} } //1是前置if(cameraId == 1){matrix.postRotate(270)} // 创建新的图片resizedBitmap = Bitmap.createBitmap(bitmap, 0, 0, bitmap.getWidth(), bitmap.getHeight(), matrix, true); //新增 如果是前置 需要镜面翻转处理if(cameraId == 1){Matrixmatrix1 = newMatrix(); matrix1.postScale(-1f,1f); resizedBitmap = Bitmap.createBitmap(resizedBitmap, 0, 0, resizedBitmap.getWidth(), resizedBitmap.getHeight(), matrix1, true)} Filefile = newFile(path); //重新写入文件try{// 写入文件FileOutputStreamfos; fos = newFileOutputStream(file); //默认jpgresizedBitmap.compress(Bitmap.CompressFormat.JPEG, 100, fos); fos.flush(); fos.close(); resizedBitmap.recycle()}catch (Exceptione){e.printStackTrace(); return} }这样就能保证预览和拍摄后保存的照片和实物一样了。
拍照必不可少的一个功能:改变焦距。在Camera中的内部类Camera.Parameters有Parameters.setZoom(int value)来调整预览图像缩放系数,因为在布局SurfaceView是全屏的,在OnTouch方法做处理,并点击屏幕进行自动变焦处理:
//默认状态privatestaticfinalintMODE_INIT = 0; //两个触摸点触摸屏幕状态privatestaticfinalintMODE_ZOOM = 1; //标识模式privateintmode = MODE_INIT; ... /** * * 触摸回调 * @param v 添加Touch事件具体的view * @param event 具体事件 * @return */@OverridepublicbooleanonTouch(Viewv, MotionEventevent){//无论多少跟手指加进来,都是MotionEvent.ACTION_DWON MotionEvent.ACTION_POINTER_DOWN//MotionEvent.ACTION_MOVE:switch (event.getAction() & MotionEvent.ACTION_MASK){//手指按下屏幕caseMotionEvent.ACTION_DOWN: mode = MODE_INIT; break; //当屏幕上已经有触摸点按下的状态的时候,再有新的触摸点被按下时会触发caseMotionEvent.ACTION_POINTER_DOWN: mode = MODE_ZOOM; //计算两个手指的距离 两点的距离startDis = SystemUtil.twoPointDistance(event); break; //移动的时候回调caseMotionEvent.ACTION_MOVE: isMove = true; //这里主要判断有两个触摸点的时候才触发if(mode == MODE_ZOOM){//只有两个点同时触屏才执行if(event.getPointerCount() < 2){returntrue} //获取结束的距离floatendDis = SystemUtil.twoPointDistance(event); //每变化10f zoom变1intscale = (int) ((endDis - startDis) / 10f); if(scale >= 1 || scale <= -1){intzoom = mCameraPresenter.getZoom() + scale; //判断zoom是否超出变焦距离if(zoom > mCameraPresenter.getMaxZoom()){zoom = mCameraPresenter.getMaxZoom()} //如果系数小于0if(zoom < 0 ){zoom = 0} //设置焦距mCameraPresenter.setZoom(zoom); //将最后一次的距离设为当前距离startDis = endDis} } break; caseMotionEvent.ACTION_UP: //判断是否点击屏幕 如果是自动聚焦if(isMove == false){//自动聚焦mCameraPresenter.autoFoucus()} isMove = false; break} returntrue}在CameraPresenter内调用:
/** * 变焦 * @param zoom 缩放系数 */publicvoidsetZoom(intzoom){if(mCamera == null){return} //获取Paramters对象Camera.Parametersparameters; parameters = mCamera.getParameters(); //如果不支持变焦if(!parameters.isZoomSupported()){return} //parameters.setZoom(zoom); //Camera对象重新设置Paramters对象参数mCamera.setParameters(parameters); mZoom = zoom} /** * 自动变焦 */publicvoidautoFoucus(){if(mCamera == null){mCamera.autoFocus(newCamera.AutoFocusCallback(){@OverridepublicvoidonAutoFocus(booleansuccess, Cameracamera){} })} }最终效果如下图:
通过Parameters.setFlashMode(String value)来控制闪光灯,参数类型有以下:
- FLASH_MODE_OFF 关闭闪光灯
- FLASH_MODE_AUTO 在预览,自动对焦和快照过程中需要时,闪光灯会自动开启。
- FLASH_MODE_ON 无论如何均使用闪光灯
- FLASH_MODE_RED_EYE 仿红眼模式,降低红眼模式
- FLASH_MODE_TORCH 系统会判断需要补光而自动决定是否开启闪光灯,手电筒模式,自动对焦
在平时中,用FLASH_MODE_OFF和FLASH_MODE_TORCH就行
/** * * 闪光灯 * @param turnSwitch true 为开启 false 为关闭 */publicvoidturnLight(booleanturnSwitch){if(mCamera == null){return} Camera.Parametersparameters = mCamera.getParameters(); if(parameters == null){return} parameters.setFlashMode(turnSwitch ? Camera.Parameters.FLASH_MODE_TORCH : Camera.Parameters.FLASH_MODE_OFF); mCamera.setParameters(parameters)}具体调用:
@OverridepublicvoidonClick(Viewv){switch (v.getId()){//拍照caseR.id.iv_photo: cy_photo.setVisibility(cy_photo.getVisibility() == View.VISIBLE ? View.GONE : View.VISIBLE); break; //改变摄像头 caseR.id.tv_change_camera: mCameraPresenter.switchCamera(); break; //关闭还是开启闪光灯 caseR.id.tv_flash: mCameraPresenter.turnLight(isTurn); tv_flash.setBackgroundResource(isTurn ? R.drawable.icon_turnon : R.drawable.icon_turnoff); isTurn = !isTurn; default: break} }实际效果:
到这里可以发现,相比于调用系统拍照的清晰度,自定义拍照就逊色一筹,感觉上面有一层蒙版罩着。调用系统拍照可以发现,屏幕亮度故意调亮,那么是不是把自定义拍照的界面亮度调大,效果清晰度会不会好一些呢,下面试试,在CustomCameraActivity加入:
/** * * 加入调整亮度 */privatevoidgetScreenBrightness(){WindowManager.LayoutParamslp = getWindow().getAttributes(); //screenBrightness的值是0.0-1.0 从0到1.0 亮度逐渐增大 如果是-1,那就是跟随系统亮度 这里调成 0.78左右lp.screenBrightness = Float.valueOf(200) * (1f / 255f); getWindow().setAttributes(lp)}在onCreate调用即可,最后效果如下:
下面简单实现录制视频的功能,利用MediaRecorder来实现直接录制视频,这里要注意:MediaRecorder是不能对每一帧数据做处理的,录制视频需要用到以下工具:
- MediaRecorder:视频编码的封装
- camera:视频画面原属数据采集
- SurfaceView:提供预览画面
MediaRecorder是Android中面向应用层的封装,用于提供音视频编码的封装操作的工具,下面直接上官方图:
下面简单介绍这几个生命周期的状态意思:
Initial:在MediaRecorder对象被创建时或者调用reset()方法后,会处于该状态。Initialized:当调用setAudioSource()或者setVideoSource()后就会处于该状态,这两个方法主要用于设置音视频的播放源配置,在该状态下可以调用reset()回到Initial状态。DataSourceConfigured:当调用setOutputFormat方法后,就会处于该状态,这个方法用来设置文件格式,如设置为mp4或者mp3,在这个状态同时可以设置音视频的封装格式,采样率,视频码率,帧率等,可以通过调用reset()回到Initial状态。Prepared:当调用上面几个方法后,就可以调用prepare()进入这个状态,只有处于这个状态才能调用start()方法。Recording:通过调用start()来进入该状态,处于这个状态就是真正录制音视频,通过调用reset()或者stop()来回到Initial状态。error:当录制过程中发生错误,就会进入该状态,调用reset()回到Initial状态。release:释放系统资源,只有在Initial状态才能调用release()回到该状态。
注意:要添加录音权限,这里不在讲述。
/** * 获取输出视频的width和height * */publicvoidgetVideoSize(){intbiggest_width=0 ,biggest_height=0;//最大分辨率intfitSize_width=0,fitSize_height=0; intfitSize_widthBig=0,fitSize_heightBig=0; Camera.Parametersparameters = mCamera.getParameters(); //得到系统支持视频格式List<Camera.Size> videoSize = parameters.getSupportedVideoSizes(); for(inti = 0;i < videoSize.size();i++){intw = videoSize.get(i).width; inth = videoSize.get(i).height; if ((biggest_width == 0 && biggest_height == 0)|| (w >= biggest_height && h >= biggest_width)){biggest_width = w; biggest_height = h} if(w == screenHeight && h == screenWidth){width = w; height = h}elseif(w == screenHeight || h == screenWidth){if(width == 0 || height == 0){fitSize_width = w; fitSize_height = h}elseif(w < screenHeight || h < screenWidth){fitSize_widthBig = w; fitSize_heightBig = h} } } if(width == 0 && height == 0){width = fitSize_width; height = fitSize_height} if(width == 0 && height == 0){width = fitSize_widthBig; height = fitSize_heightBig} if(width == 0 && height == 0){width = biggest_width; height = biggest_height} }在初始化相机方法调用,并且创建MediaRecorder对象:
@OverridepublicvoidsurfaceCreated(SurfaceHolderholder){//surface创建时执行if (mCamera == null){openCamera(mCameraId)} //并设置预览startPreview(); //新增获取系统支持视频getVideoSize(); mediaRecorder = newMediaRecorder()}//解锁Camera硬件mCamera.unlock(); mediaRecorder.setCamera(mCamera); //音频源 麦克风mediaRecorder.setAudioSource(MediaRecorder.AudioSource.CAMCORDER); //视频源 cameramediaRecorder.setVideoSource(MediaRecorder.VideoSource.CAMERA); //输出格式mediaRecorder.setOutputFormat(MediaRecorder.OutputFormat.MPEG_4); //音频编码mediaRecorder.setAudioEncoder(MediaRecorder.VideoEncoder.DEFAULT); //视频编码mediaRecorder.setVideoEncoder(MediaRecorder.VideoEncoder.H264); //设置帧频率mediaRecorder.setVideoEncodingBitRate(1 * 1024 * 1024 * 100); Log.d("sssd视频宽高:","宽"+width+"高"+height+""); mediaRecorder.setVideoSize(width,height); //每秒的帧数mediaRecorder.setVideoFrameRate(24);如果不设置调整保存视频的角度,用后置录制视频会逆时针翻转90度,所以需要设置输出顺时针旋转90度:
//调整视频旋转角度 如果不设置 后置和前置都会被旋转播放if(mCameraId == Camera.CameraInfo.CAMERA_FACING_FRONT){if(orientation == 270 || orientation == 90 || orientation == 180){mediaRecorder.setOrientationHint(180)}else{mediaRecorder.setOrientationHint(0)} }else{if(orientation == 90){mediaRecorder.setOrientationHint(90)} }整个录制方法如下:
/** * * 录制方法 */publicvoidstartRecord(Stringpath,Stringname){//解锁Camera硬件mCamera.unlock(); mediaRecorder.setCamera(mCamera); //音频源 麦克风mediaRecorder.setAudioSource(MediaRecorder.AudioSource.CAMCORDER); //视频源 cameramediaRecorder.setVideoSource(MediaRecorder.VideoSource.CAMERA); //输出格式mediaRecorder.setOutputFormat(MediaRecorder.OutputFormat.MPEG_4); //音频编码mediaRecorder.setAudioEncoder(MediaRecorder.VideoEncoder.DEFAULT); //视频编码mediaRecorder.setVideoEncoder(MediaRecorder.VideoEncoder.H264); //设置帧频率mediaRecorder.setVideoEncodingBitRate(1 * 1024 * 1024 * 100); Log.d("sssd视频宽高:","宽"+width+"高"+height+""); mediaRecorder.setVideoSize(width,height); //每秒的帧数mediaRecorder.setVideoFrameRate(24); //调整视频旋转角度 如果不设置 后置和前置都会被旋转播放if(mCameraId == Camera.CameraInfo.CAMERA_FACING_FRONT){if(orientation == 270 || orientation == 90 || orientation == 180){mediaRecorder.setOrientationHint(180)}else{mediaRecorder.setOrientationHint(0)} }else{if(orientation == 90){mediaRecorder.setOrientationHint(90)} } Filefile = newFile(path); if(!file.exists()){file.mkdirs()} //设置输出文件名字mediaRecorder.setOutputFile(path + File.separator + name + "mp4"); Filefile1 = newFile(path + File.separator + name + "mp4"); if(file1.exists()){file1.delete()} //设置预览mediaRecorder.setPreviewDisplay(mSurfaceView.getHolder().getSurface()); try{//准备录制mediaRecorder.prepare(); //开始录制mediaRecorder.start()} catch (IOExceptione){e.printStackTrace()} }当停止录制后需要把MediaRecorder释放,并且重新调用预览方法:
/** * * 停止录制 */publicvoidstopRecord(){if(mediaRecorder != null){mediaRecorder.release(); mediaRecorder = null} if(mCamera != null){mCamera.release()} openCamera(mCameraId); //并设置预览startPreview()}mCameraPresenter.startRecord(Configuration.OUTPATH,"video");当录制完需要播放,用新的界面来,用SurfaceView+MediaPlayer来实现:
publicclassPlayAudioActivityextendsAppCompatActivityimplementsMediaPlayer.OnCompletionListener,MediaPlayer.OnPreparedListener{privateSurfaceViewsf_play; privateMediaPlayerplayer; @OverrideprotectedvoidonCreate(BundlesavedInstanceState){super.onCreate(savedInstanceState); setContentView(R.layout.activity_playaudio); sf_play = findViewById(R.id.sf_play); //下面开始实例化MediaPlayer对象player = newMediaPlayer(); player.setOnCompletionListener(this); player.setOnPreparedListener(this); //设置数据数据源,也就播放文件地址,可以是网络地址StringdataPath = Configuration.OUTPATH + "/videomp4"; try{player.setDataSource(dataPath)} catch (Exceptione){e.printStackTrace()} sf_play.getHolder().addCallback(newSurfaceHolder.Callback(){@OverridepublicvoidsurfaceCreated(SurfaceHolderholder){//将播放器和SurfaceView关联起来player.setDisplay(holder); //异步缓冲当前视频文件,也有一个同步接口player.prepareAsync()} @OverridepublicvoidsurfaceChanged(SurfaceHolderholder, intformat, intwidth, intheight){} @OverridepublicvoidsurfaceDestroyed(SurfaceHolderholder){} })} /** * * 设置循环播放 * @param mp */@OverridepublicvoidonCompletion(MediaPlayermp){player.start(); player.setLooping(true)} /** * 这边播放 * @param mp */@OverridepublicvoidonPrepared(MediaPlayermp){player.start()} /** * 释放资源 * */@OverrideprotectedvoidonDestroy(){super.onDestroy(); if(player != null){player.reset(); player.release(); player = null} } }实际效果:
视频存放路径信息:
下面实现人脸检测,注意是人脸检测不是人脸识别,步骤如下:
- 在相机预览后,调用startFaceDetection方法开启人脸检测
- 调用setFaceDetectionListener(FaceDetectionListener listener)设置人脸检测回调
- 自定义View,用来绘制人脸大致区域
- 在人脸回调中,所获取的人脸信息传递给自定义View,自定义View根据人脸信息绘制大致区域
在相机调用开启预览后才能调用:
/** * 开始预览 */privatevoidstartPreview(){try{//根据所传入的SurfaceHolder对象来设置实时预览mCamera.setPreviewDisplay(mSurfaceHolder); //调整预览角度setCameraDisplayOrientation(mAppCompatActivity,mCameraId,mCamera); mCamera.startPreview(); //开启人脸检测startFaceDetect()} catch (IOExceptione){e.printStackTrace()} }/** * 人脸检测 */privatevoidstartFaceDetect(){//开始人脸检测,这个要调用startPreview之后调用mCamera.startFaceDetection(); //添加回调mCamera.setFaceDetectionListener(newCamera.FaceDetectionListener(){@OverridepublicvoidonFaceDetection(Camera.Face[] faces, Cameracamera){// mCameraCallBack.onFaceDetect(transForm(faces), camera);mFaceView.setFace(transForm(faces)); Log.d("sssd", "检测到" + faces.length + "人脸"); for(inti = 0;i < faces.length;i++){Log.d("第"+(i+1)+"张人脸","分数"+faces[i].score+"左眼"+faces[i].leftEye+"右眼"+faces[i].rightEye+"嘴巴"+faces[i].mouth)} } })}在Face源码中,可以看到这么一段描述:
Bounds of the face. (-1000, -1000) represents the top-left of the camera field of view, and (1000, 1000) represents the bottom-right of the field of view. For example, suppose the size of the viewfinder UI is 800x480. The rect passed from the driver is (-1000, -1000, 0, 0). The corresponding viewfinder rect should be (0, 0, 400, 240). It is guaranteed left < right and top < bottom. The coordinates can be smaller than -1000 or bigger than 1000. But at least one vertex will be within (-1000, -1000) and (1000, 1000). <p>The direction is relative to the sensor orientation, that is, what the sensor sees. The direction is not affected by the rotation or mirroring of{@link #setDisplayOrientation(int)}. The face bounding rectangle does not provide any information about face orientation.</p> <p>Here is the matrix to convert driver coordinates to View coordinates in pixels.</p> <pre> Matrix matrix = new Matrix(); CameraInfo info = CameraHolder.instance().getCameraInfo()[cameraId]; // Need mirror for front camera. boolean mirror = (info.facing == CameraInfo.CAMERA_FACING_FRONT); matrix.setScale(mirror ? -1 : 1, 1); // This is the value for android.hardware.Camera.setDisplayOrientation. matrix.postRotate(displayOrientation); // Camera driver coordinates range from (-1000, -1000) to (1000, 1000). // UI coordinates range from (0, 0) to (width, height). matrix.postScale(view.getWidth() / 2000f, view.getHeight() / 2000f); matrix.postTranslate(view.getWidth() / 2f, view.getHeight() / 2f); </pre> @see #startFaceDetection() 具体意思是在人脸使用的坐标和安卓屏幕坐标是不一样的,并且举了一个例子:如果屏幕尺寸是800*480,现在有一个矩形位置在人脸坐标系中位置是(-1000,-1000,0,0),那么在安卓屏幕坐标的位置是(0,0,400,240)。
并且给了转换坐标的具体方法:
/** * 将相机中用于表示人脸矩形的坐标转换成UI页面的坐标 * * @param faces 人脸数组 * @return */privateArrayList<RectF> transForm(Camera.Face[] faces){Matrixmatrix = newMatrix(); booleanmirror; if (mCameraId == Camera.CameraInfo.CAMERA_FACING_FRONT){mirror = true} else{mirror = false} //前置需要镜像if (mirror){matrix.setScale(-1f, 1f)} else{matrix.setScale(1f, 1f)} //后乘旋转角度matrix.postRotate(Float.valueOf(orientation)); //后乘缩放matrix.postScale(mSurfaceView.getWidth() / 2000f,mSurfaceView.getHeight() / 2000f); //再进行位移matrix.postTranslate(mSurfaceView.getWidth() / 2f, mSurfaceView.getHeight() / 2f); ArrayList<RectF> arrayList = newArrayList<>(); for (Camera.FacerectF : faces){RectFsrcRect = newRectF(rectF.rect); RectFdstRect = newRectF(0f, 0f, 0f, 0f); //通过Matrix映射 将srcRect放入dstRect中matrix.mapRect(dstRect, srcRect); arrayList.add(dstRect)} returnarrayList}packagecom.knight.cameraone.view; importandroid.content.Context; importandroid.graphics.Canvas; importandroid.graphics.Color; importandroid.graphics.Paint; importandroid.graphics.RectF; importandroid.support.annotation.Nullable; importandroid.util.AttributeSet; importandroid.util.TypedValue; importandroid.view.View; importjava.util.ArrayList; /** * @author created by knight * @organize * @Date 2019/10/11 13:54 * @descript:人脸框 */publicclassFaceDeteViewextendsView{privatePaintmPaint; privateStringmColor = "#42ed45"; privateArrayList<RectF> mFaces = null; publicFaceDeteView(Contextcontext){super(context); init(context)} publicFaceDeteView(Contextcontext, @NullableAttributeSetattrs){super(context, attrs); init(context)} publicFaceDeteView(Contextcontext, @NullableAttributeSetattrs, intdefStyleAttr){super(context, attrs, defStyleAttr); init(context)} privatevoidinit(Contextcontext){mPaint = newPaint(); //画笔颜色mPaint.setColor(Color.parseColor(mColor)); //只绘制图形轮廓mPaint.setStyle(Paint.Style.STROKE); //设置粗细mPaint.setStrokeWidth(TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP,1f,context.getResources().getDisplayMetrics())); //设置抗锯齿mPaint.setAntiAlias(true)} @OverrideprotectedvoidonDraw(Canvascanvas){super.onDraw(canvas); if(mFaces != null){for(RectFface:mFaces){canvas.drawRect(face,mPaint)} } } /** * 设置人人脸信息 */publicvoidsetFace(ArrayList<RectF> mFaces){this.mFaces = mFaces; //重绘矩形框invalidate()} }布局文件:
<SurfaceViewandroid:id="@+id/sf_camera"android:layout_width="match_parent"android:layout_height="match_parent"/> <!-- 新增 --> <com.knight.cameraone.view.FaceDeteView android:id="@+id/faceView"android:layout_width="match_parent"android:layout_height="match_parent"/>并增加人脸检测开关:
/** * 开启人脸检测 * */publicvoidturnFaceDetect(booleanisDetect){mFaceView.setVisibility(isDetect ? View.VISIBLE : View.GONE)}这里只是将自定义View不显示,具体效果图如下:
































