Jetpack Compose中使用相机

前言

官方文档

https://developer.android.google.cn/jetpack/androidx/releases/camera?hl

在 Jetpack Compose 中获取相机画面可以按照以下步骤进行:

添加依赖

向您的 /gradle/libs.versions.toml 中添加以下内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
[versions]
agp = "8.6.0"
camerax = "1.5.1"
cameraView = "1.5.1"


[libraries]
# Contains the basic camera functionality such as SurfaceRequest
androidx-camera-core = { module = "androidx.camera:camera-core", version.ref = "camerax" }
# Contains the CameraXViewfinder composable
androidx-camera-compose = { module = "androidx.camera:camera-compose", version.ref = "camerax" }
# Allows us to bind the camera preview to our UI lifecycle
androidx-camera-lifecycle = { group = "androidx.camera", name = "camera-lifecycle", version.ref = "camerax" }
# The specific camera implementation that renders the preview
androidx-camera-camera2 = { module = "androidx.camera:camera-camera2", version.ref = "camerax" }
androidx-camera-view = { group = "androidx.camera", name = "camera-view", version.ref = "cameraView" }

依赖

1
2
3
4
5
6
7
dependencies {
implementation(libs.androidx.camera.core)
implementation(libs.androidx.camera.compose)
implementation(libs.androidx.camera.lifecycle)
implementation(libs.androidx.camera.camera2)
implementation(libs.androidx.camera.view)
}

注意

agp 最低版本要求是:”8.6.0”

设置权限

添加权限

AndroidManifest.xml 文件中添加相机权限:

1
<uses-permission android:name="android.permission.CAMERA" />

如果你的应用面向 Android 13 及以上版本,还需要在 AndroidManifest.xml 中声明相机使用情况:

1
2
3
<uses-permission android:name="android.permission.CAMERA" />
<uses-feature android:name="android.hardware.camera" />
<uses-feature android:name="android.hardware.camera.autofocus" />

权限申请

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
import android.Manifest
import android.content.pm.PackageManager
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.compose.ui.platform.LocalContext
import androidx.core.content.ContextCompat
import androidx.compose.foundation.layout.Column
import androidx.compose.material3.Button
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember

@Composable
fun CameraPermissionRequest(onPermissionGranted: () -> Unit) {
val context = LocalContext.current
var hasCameraPermission by remember {
mutableStateOf(
ContextCompat.checkSelfPermission(
context,
Manifest.permission.CAMERA
) == PackageManager.PERMISSION_GRANTED
)
}

val launcher = rememberLauncherForActivityResult(
contract = ActivityResultContracts.RequestPermission()
) { isGranted: Boolean ->
hasCameraPermission = isGranted
}

if (hasCameraPermission) {
onPermissionGranted()
} else {
Column {
Text("需要相机权限才能使用相机功能。")
Button(onClick = { launcher.launch(Manifest.permission.CAMERA) }) {
Text("请求权限")
}
}
}
}

图像翻转

预览翻转

我这里摄像头预览的界面和拍照获取的界面图片方向不一致,AI提供的翻转previewView也不行,最后试的翻转graphicsLayer可以。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
val cameraSelector = ZCameraUtils.getCameraSelector()
var modifier = Modifier
.fillMaxSize()
if (cameraSelector == CameraSelector.DEFAULT_BACK_CAMERA) {
modifier = modifier.graphicsLayer {
scaleX = -1f
scaleY = -1f
}
} else {
modifier = modifier.graphicsLayer {
scaleY = -1f
}
}
AndroidView(
factory = {
previewView.implementationMode = PreviewView.ImplementationMode.COMPATIBLE
previewView
},
modifier = modifier
)

要注意以下两个地方

必须设置previewView的实现模式(Use a TextureView for the preview.)

1
previewView.implementationMode = PreviewView.ImplementationMode.COMPATIBLE

设置graphicsLayer

1
2
3
4
5
Modifier
.fillMaxSize()
.graphicsLayer{
scaleY = -1f
}

图像旋转

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
fun rotateBitmap(bm: Bitmap, degree: Float): Bitmap {
val matrix: Matrix = Matrix()
matrix.setRotate(
degree,
bm.getWidth() / 2f, // 旋转中心 X
bm.getHeight() / 2f
) // 旋转中心 Y

return Bitmap.createBitmap(
bm, 0, 0,
bm.getWidth(), bm.getHeight(),
matrix, true
)
}

fun Bitmap.rotate180(): Bitmap? {
return rotateBitmap(this, 180f)
}

