Android Camera各个API拍照实践 Androi

Android Camera各个API拍照实践

前言

好久没更新博客了,过了年基本就没怎么动了,不过还是做了一些东西,最近有时间觉得还是得写一写,不然过段时间就忘了,不划算。

最近把Android Camera的三种API一一试了下,实现了拍照和录像,和图片、bitmap相关的功能也练习了下,比如获取、保存、删除图片等。

ps. 录像的文章也写好了,有兴趣可以继续看下:

Android Camera各个API录像实践

下面就来记录下Android三种API的拍照实践。

目标

先来明确下目标,即要做到的效果:

  1. 能够使用Android三种API预览、拍照、显示结果: Camera1、Camera2、CameraX
  2. 三种API能够自由切换,互不干扰
  3. 能够拿到拍照结果,并对图片进行一些操作,如保存、删除等

还是比较简单的,下面就开干。

使用Camera1 API

接口封装

在一段摸索后,我先抽象了一个接口,用来统一三种API的行为:

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
32
33
34
35
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的写法:

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
32
33
34
35
36
37
38
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也是一样:

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
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就行了

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
32
33
34
35
36
37
38
39
40
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释放资源

只会写拍照,没什么好说的,要能正确释放资源,才是一个好程序员,下面看下释放资源:

1
2
3
4
5
6
7
8
kotlin复制代码/**
* 释放资源
*/
override fun release() {
mCamera?.stopPreview()
mCamera?.release()
mCamera = null
}

完整代码

上面讲了个大概,下面提供完整代码,加了个takePhotoNoFeeling无感拍照和continuePreview,使用Camera1拍照后,需要调用startPreview继续预览才能再拍照,需要注意下。
(代码有点长,还是去GitHub看吧)

Camera1CaptureHelper

使用Camera2 API

Camera1被标记过时了,Google推荐使用CameraX,CameraX其实用的也是Camera2,只不过Camera2比较难用,但是学习嘛,总得试试,就仿照上面Camera1的写法,下面看下Camera2的使用。

Camera2预览

Camera1中我们用了SurfaceView,这里就来用下TextureView,关于两者区别可以查下资料,这里不详叙,下面看预览代码:

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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
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都传进去,不然就出错了。

1
2
3
4
5
6
7
kotlin复制代码// 5.创建Capture Session
val surface = getSurface(view)
mSession = startCaptureSession(mutableListOf(
// 注意一定要传入使用到的surface,不然会闪退
surface,
mImageReader!!.surface
), mCameraDevice!!)

另外,这两者的尺寸也要匹配下,我这两个都是用的最大尺寸:

1
2
3
4
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,在过程中修改配置,达到想要的效果:

1
2
3
4
5
6
7
8
9
10
11
12
13
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来的麻烦:

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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
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涉及的东西更多,释放资源也更复杂些,需要注意下。

1
2
3
4
5
6
7
8
9
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管理依赖,实际都差不多,就下面几个库(算是把拍照也引入了):

1
2
3
4
5
6
7
8
9
10
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里面添加上依赖,就能开始写代码了。

1
2
3
4
5
6
7
8
9
10
11
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如何预览:

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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
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的预览很简单,拍照就更简单了:

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
32
33
34
35
36
37
38
39
40
41
42
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不用了直接解除生命周期的绑定就行了。

1
2
3
4
5
6
7
kotlin复制代码/**
* 释放资源
*/
override fun release() {
// 取消绑定生命周期观察者
mCameraProvider?.unbindAll()
}

完整代码

CameraXCaptureHelper

使用Demo

我这写了个例子,用了这三个API,还加上了系统拍照、系统选取、系统分享、保存相册等,有兴趣可以参考下:

image.png

Demo地址:

TakePhotoFragment

小结

花了点时间,把Android相机中Camera1、Camera2、CameraX三个API的拍照功能实践了下,并编写成工具类方便使用。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

0%