Android相机各个API拍照实践
前言
好久没更新博客了,过了年基本就没怎么动了,不过还是做了一些东西,最近有时间觉得还是得写一写,不然过段时间就忘了,不划算。
最近把Android Camera的三种API一一试了下,实现了拍照和录像,和图片、bitmap相关的功能也练习了下,比如获取、保存、删除图片等。
下面就来记录下Android三种API的拍照实践。
目标
先来明确下目标,即要做到的效果:
- 能够使用Android三种API预览、拍照、显示结果: Camera1、Camera2、CameraX
- 三种API能够自由切换,互不干扰
- 能够拿到拍照结果,并对图片进行一些操作,如保存、删除等
还是比较简单的,下面就开干。
使用Camera1 API
接口封装
在一段摸索后,我先抽象了一个接口,用来统一三种API的行为:
kotlin
import android.graphics.Bitmap
import androidx.activity.ComponentActivity
import androidx.core.util.Consumer
interface ICameraCaptureHelper<in T> {
/**
* 使用相机API开始预览
*
* @param activity 带lifecycle的activity,提供context,并且便于使用协程
* @param view 根据不同的API可能传入不同的预览页面: SurfaceView、TextureView、PreviewView
*/
fun startPreview(
activity: ComponentActivity,
view: T
)
/**
* 使用相机API拍照
*
* @param activity 带lifecycle的activity,提供context,并且便于使用协程
* @param view 根据不同的API可能传入不同的预览页面: SurfaceView、TextureView、PreviewView
* @param callback 结果回调
*/
fun takePhoto(
activity: ComponentActivity,
view: T,
callback: Consumer<Bitmap>
)
/**
* 释放资源
*/
fun release()
}
其实就三个方法,预览、拍照、释放资源,这里我传入了ComponentActivity来获取context,也方便用它的lifecycleScope来使用协程。因为拍照预览可以用SurfaceView、TextureView、PreviewView这几个,我这直接设置成了泛型,看情况用吧。
封装好接口,我们就一步一步实现功能了。
Camera1预览
使用Camera1 API,必须先预览才能拍照,其他API倒没有要求。我这用了SurfaceView来预览,TextureView也可以,下面看下startPreview的写法:
kotlin
/**
* 使用Camera API进行预览
*
* @param activity 带lifecycle的activity,提供context,并且便于使用协程
* @param view Camera API使用的 SurfaceView
*/
override fun startPreview(
activity: ComponentActivity,
view: SurfaceView
) {
// IO协程中执行,
activity.lifecycleScope.launch(Dispatchers.IO) {
// 1、获取后置摄像头ID: 默认 Camera.CameraInfo.CAMERA_FACING_BACK
val cameraId = getCameraId(mFacingType)
// 2、获取相机实例
if (mCamera == null) {
mCamera = Camera.open(cameraId)
}
// 3、设置和屏幕方向一致
setCameraDisplayOrientation(activity, mCamera!!, cameraId)
// 4、设置相机参数
setCameraParameters(mCamera!!)
// 5、在startPreview前设置holder(有前提: surfaceCreated已完成)
// 不要在surfaceCreated设置,不然有问题,使用工具类没法收到surfaceCreated回调
mCamera!!.setPreviewDisplay(view.holder)
// 6、设置SurfaceHolder回调
view.holder.addCallback(mSurfaceCallback)
// 7、开始预览
mCamera!!.startPreview()
}
}
主要就是这七步,首先要根据摄像头类型获得cameraId,再根据cameraId去打开摄像头,这时候摄像头的方向默认是横着的,还得改下摄像头方向,详细的代码后面会完整提供,现在大致看下流程,
参数设置特别要注意下,这里的width要比height更大,而且要选对尺寸,不然会造成各种意想不到的问题,后面几个API也是一样:
kotlin
private fun setCameraParameters(camera: Camera) {
val params = camera.parameters
// 设置图像格式
params.previewFormat = ImageFormat.NV21
// 设置预览尺寸(注意相机方向是width>height)
val previewSize = getOptimalPreviewSize(params.supportedPreviewSizes, 1920, 1080)
params.setPreviewSize(previewSize.width, previewSize.height)
// 设置图片尺寸
params.setPictureSize(previewSize.width, previewSize.height)
// 设置对焦模式
params.focusMode = Camera.Parameters.FOCUS_MODE_CONTINUOUS_PICTURE
// 设置闪光灯模式
params.flashMode = Camera.Parameters.FLASH_MODE_AUTO
// 设置场景模式
params.sceneMode = Camera.Parameters.SCENE_MODE_AUTO
// 应用参数设置
camera.parameters = params
}
private fun getOptimalPreviewSize(sizes: List<Camera.Size>, w: Int, h: Int): Camera.Size {
val targetRatio = w.toDouble() / h
return sizes.minByOrNull { abs(it.width.toDouble() / it.height - targetRatio) } ?: sizes[0]
}
另外一个需要注意的就是mSurfaceCallback,正常使用的话,应该在mSurfaceCallback的surfaceCreated里面去open Camera的,不过我这写成了工具类,调用的时候收不到surfaceCreated,而是已经created了,所以不处理mSurfaceCallback,直接open,当然这里也要根据实际情况去处理。
再一个就是在startPreview前,一定要调用setPreviewDisplay,传入SurfaceView的holder,再通过mCamera去startPreview就可以预览了,预览的时候确保下SurfaceView处于VISIBLE状态,预览就完成了。
Camera1拍照
搞定预览后,拍照功能其实就完成的差不多了,通过mCamera去takePicture就行了
kotlin
/**
* 使用相机API拍照
*
* @param activity 带lifecycle的activity,提供context,并且便于使用协程
* @param view SurfaceView
* @param callback 结果回调
*/
override fun takePhoto(
activity: ComponentActivity,
view: SurfaceView,
callback: Consumer<Bitmap>
) {
// camera1 API需要先预览才能拍照
if (mCamera == null) {
throw IllegalStateException("camera not prepared!!!")
}
// IO协程中执行,
activity.lifecycleScope.launch(Dispatchers.IO) {
mCamera!!.takePicture(null, null) { data, _ ->
// 处理拍照结果
val bitmap = BitmapFactory.decodeByteArray(data, 0, data.size)
val cameraInfo = Camera.CameraInfo()
Camera.getCameraInfo(Camera.CameraInfo.CAMERA_FACING_BACK, cameraInfo)
val rotation = cameraInfo.orientation
// 将结果投递到UI线程
activity.lifecycleScope.launch(Dispatchers.Main) {
callback.accept(rotateBitmap(bitmap, rotation))
}
}
}
}
private fun rotateBitmap(bitmap: Bitmap, degrees: Int): Bitmap {
val matrix = Matrix()
matrix.postRotate(degrees.toFloat())
return Bitmap.createBitmap(bitmap, 0, 0, bitmap.width, bitmap.height, matrix, true)
}
这里传出的bitmap,并且调整了下方向,没什么好说的。
Camera1释放资源
只会写拍照,没什么好说的,要能正确释放资源,才是一个好程序员,下面看下释放资源:
kotlin
/**
* 释放资源
*/
override fun release() {
mCamera?.stopPreview()
mCamera?.release()
mCamera = null
}
完整代码
上面讲了个大概,下面提供完整代码,加了个takePhotoNoFeeling无感拍照和continuePreview,使用Camera1拍照后,需要调用startPreview继续预览才能再拍照,需要注意下。 (代码有点长,还是去GitHub看吧)
使用Camera2 API
Camera1被标记过时了,Google推荐使用CameraX,CameraX其实用的也是Camera2,只不过Camera2比较难用,但是学习嘛,总得试试,就仿照上面Camera1的写法,下面看下Camera2的使用。
Camera2预览
Camera1中我们用了SurfaceView,这里就来用下TextureView,关于两者区别可以查下资料,这里不详叙,下面看预览代码:
kotlin
/**
* 使用Camera2 API进行预览
*
* @param activity 带lifecycle的activity,提供context,并且便于使用协程
* @param view Camera API2使用的 TextureView(当然也能用SurfaceView)
*/
override fun startPreview(
activity: ComponentActivity,
view: TextureView
) {
// 持有TextureView的弱引用,便于释放资源
mTextureViewRef = WeakReference(view)
// IO协程中执行,
activity.lifecycleScope.launch(Dispatchers.IO) {
// 1、获取CameraManager
val cameraManager = ContextCompat.getSystemService(activity, CameraManager::class.java)
?: throw IllegalStateException("get cameraManager fail")
// 2、获取摄像头mCameraId、摄像头信息mCameraCharacteristics
// 默认 CameraCharacteristics.LENS_FACING_BACK
chooseCameraIdByFacing(mFacingType, cameraManager)
// 3、开启相机
mCameraDevice = openCamera(cameraManager)
// 4、设置如何读取图片的ImageReader
mImageReader = getImageReader()
// 5.创建Capture Session
val surface = getSurface(view)
mSession = startCaptureSession(mutableListOf(
// 注意一定要传入使用到的surface,不然会闪退
surface,
mImageReader!!.surface
), mCameraDevice!!)
// 6.设置textureView回调
view.surfaceTextureListener = mTextureViewCallback
// 7.开始预览,预览和拍照都用request实现
preview(surface)
}
}
其实吧,和Camera1类似,都要选择相机得到mCameraId,再开启相机,只不过Camera2预览的时候,要要创建Session对话,还要把要输出的surface全部传进去,而且图片要通过ImageReader去读取,这么一搞真就复杂多了。
这里要注意下创建的session,要把预览及ImageReader的surface都传进去,不然就出错了。
kotlin
// 5.创建Capture Session
val surface = getSurface(view)
mSession = startCaptureSession(mutableListOf(
// 注意一定要传入使用到的surface,不然会闪退
surface,
mImageReader!!.surface
), mCameraDevice!!)
另外,这两者的尺寸也要匹配下,我这两个都是用的最大尺寸:
kotlin
val map = info.get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP)
?: throw IllegalStateException("Cannot get available preview/video sizes")
val largest = map.getOutputSizes(ImageFormat.JPEG).maxByOrNull { it.width * it.height }
mLargestSize = largest ?: throw IllegalStateException("Cannot get largest preview size")
和mSurfaceCallback类似,Camera2的openCamera也应该在mTextureViewCallback的onSurfaceTextureAvailable中调用,只不过我这写成工具类,收不到这个回调,直接就用了。
因为这里有很多回调,我这用了协程和suspend方法,不是必须,只不过能让代码结构更清晰。
Camera2的preview是通过发送重复的请求实现的,其实可以持有previewRequestBuilder,在过程中修改配置,达到想要的效果:
kotlin
private fun preview(surface: Surface) {
// 通过模板创建RequestBuilder
// CaptureRequest还可以配置很多其他信息,例如图像格式、图像分辨率、传感器控制、闪光灯控制、
// 3A(自动对焦-AF、自动曝光-AE和自动白平衡-AWB)控制等
val previewRequestBuilder =
mCameraDevice!!.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW)
// 设置预览画面
previewRequestBuilder.addTarget(surface)
mPreviewRequest = previewRequestBuilder.build()
mSession!!.setRepeatingRequest(mPreviewRequest!!, null, mHandler)
}
Camera2拍照
看到上面Camera2的预览就挺麻烦了,结果Camera2的拍照也比Camera1来的麻烦:
kotlin
/**
* 使用相机API拍照
*
* @param activity 带lifecycle的activity,提供context,并且便于使用协程
* @param view Camera2 API使用的 TextureView(当然也能用SurfaceView)
* @param callback 结果回调
*/
override fun takePhoto (
activity: ComponentActivity,
view: TextureView,
callback: Consumer<Bitmap>
) {
// IO协程中执行,
activity.lifecycleScope.launch(Dispatchers.IO) {
// 1、创建拍照的请求
val captureRequestBuilder =
mCameraDevice!!.createCaptureRequest(CameraDevice.TEMPLATE_STILL_CAPTURE)
// 2、设置参数
captureRequestBuilder.addTarget(mImageReader!!.surface)
captureRequestBuilder.set(CaptureRequest.CONTROL_MODE, CameraMetadata.CONTROL_MODE_AUTO)
// 3、设置拍照方向
val rotation = activity.windowManager.defaultDisplay.rotation
captureRequestBuilder.set(CaptureRequest.JPEG_ORIENTATION,
getJpegOrientation(mCameraCharacteristics!!, rotation))
mCaptureRequest = captureRequestBuilder.build()
// 4、拍照
// mSession?.stopRepeating() // 这行代码只是为了防止重复请求
// mSession?.abortCaptures() // 这行代码只是为了防止重复请求
mSession!!.capture(mCaptureRequest!!, object : CameraCaptureSession.CaptureCallback() {
override fun onCaptureCompleted(
session: CameraCaptureSession,
request: CaptureRequest,
result: TotalCaptureResult
) {
// 图片已捕获
// 可选步骤,根据需要进行处理
}
}, mHandler)
// 5、设置图片回调,拿到结果
setImageReaderCallback(callback)
}
}
private fun setImageReaderCallback(callback: Consumer<Bitmap>) {
mImageReader?.setOnImageAvailableListener({
val image = mImageReader!!.acquireNextImage()
image?.use {
val planes = it.planes
if (planes.isNotEmpty()) {
val buffer = planes[0].buffer
val data = ByteArray(buffer.remaining())
buffer.get(data)
// 转成bitmap
val bitmap = BitmapFactory.decodeByteArray(data, 0, data.size)
// 传递结果,mHandler应该是在UI线程了
callback.accept(bitmap)
}
}
}, mHandler)
}
这里需要用captureRequestBuilder创建拍照请求,设置好参数,最后通过mSession去拍照,这里能拿到TotalCaptureResult,里面有很多数据,只不过我只想拿bitmap,所以要去mImageReader获取。
Camera2释放资源
Camera2涉及的东西更多,释放资源也更复杂些,需要注意下。
kotlin
override fun release() {
// 从 SurfaceTexture 中移除 SurfaceTextureListener
mTextureViewRef?.get()?.surfaceTextureListener = null
// 需要关闭这三个
mCameraDevice?.close()
mSession?.close()
mImageReader?.close()
mHandler.removeCallbacksAndMessages(null)
}
完整代码
需要注意Android 5.0以后版本才能使用Camera2 API。 Camera2CaptureHelper
使用CameraX API
Camera1 API简单但是过时了,Camera2 API功能强大,使用起来却十分复杂,还好Google在JetPack中提供了CameraX,方便我们使用Camera的相关功能,下面就俩看看吧。
引入CameraX库
CameraX作为JetPack的库,还是需要我们引入库的,我这用的version catalog管理依赖,实际都差不多,就下面几个库(算是把拍照也引入了):
toml
# cameraX
camerax = "1.1.0-beta01"
# cameraX
camerax = { module = "androidx.camera:camera-core", version.ref = "camerax"}
camerax_camera2 = { module = "androidx.camera:camera-camera2", version.ref = "camerax"}
camerax_video = { module = "androidx.camera:camera-video", version.ref = "camerax"}
camerax_lifecycler = { module = "androidx.camera:camera-lifecycle", version.ref = "camerax"}
camerax_view = { module = "androidx.camera:camera-view", version.ref = "camerax"}
camerax_extensions = { module = "androidx.camera:camera-extensions", version.ref = "camerax"}
这里的cameraX版本并没有用最新的,我试了1.2和1.3版本,需要升级比较高的gradle版本,想想还是算了。
在dependence里面添加上依赖,就能开始写代码了。
kotlin
dependencies {
//。。。
// CameraX 相关依赖
implementation(libs.camerax)
implementation(libs.camerax.camera2)
implementation(libs.camerax.lifecycler)
implementation(libs.camerax.video)
implementation(libs.camerax.view)
implementation(libs.camerax.extensions)
}
CameraX预览
前面我们分别使用了SurfaceView和TextureView进行预览,而在CameraX里面提供了更好的PreviewView来预览,下面看下CameraX如何预览:
kotlin
/**
* 使用CameraX API进行预览
*
* @param activity 带lifecycle的activity,提供context,并且便于使用协程
* @param view Camera API使用的 PreviewView
*/
override fun startPreview(
activity: ComponentActivity,
view: PreviewView
) {
val cameraProviderFuture = ProcessCameraProvider.getInstance(activity)
cameraProviderFuture.addListener({
// 用于将相机的生命周期绑定到生命周期所有者
// 消除了打开和关闭相机的任务,因为 CameraX 具有生命周期感知能力
mCameraProvider = cameraProviderFuture.get()
// 预览
val preview = Preview.Builder()
.build()
.also {
it.setSurfaceProvider(view.surfaceProvider)
}
// 拍照的使用场景
imageCapture = ImageCapture.Builder()
.build()
// 选择摄像头,省去了去判断摄像头ID
// 默认 CameraSelector.DEFAULT_BACK_CAMERA
val cameraSelector = mSelector
try {
// Unbind use cases before rebinding
mCameraProvider!!.unbindAll()
// 将相机绑定到 lifecycleOwner,就不用手动关闭了
mCameraProvider!!.bindToLifecycle(
activity, cameraSelector, preview, imageCapture)
} catch(exc: Exception) {
Log.e("TAG", "Use case binding failed", exc)
}
// 回调代码在主线程处理
}, ContextCompat.getMainExecutor(activity))
}
果然看起来就舒服多了,和Camera2比起来,简单太多了,就是获取了一个mCameraProvider,设置下preview,然后绑定到activity的生命周期就能预览了,根本不需要怎么解释。
如果要拍照,创建个imageCapture,在bindToLifecycle最后加上就行了,这里还帮我们搞定了异步线程问题。
CameraX拍照
CameraX的预览很简单,拍照就更简单了:
kotlin
/**
* 使用相机API拍照
*
* @param activity 带lifecycle的activity,提供context,并且便于使用协程
* @param view Camerax API使用的 PreviewView
* @param callback 结果回调
*/
override fun takePhoto (
activity: ComponentActivity,
view: PreviewView,
callback: Consumer<Bitmap>
) {
// Get a stable reference of the modifiable image capture use case
val imageCapture = imageCapture ?: return
// 直接拍照拿bitmap,存文件可以用 OutputFileOptions
imageCapture.takePicture(
ContextCompat.getMainExecutor(activity),
object : ImageCapture.OnImageCapturedCallback() {
override fun onCaptureSuccess(image: ImageProxy) {
// 转换为 Bitmap,并传递结果
callback.accept(imageProxyToBitmap(image))
image.close()
}
override fun onError(exc: ImageCaptureException) {
// 处理拍摄过程中的异常
Log.e("TAG", "Photo capture failed: ${exc.message}", exc)
}
}
)
}
private fun imageProxyToBitmap(image: ImageProxy): Bitmap {
val planeProxy = image.planes[0]
val buffer = planeProxy.buffer
val bytes = ByteArray(buffer.remaining())
buffer.get(bytes)
return BitmapFactory.decodeByteArray(bytes, 0, bytes.size)
}
直接通过imageCapture的takePicture拍照,拿到image对象就能获取bitmap了,so easy!
CameraX释放资源
CameraX不用了直接解除生命周期的绑定就行了。
kotlin
/**
* 释放资源
*/
override fun release() {
// 取消绑定生命周期观察者
mCameraProvider?.unbindAll()
}
完整代码
使用Demo
我这写了个例子,用了这三个API,还加上了系统拍照、系统选取、系统分享、保存相册等,有兴趣可以参考下:
Demo地址:
小结
花了点时间,把Android相机中Camera1、Camera2、CameraX三个API的拍照功能实践了下,并编写成工具类方便使用。