Android: Camera相机开发详解(中) ——实现预览、
前言
-
在上一篇文章中给小伙伴们介绍了进行Camera开发需要了解的知识点,如果你还没有看过的话,建议先去看上一篇文章《Android: Camera相机开发详解(上) —— 知识储备》
-
本篇文章会带着小伙伴们一步一步实现自己的Camera,并在实现的过程中验证上一篇中所讲解的结论
实现思路:
-
在xml布局中定义一个SurfaceView,用于预览相机采集的数据
-
给SurfaceHolder添加回调,在surfaceCreated(holder: SurfaceHolder?)回调中打开相机
-
成功打开相机后,设置相机参数。比如:对焦模式,预览大小,照片保存大小等等
-
设置相机预览时的旋转角度,然后调用startPreview()开始预览
-
调用takePicture方法拍照 或者 是在Camera的预览回调中 保存照片
-
对保存的照片进行旋转处理,使其为"自然方向"
-
关闭页面,释放相机资源
具体实现步骤:
一丶申请权限
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
二、在xml布局文件中定义一个SurfaceView
<SurfaceView
android:id="@+id/surfaceView"
android:layout_width="match_parent"
android:layout_height="match_parent" />
三、创建一个CameraHelper类
class CameraHelper(activity: Activity, surfaceView: SurfaceView) : Camera.PreviewCallback {
private var mCamera: Camera? = null //Camera对象
private lateinit var mParameters: Camera.Parameters //Camera对象的参数
private var mSurfaceView: SurfaceView = surfaceView //用于预览的SurfaceView对象
var mSurfaceHolder: SurfaceHolder //SurfaceHolder对象
private var mActivity: Activity = activity
private var mCallBack: CallBack? = null //自定义的回调
var mCameraFacing = Camera.CameraInfo.CAMERA_FACING_BACK //摄像头方向
var mDisplayOrientation: Int = 0 //预览旋转的角度
private var picWidth = 2160 //保存图片的宽
private var picHeight = 3840 //保存图片的高
}
由于对Camera的操作等代码比较多,本着各司其职的原则,创建了一个CameraHelper类来处理Camera相关的操作,如果放在Activity中对Camera操作会使Activity臃肿复杂
CameraHelper的构造方法有两个,一个是Activity对象,一个是SurfaceView对象(就是xml文件里定义的SurfaceView)
四、给SurfaceView对象添加回调函数,并初始化相机
private fun init() {
mSurfaceHolder.addCallback(object : SurfaceHolder.Callback {
override fun surfaceChanged(holder: SurfaceHolder?, format: Int, width: Int, height: Int) {
}
override fun surfaceDestroyed(holder: SurfaceHolder?) {
releaseCamera() //释放相机资源
}
override fun surfaceCreated(holder: SurfaceHolder?) { //surface创建
if (mCamera == null) {
openCamera(mCameraFacing) //打开相机
}
startPreview() //开始预览
}
})
}
//打开相机
private fun openCamera(cameraFacing: Int = Camera.CameraInfo.CAMERA_FACING_BACK): Boolean {
var supportCameraFacing = supportCameraFacing(cameraFacing) //判断手机是否支持前置/后置摄像头
if (supportCameraFacing) {
try {
mCamera = Camera.open(cameraFacing)
initParameters(mCamera!!) //初始化相机配置信息
mCamera?.setPreviewCallback(this)
} catch (e: Exception) {
e.printStackTrace()
toast("打开相机失败!")
return false
}
}
return supportCameraFacing
}
//判断是否支持某个相机
private fun supportCameraFacing(cameraFacing: Int): Boolean {
var info = Camera.CameraInfo()
for (i in 0 until Camera.getNumberOfCameras()) {
Camera.getCameraInfo(i, info)
if (info.facing == cameraFacing) return true
}
return false
}
在CameraHelper的创建后调用init()方法。在init()方法中,我们首先对mSurfaceHolder添加了一个回调,这个回调会告诉我们SurfaceView中surface的变化(在上一篇上有讲解)
在surfaceCreated(holder: SurfaceHolder?) 回调中打开相机。因为相机开始预览的时候,如果SurfaceView中的surface还没有创建,就回抛出异常,所以我们在surface创建后再对相机进行操作
我们调用相机的open()方法打开一个摄像头,在打开摄像头之前判断一下手机是否支持我们将要打开的摄像头。
五、配置相机参数
//配置相机参数
private fun initParameters(camera: Camera) {
try {
mParameters = camera.parameters
mParameters.previewFormat = ImageFormat.NV21 //设置预览图片的格式
//获取与指定宽高相等或最接近的尺寸
//设置预览尺寸
val bestPreviewSize = getBestSize(mSurfaceView.width, mSurfaceView.height, mParameters.supportedPreviewSizes)
bestPreviewSize?.let {
mParameters.setPreviewSize(it.width, it.height)
}
//设置保存图片尺寸
val bestPicSize = getBestSize(picWidth, picHeight, mParameters.supportedPictureSizes)
bestPicSize?.let {
mParameters.setPictureSize(it.width, it.height)
}
//对焦模式
if (isSupportFocus(Camera.Parameters.FOCUS_MODE_CONTINUOUS_PICTURE))
mParameters.focusMode = Camera.Parameters.FOCUS_MODE_CONTINUOUS_PICTURE
camera.parameters = mParameters
} catch (e: Exception) {
e.printStackTrace()
toast("相机初始化失败!")
}
}
//获取与指定宽高相等或最接近的尺寸
private fun getBestSize(targetWidth: Int, targetHeight: Int, sizeList: List<Camera.Size>): Camera.Size? {
var bestSize: Camera.Size? = null
var targetRatio = (targetHeight.toDouble() / targetWidth) //目标大小的宽高比
var minDiff = targetRatio
for (size in sizeList) {
var supportedRatio = (size.width.toDouble() / size.height)
log("系统支持的尺寸 : ${size.width} * ${size.height} , 比例$supportedRatio")
}
for (size in sizeList) {
if (size.width == targetHeight && size.height == targetWidth) {
bestSize = size
break
}
var supportedRatio = (size.width.toDouble() / size.height)
if (Math.abs(supportedRatio - targetRatio) < minDiff) {
minDiff = Math.abs(supportedRatio - targetRatio)
bestSize = size
}
}
log("目标尺寸 :$targetWidth * $targetHeight , 比例 $targetRatio")
log("最优尺寸 :${bestSize?.height} * ${bestSize?.width}")
return bestSize
}
我们对预览大小和保存图片大小进行设置,在设置的时候,我们应该获取到与指定宽高相等或最接近的尺寸,这样的话才能保证图片既不变形又能最接近我们指定的大小。
下面是vivo x9的后置摄像头支持的尺寸:
相机预览大小.png 保存图片的大小.png六、开始预览
//开始预览
fun startPreview() {
mCamera?.let {
it.setPreviewDisplay(mSurfaceHolder) //设置相机预览对象
// setCameraDisplayOrientation(mActivity) //设置预览时相机旋转的角度
it.startPreview()
}
}
调用startPreview()方法开始预览,我们先看一下预览效果:
设置角度前预览效果.jpg我们可以看到,画面并不是"自然方向"而且被拉伸。这个在上一篇已经讲解过,下面通过setDisplayOrientation(int degree)方法,使其正常显示
//设置预览旋转的角度
private fun setCameraDisplayOrientation(activity: Activity) {
var info = Camera.CameraInfo()
Camera.getCameraInfo(mCameraFacing, info)
val rotation = activity.windowManager.defaultDisplay.rotation
var screenDegree = 0
when (rotation) {
Surface.ROTATION_0 -> screenDegree = 0
Surface.ROTATION_90 -> screenDegree = 90
Surface.ROTATION_180 -> screenDegree = 180
Surface.ROTATION_270 -> screenDegree = 270
}
if (info.facing == Camera.CameraInfo.CAMERA_FACING_FRONT) {
mDisplayOrientation = (info.orientation + screenDegree) % 360
mDisplayOrientation = (360 - mDisplayOrientation) % 360 // compensate the mirror
} else {
mDisplayOrientation = (info.orientation - screenDegree + 360) % 360
}
mCamera?.setDisplayOrientation(mDisplayOrientation)
log("屏幕的旋转角度 : $rotation")
log("setDisplayOrientation(result) : $mDisplayOrientation")
}
设置后预览效果如下:
设置角度后预览效果.jpg上一篇提到的相机的预览方向:
相机预览方向.png 后置摄像头预览旋转角度.png 前置摄像头预览旋转角度.png通过日志我们看到,前后摄像头的预览旋转角度都是90
前置摄像头在进行角度旋转之前,图像会进行一个水平的镜像翻转,所以前置摄像头应该设置的旋转角度是 270 - 180 = 90
七、进行拍照
拍照的话有两种方式:
-
调用takePicture(ShutterCallback shutter, PictureCallback raw,
PictureCallback jpeg) 方法 -
在相机的预览回调里直接保存
1.调用takePicture(ShutterCallback shutter, PictureCallback raw, PictureCallback jpeg) 拍照
//拍摄照片
fun takePic() {
mCamera?.let {
it.takePicture({}, null, { data, _ ->
it.startPreview()
savePic(data) //保存图片
})
}
}
//保存照片
private fun savePic(data: ByteArray?) {
thread {
try {
val temp = System.currentTimeMillis()
val picFile = FileUtil.createCameraFile()
if (picFile != null && data != null) {
val rawBitmap = BitmapFactory.decodeByteArray(data, 0, data.size)
Okio.buffer(Okio.sink(picFile)).write(BitmapUtils.toByteArray(resultBitmap)).close()
runOnUiThread {
toast("图片已保存! ${picFile.absolutePath}")
log("图片已保存! 耗时:${System.currentTimeMillis() - temp} 路径: ${picFile.absolutePath}")
}
}
} catch (e: Exception) {
e.printStackTrace()
runOnUiThread {
toast("保存图片失败!")
}
}
}
}
takePicture(ShutterCallback shutter, PictureCallback raw, PictureCallback jpeg)方法有3个参数,而且这3个参数都是抽象接口:
-
第一个是点击拍照时的回调。
如果传null,则没有任何效果
如果写一个空实现,则在点击拍照时会有"咔擦"声 -
第二个和第三个参数类型一样,PictureCallback 有一个抽象方法
void onPictureTaken(byte[] data, Camera camera)
data就是点击拍照后相机返回的照片的byte数组,用该数组创建一个bitmap保存下来,就得到了拍摄的照片
2.在相机的预览回调里直接保存
override fun onPreviewFrame(data: ByteArray?, camera: Camera?) {
savePic(data) //保存照片
}
注意:实际上这个回调方法会一直一直的调用,如果要保存一张照片的话应该加个字段进行控制,此处只是做演示
在保存图片的时候,我们需要开启一个子线程来进行操作,通过日志输出可以看到保存图片所用时间和保存路径:
保存图片.png八、调整保存照片的方向
与预览时方向类似,照片在保存时也有一个方向。我们先看一下在上一步中保存的照片是什么样的:
后置摄像头:
后置摄像头拍摄照片.png
前置摄像头:
前置摄像头拍摄照片.png
下面我们在保存图片的时候,对照片进行旋转处理,保存照片的方法应该如下:
private fun savePic(data: ByteArray?) {
thread {
try {
val temp = System.currentTimeMillis()
val picFile = FileUtil.createCameraFile()
if (picFile != null && data != null) {
val rawBitmap = BitmapFactory.decodeByteArray(data, 0, data.size)
val resultBitmap = if (mCameraHelper.mCameraFacing == Camera.CameraInfo.CAMERA_FACING_FRONT)
BitmapUtils.rotate(rawBitmap, 270f) //前置摄像头旋转270°
else
BitmapUtils.rotate(rawBitmap, 90f) //后置摄像头旋转90°
Okio.buffer(Okio.sink(picFile)).write(BitmapUtils.toByteArray(resultBitmap)).close()
runOnUiThread {
toast("图片已保存! ${picFile.absolutePath}")
log("图片已保存! 耗时:${System.currentTimeMillis() - temp} 路径: ${picFile.absolutePath}")
}
}
} catch (e: Exception) {
e.printStackTrace()
runOnUiThread {
toast("保存图片失败!")
}
}
}
}
//图片工具类
object BitmapUtils {
//水平镜像翻转
fun mirror(rawBitmap: Bitmap): Bitmap {
var matrix = Matrix()
matrix.postScale(-1f, 1f)
return Bitmap.createBitmap(rawBitmap, 0, 0, rawBitmap.width, rawBitmap.height, matrix, true)
}
//旋转
fun rotate(rawBitmap: Bitmap, degree: Float): Bitmap {
var matrix = Matrix()
matrix.postRotate(degree)
return Bitmap.createBitmap(rawBitmap, 0, 0, rawBitmap.width, rawBitmap.height, matrix, true)
}
|
然后我们在进行一次拍照:
后置摄像头:
后置摄像头拍摄照片旋转后.png
前置摄像头:
前置摄像头拍摄照片旋转后.png
对比一下上一篇文章所讲的相机保存照片的方向:
图六、采集的图像方向.png关于前置摄像头所拍摄照片,需要注意的是,由于在setDisplayOrientation()设置相机预览方向的时候系统默认做了一个水平镜面的翻转,所以我们通过前置摄像头保存来的照片并不是和预览时看到的一样,两者是水平镜像关系。所以,一般情况下我们不仅仅需要对前置摄像头做旋转,还应该做一个水平方向的镜面翻转处理。
在上面保存图片的方法中判断如果是前置摄像头的话,代码修改如下:
BitmapUtils.mirror(BitmapUtils.rotate(rawBitmap, 270f)) //旋转270,然后水平镜面翻转
这样的话,就能保证所拍摄照片与在预览时所呈现的画面是一模一样的,如下图:
前置摄像头预览与保存一致.png注:如果有小伙伴对这点还不太理解的话,墙裂建议自己用前置摄像头自拍一张,然后在对比保存的照片与预览时手机里显示的画面,就很容易理解了
不是我不愿意自己自拍来给小伙们演示,长相实在是有点惨,所以大家还是自己亲自验证吧o(╥﹏╥)o
九、释放相机资源
在Activity销毁前或者是关闭相机时,应当释放当前相机资源
//释放相机
fun releaseCamera() {
if (mCamera != null) {
mCamera?.stopPreview()
mCamera?.setPreviewCallback(null)
mCamera?.release()
mCamera = null
}
}
完整效果如下:
拍照效果图.gif总结
本篇文章主要给小伙伴们介绍了实现Camera拍照功能的流程及步骤,并且用实际效果验证了上一篇文章中所讲解的理论
下一篇文章将会给小伙伴们介绍如何实现人脸检测功能,敬请期待~~
完整代码
https://github.com/smashinggit/Study
注:此工程包含多个module,本文所用代码均在camerademo文件夹下