使用

1
2
3
4
5
6
7
8
9
10
val cameraSelector = ZCameraUtils.getCameraSelector()
latestBitmap?.let {
if (cameraSelector == CameraSelector.DEFAULT_BACK_CAMERA) {
capturedImage = it.rotate180()
} else {
capturedImage = it
}
} ?: run {
isTakingPhoto.value = false
}

图像翻转

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
/**
* 对 Bitmap 进行水平镜像翻转(左右翻转)
*
* @return 新的翻转后的 Bitmap;如果原图为空或无效,则返回原图
*/
fun Bitmap.mirrorHorizontal(): Bitmap {
if (this.isRecycled) return this

val matrix = Matrix().apply {
postScale(-1f, 1f) // 水平翻转(X轴缩放 -1)
postTranslate(width.toFloat(), 0f) // 将图像移回可视区域
}

return try {
Bitmap.createBitmap(this, 0, 0, width, height, matrix, true)
} catch (e: OutOfMemoryError) {
// 内存不足时可选择返回原图或 null
e.printStackTrace()
this
}
}

fun Bitmap.mirrorVertical(): Bitmap {
if (this.isRecycled) return this

val matrix = Matrix().apply {
postScale(1f, -1f) // 垂直翻转
postTranslate(0f, height.toFloat())
}

return try {
Bitmap.createBitmap(this, 0, 0, width, height, matrix, true)
} catch (e: OutOfMemoryError) {
e.printStackTrace()
this
}
}

获取摄像头

获取所有的摄像头

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
69
/**
* 摄像头信息数据类
* @param isFrontFacing 是否为前置摄像头
* @param lensFacing 镜头朝向类型 (LENS_FACING_FRONT 或 LENS_FACING_BACK)
* @param cameraSelector 对应的 CameraSelector(可用于绑定相机)
* @param displayName 显示名称(如"后置摄像头"、"前置摄像头")
*/
data class CameraInfo(
val isFrontFacing: Boolean,
val lensFacing: Int,
val cameraSelector: CameraSelector,
val displayName: String
)

object ZCameraUtils {

/**
* 获取设备可用的摄像头列表
* @param context 上下文
* @param callback 回调函数,返回摄像头信息列表
*/
fun getAvailableCameras(
context: Context,
callback: (List<CameraInfo>) -> Unit
) {
val cameraProviderFuture = ProcessCameraProvider.getInstance(context)
cameraProviderFuture.addListener({
try {
val cameraProvider = cameraProviderFuture.get()
val cameraInfos = mutableListOf<CameraInfo>()

// 获取所有可用摄像头
for (cameraInfo in cameraProvider.availableCameraInfos) {
// 判断是前置还是后置摄像头
val lensFacing = cameraInfo.lensFacing
val isFrontFacing = lensFacing == CameraSelector.LENS_FACING_FRONT

// 创建对应的 CameraSelector
val cameraSelector = if (isFrontFacing) {
CameraSelector.DEFAULT_FRONT_CAMERA
} else {
CameraSelector.DEFAULT_BACK_CAMERA
}

val displayName = if (isFrontFacing) "前置摄像头" else "后置摄像头"

cameraInfos.add(
CameraInfo(
isFrontFacing = isFrontFacing,
lensFacing = lensFacing,
cameraSelector = cameraSelector,
displayName = displayName
)
)
}

Log.d("CameraX", "可用摄像头数量: ${cameraInfos.size}")
cameraInfos.forEach {
Log.d("CameraX", "摄像头: ${it.displayName}, 前置=${it.isFrontFacing}")
}

callback(cameraInfos)
} catch (e: Exception) {
Log.e("CameraX", "获取摄像头列表失败", e)
callback(emptyList())
}
}, ContextCompat.getMainExecutor(context))
}
}

调用方式

1
2
3
4
5
6
7
8
ZCameraUtils.getAvailableCameras(context) { 
cameras ->
cameras.forEach {
info ->
Log.d("Camera", "名称: ${info.displayName}")
Log.d("Camera", "CameraSelector: ${info.cameraSelector}")
}
}

获取设备使用的摄像头

这里根据型号获取摄像头的方向是因为

平板前面的摄像头有的是后置的,有的是前置的。

所以只能根据型号来获取用前置的还是后置的。

1
2
3
4
5
6
7
8
9
10
11
object ZCameraUtils {
fun getCameraSelector(): CameraSelector {
val systemModel = ZDeviceUtils.getSystemModel()
val backArr = arrayOf("G156JW_A13")
return if (systemModel in backArr) {
CameraSelector.DEFAULT_BACK_CAMERA
} else {
CameraSelector.DEFAULT_FRONT_CAMERA
}
}
}

获取设备型号

1
2
3
4
5
6
7
8
9
10
public class ZDeviceUtils {
/**
* 获取手机型号
*
* @return 手机型号
*/
public static String getSystemModel() {
return Build.MODEL;
}
}

预览与高清拍照

工具类

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
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
import android.content.Context
import android.graphics.Bitmap
import android.graphics.Matrix
import android.util.Log
import android.util.Size
import android.view.View
import androidx.camera.core.CameraSelector
import androidx.camera.core.ImageCapture
import androidx.camera.core.ImageCaptureException
import androidx.camera.core.Preview
import androidx.camera.core.UseCaseGroup
import androidx.camera.core.resolutionselector.AspectRatioStrategy
import androidx.camera.core.resolutionselector.ResolutionSelector
import androidx.camera.core.resolutionselector.ResolutionStrategy
import androidx.camera.lifecycle.ProcessCameraProvider
import androidx.camera.view.PreviewView
import androidx.core.content.ContextCompat
import androidx.lifecycle.LifecycleOwner
import com.xhkjedu.zxs_android.utils.common.ZDeviceUtils

object ZCameraUtils {
fun getCameraSelector(): CameraSelector {
val systemModel = ZDeviceUtils.getSystemModel()
val backArr = arrayOf("G156JW_A13")
return if (systemModel in backArr) {
CameraSelector.DEFAULT_BACK_CAMERA
} else {
CameraSelector.DEFAULT_FRONT_CAMERA
}
}


fun rotateBitmap(bm: Bitmap, degree: Float): Bitmap {
val matrix: Matrix = Matrix()
matrix.setRotate(
degree,
bm.getWidth() / 2f, // 旋转中心 X
bm.getHeight() / 2f
) // 旋转中心 Y

return Bitmap.createBitmap(
bm, 0, 0,
bm.getWidth(), bm.getHeight(),
matrix, true
)
}

fun Bitmap.rotate180(): Bitmap {
return rotateBitmap(this, 180f)
}


/**
* 对 Bitmap 进行水平镜像翻转(左右翻转)
*
* @return 新的翻转后的 Bitmap;如果原图为空或无效,则返回原图
*/
fun Bitmap.mirrorHorizontal(): Bitmap {
if (this.isRecycled) return this

val matrix = Matrix().apply {
postScale(-1f, 1f) // 水平翻转(X轴缩放 -1)
postTranslate(width.toFloat(), 0f) // 将图像移回可视区域
}

return try {
Bitmap.createBitmap(this, 0, 0, width, height, matrix, true)
} catch (e: OutOfMemoryError) {
// 内存不足时可选择返回原图或 null
e.printStackTrace()
this
}
}

fun Bitmap.mirrorVertical(): Bitmap {
if (this.isRecycled) return this

val matrix = Matrix().apply {
postScale(1f, -1f) // 垂直翻转
postTranslate(0f, height.toFloat())
}

return try {
Bitmap.createBitmap(this, 0, 0, width, height, matrix, true)
} catch (e: OutOfMemoryError) {
e.printStackTrace()
this
}
}

// 绑定相机用例:预览 + 高质量拍照
fun bindCameraUseCases(
previewView: PreviewView?,
context: Context,
lifecycleOwner: LifecycleOwner,
onImageCaptureReady: (ImageCapture) -> Unit
) {
if (previewView == null) return
val cameraProviderFuture = ProcessCameraProvider.getInstance(context)
cameraProviderFuture.addListener({
val cameraProvider: ProcessCameraProvider = cameraProviderFuture.get()

// 预览与拍照共用同一套宽高比,避免 16:9 预览 + 4:3 成片导致取景范围不一致
val aspectStrategy = AspectRatioStrategy.RATIO_4_3_FALLBACK_AUTO_STRATEGY

val previewSelector = ResolutionSelector.Builder()
.setAspectRatioStrategy(aspectStrategy)
.setResolutionStrategy(
ResolutionStrategy(
Size(1440, 1080),
ResolutionStrategy.FALLBACK_RULE_CLOSEST_HIGHER_THEN_LOWER
)
)
.build()

val captureSelector = ResolutionSelector.Builder()
.setAllowedResolutionMode(ResolutionSelector.PREFER_HIGHER_RESOLUTION_OVER_CAPTURE_RATE)
.setAspectRatioStrategy(aspectStrategy)
.setResolutionStrategy(ResolutionStrategy.HIGHEST_AVAILABLE_STRATEGY)
.build()

fun bindWhenViewportReady() {
val viewPort = previewView.viewPort
if (viewPort == null || previewView.width == 0 || previewView.height == 0) {
return
}

val rotation = previewView.display.rotation

val preview = Preview.Builder()
.setResolutionSelector(previewSelector)
.setTargetRotation(rotation)
.build()
.also { it.surfaceProvider = previewView.surfaceProvider }

val imageCapture = ImageCapture.Builder()
.setCaptureMode(ImageCapture.CAPTURE_MODE_MAXIMIZE_QUALITY)
.setJpegQuality(100)
.setResolutionSelector(captureSelector)
.setTargetRotation(rotation)
.build()

onImageCaptureReady(imageCapture)
cameraProvider.unbindAll()
val group = UseCaseGroup.Builder()
.setViewPort(viewPort)
.addUseCase(preview)
.addUseCase(imageCapture)
.build()
cameraProvider.bindToLifecycle(
lifecycleOwner,
getCameraSelector(),
group
)
}

val layoutListener = object : View.OnLayoutChangeListener {
override fun onLayoutChange(
v: View?,
left: Int,
top: Int,
right: Int,
bottom: Int,
oldLeft: Int,
oldTop: Int,
oldRight: Int,
oldBottom: Int
) {
if (previewView.width > 0 && previewView.height > 0 && previewView.viewPort != null) {
previewView.removeOnLayoutChangeListener(this)
bindWhenViewportReady()
}
}
}

fun scheduleBind() {
previewView.post {
if (previewView.width > 0 && previewView.height > 0 && previewView.viewPort != null) {
bindWhenViewportReady()
} else {
previewView.addOnLayoutChangeListener(layoutListener)
}
}
}

if (previewView.isAttachedToWindow) {
scheduleBind()
} else {
previewView.addOnAttachStateChangeListener(object : View.OnAttachStateChangeListener {
override fun onViewAttachedToWindow(v: View) {
previewView.removeOnAttachStateChangeListener(this)
scheduleBind()
}

override fun onViewDetachedFromWindow(v: View) {}
})
}
}, ContextCompat.getMainExecutor(context))
}


/**
* 高质量拍照 - 直接返回 Bitmap(内存方式,适合临时处理)
*/
fun takeHighQualityPhoto(
context: Context,
imageCapture: ImageCapture?,
rotation: Int,
onImageCaptured: (Bitmap) -> Unit,
onError: (String) -> Unit = {}
) {
val capture = imageCapture ?: run {
onError("ImageCapture 未初始化")
return
}

capture.targetRotation = rotation

capture.takePicture(
ContextCompat.getMainExecutor(context),
object : ImageCapture.OnImageCapturedCallback() {
override fun onError(exc: ImageCaptureException) {
Log.e("CameraX", "拍照失败: ${exc.message}", exc)
onError(exc.message ?: "拍照失败")
}

override fun onCaptureSuccess(image: androidx.camera.core.ImageProxy) {
try {
val bitmap = image.toBitmap()
onImageCaptured(bitmap)
Log.d("CameraX", "高质量拍照成功: ${bitmap.width}x${bitmap.height}")
} catch (e: Exception) {
Log.e("CameraX", "Bitmap转换失败", e)
onError("Bitmap转换失败")
} finally {
image.close()
}
}
}
)
}
}

预览

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
val context = LocalContext.current
val lifecycleOwner = androidx.lifecycle.compose.LocalLifecycleOwner.current
val previewView = remember { PreviewView(context) }
// 用于存储ImageCapture实例的变量
val imageCaptureInstance = remember { mutableStateOf<ImageCapture?>(null) }
// 创建相机执行器
val cameraExecutor = remember { Executors.newSingleThreadExecutor() }

// 初始化相机,获取高质量 ImageCapture 实例
LaunchedEffect(Unit) {
ZCameraUtils.bindCameraUseCases(previewView, context, lifecycleOwner)
{
imageCapture ->
imageCaptureInstance.value = imageCapture
}
}

// 释放相机资源
DisposableEffect(Unit) {
onDispose {
cameraExecutor.shutdown()
}
}


// 相机预览
Box(
modifier = Modifier
.fillMaxSize()
.padding(start = leftRightSpace, end = leftRightSpace)
.clip(RectangleShape)
) {
val cameraSelector = ZCameraUtils.getCameraSelector()
var modifier = Modifier
.fillMaxSize()
if (cameraSelector == CameraSelector.DEFAULT_BACK_CAMERA) {
modifier = modifier.graphicsLayer {
scaleX = -1f
scaleY = -1f
}
} else {
modifier = modifier.graphicsLayer {
scaleY = -1f
}
}
AndroidView(
factory = {
previewView.implementationMode = PreviewView.ImplementationMode.COMPATIBLE
previewView
},
modifier = modifier
)
}

拍照

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
val isTakingPhoto = remember { mutableStateOf(false) }

if (isTakingPhoto.value) {
return@clickableDebounced
}
isTakingPhoto.value = true

val imageCapture = imageCaptureInstance.value
if (imageCapture == null) {
Log.e(TAG, "ImageCapture 未初始化")
isTakingPhoto.value = false
return@clickableDebounced
}

// 使用高质量拍照
ZCameraUtils.takeHighQualityPhoto(
context = context,
imageCapture = imageCapture,
rotation = previewView.display.rotation,
onImageCaptured = {
bitmap ->
val cameraSelector = ZCameraUtils.getCameraSelector()
capturedImage = if (cameraSelector == CameraSelector.DEFAULT_BACK_CAMERA) {
bitmap.rotate180()
} else {
bitmap
}
},
onError = {
errorMsg ->
Log.e(TAG, "拍照失败: $errorMsg")
isTakingPhoto.value = false
}
)

预览与分析拍照

这样既可以预览,也可以使用用于分析的Bitmap作为拍照的图片。

工具类

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
import android.content.Context
import android.graphics.Bitmap
import android.util.Size
import androidx.camera.core.ImageAnalysis
import androidx.camera.lifecycle.ProcessCameraProvider
import androidx.camera.view.PreviewView
import androidx.core.content.ContextCompat
import androidx.lifecycle.LifecycleOwner

object ZCameraUtils {
// 绑定相机用例:预览 + 图像分析(获取帧)
fun bindCameraUseCases(
previewView: PreviewView?,
context: Context,
lifecycleOwner: LifecycleOwner,
onFrameCaptured: (Bitmap) -> Unit
) {
if (previewView == null) return
val cameraProviderFuture = ProcessCameraProvider.getInstance(context)
cameraProviderFuture.addListener({
val cameraProvider: ProcessCameraProvider = cameraProviderFuture.get()

val targetResolution = Size(1920, 1080)
// 1. 预览用例
val preview = androidx.camera.core.Preview.Builder()
.setTargetResolution(targetResolution)
.build()
.also { it.surfaceProvider = previewView.surfaceProvider }

// 2. 图像分析用例(实时获取帧)
val imageAnalysis = ImageAnalysis.Builder()
.setTargetResolution(targetResolution)
.setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST) // 只保留最新帧
.build()
.also { analysis ->
analysis.setAnalyzer(
ContextCompat.getMainExecutor(previewView.context)
) { image ->
// 将 ImageProxy 转换为 Bitmap(关键步骤)
val bitmap = image.toBitmap()
onFrameCaptured(bitmap)
image.close() // 必须关闭,否则阻塞后续帧
}
}

// 绑定用例
cameraProvider.unbindAll()
cameraProvider.bindToLifecycle(
lifecycleOwner,
androidx.camera.core.CameraSelector.DEFAULT_FRONT_CAMERA,
preview,
imageAnalysis
)
}, ContextCompat.getMainExecutor(context))
}
}

预览

1
2
3
4
5
6
7
8
9
10
11
12
13
val context = LocalContext.current
val lifecycleOwner = androidx.lifecycle.compose.LocalLifecycleOwner.current
val previewView = remember { PreviewView(context) }

var latestBitmap by remember { mutableStateOf<Bitmap?>(null) } // 存储获取的画面

// 初始化相机
LaunchedEffect(Unit) {
ZCameraUtils.bindCameraUseCases(previewView, context, lifecycleOwner) {
bitmap ->
latestBitmap = bitmap // 保存获取的帧画面
}
}

拍照

拍照的时候直接取latestBitmap的copy就行

1
2
3
4
5
var capturedImage by remember { mutableStateOf<Bitmap?>(null) }

latestBitmap?.let {
capturedImage = it.copy(it.config!!, it.isMutable)
}

注意

默认用于分析的图片比较模糊,通过设置setTargetResolution,可以调整其清晰度。