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("请求权限")
}
}
}
}

示例

工具类

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
package com.xhkjedu.camera_test.utils.camera

import android.annotation.SuppressLint
import android.content.ContentValues
import android.content.Context
import android.graphics.Bitmap
import android.graphics.ImageDecoder
import android.os.Build
import android.provider.MediaStore
import android.util.Log
import android.util.Size
import android.view.Display
import android.view.Surface
import androidx.camera.core.CameraInfo
import androidx.camera.core.CameraSelector
import androidx.camera.core.ImageAnalysis
import androidx.camera.core.ImageCapture
import androidx.camera.core.ImageCaptureException
import androidx.camera.core.Preview
import androidx.camera.lifecycle.ProcessCameraProvider
import androidx.camera.view.PreviewView
import androidx.compose.runtime.MutableState
import androidx.core.content.ContextCompat
import androidx.lifecycle.LifecycleOwner
import java.text.SimpleDateFormat
import java.util.Locale

object ZCameraUtils {
fun startCameraPreview(
previewView: PreviewView,
context: Context,
imageCaptureInstance: MutableState<ImageCapture?>,
lifecycleOwner: LifecycleOwner,
) {
val cameraProviderFuture = ProcessCameraProvider.getInstance(context)
cameraProviderFuture.addListener({
val cameraProvider: ProcessCameraProvider = cameraProviderFuture.get()

// 预览用例
val preview = Preview.Builder()
.build()
.also {
it.surfaceProvider = previewView.surfaceProvider
}


previewView.scaleType = PreviewView.ScaleType.FILL_CENTER
// 图像捕获用例
val imageCapture = ImageCapture.Builder()
.build()

imageCaptureInstance.value = imageCapture

// 选择后置摄像头
val cameraSelector = CameraSelector.DEFAULT_FRONT_CAMERA

try {
// 解绑所有先前绑定的用例
cameraProvider.unbindAll()

// 绑定相机和用例
cameraProvider.bindToLifecycle(
lifecycleOwner, cameraSelector, preview, imageCapture
)
} catch (exc: Exception) {
Log.e("CameraX", "使用相机时发生错误", exc)
}

}, ContextCompat.getMainExecutor(context))
}

fun takePhoto(
context: Context,
imageCapture: ImageCapture?,
onImageCaptured: (Bitmap) -> Unit
) {
// 获取ImageCapture实例
val imageCapture = imageCapture ?: return

imageCapture.targetRotation = Surface.ROTATION_180
// 创建带时间戳的文件名
val name = SimpleDateFormat("yyyy-MM-dd-HH-mm-ss-SSS", Locale.US)
.format(System.currentTimeMillis())

// 创建内容值
val contentValues = ContentValues().apply {
put(MediaStore.MediaColumns.DISPLAY_NAME, name)
put(MediaStore.MediaColumns.MIME_TYPE, "image/jpeg")
if (Build.VERSION.SDK_INT > Build.VERSION_CODES.P) {
put(MediaStore.Images.Media.RELATIVE_PATH, "Pictures/CameraX-Image")
}
}


// 创建输出选项对象
val outputOptions = ImageCapture.OutputFileOptions
.Builder(
context.contentResolver,
MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
contentValues
)
.build()

// 拍摄照片
imageCapture.takePicture(
outputOptions,
ContextCompat.getMainExecutor(context),
object : ImageCapture.OnImageSavedCallback {
override fun onError(exc: ImageCaptureException) {
Log.e("CameraX", "拍照失败: ${exc.message}", exc)
}

@SuppressLint("ObsoleteSdkInt")
@Suppress("DEPRECATION")
override fun onImageSaved(output: ImageCapture.OutputFileResults) {
val savedUri = output.savedUri ?: return

// 将保存的URI转换为Bitmap
val bitmap = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
val source = ImageDecoder.createSource(context.contentResolver, savedUri)
ImageDecoder.decodeBitmap(source)
} else {
MediaStore.Images.Media.getBitmap(context.contentResolver, savedUri)
}
onImageCaptured(bitmap)
Log.d("CameraX", "照片已保存到: $savedUri")
}
}
)
}
}

视图

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
import android.content.ContentValues
import android.content.Context
import android.graphics.Bitmap
import android.graphics.ImageDecoder
import android.os.Build
import android.provider.MediaStore
import android.util.Log
import androidx.camera.core.CameraSelector
import androidx.camera.core.ImageCapture
import androidx.camera.core.ImageCaptureException
import androidx.camera.core.Preview
import androidx.camera.lifecycle.ProcessCameraProvider
import androidx.camera.view.PreviewView
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.asImageBitmap
import androidx.compose.ui.graphics.painter.BitmapPainter
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.unit.dp
import androidx.compose.ui.viewinterop.AndroidView
import androidx.core.content.ContextCompat
import androidx.lifecycle.LifecycleOwner
import java.text.SimpleDateFormat
import java.util.Locale
import java.util.concurrent.ExecutorService
import java.util.concurrent.Executors

@Composable
fun CameraScreen() {
val context = LocalContext.current
val lifecycleOwner = androidx.lifecycle.compose.LocalLifecycleOwner.current
val previewView = remember { PreviewView(context) }
var capturedImage by remember { mutableStateOf<Bitmap?>(null) }
// 用于存储ImageCapture实例的变量
val imageCaptureInstance = remember { mutableStateOf<ImageCapture?>(null) }
// 创建相机执行器
val cameraExecutor = remember { Executors.newSingleThreadExecutor() }

// 初始化相机
LaunchedEffect(Unit) {
ZCameraUtils.startCamera(previewView, context, imageCaptureInstance, lifecycleOwner, cameraExecutor)
}
// 释放相机资源
DisposableEffect(Unit) {
onDispose {
cameraExecutor.shutdown()
}
}
Box(modifier = Modifier.fillMaxSize()) {
// 相机预览
AndroidView(
factory = {
previewView.implementationMode = PreviewView.ImplementationMode.COMPATIBLE
previewView
},
modifier = Modifier
.fillMaxSize()
.graphicsLayer{
scaleY = -1f
}
)

capturedImage?.let {
Image(
painter = BitmapPainter(
it.asImageBitmap()
),
modifier = Modifier
.fillMaxSize()
.background(Color(0xaa000000)),
contentDescription = "显示Bitmap图片"
)
}


// 拍照按钮和其他控制
Column(
modifier = Modifier
.align(Alignment.BottomCenter)
.fillMaxSize(),
verticalArrangement = Arrangement.Bottom,
horizontalAlignment = Alignment.CenterHorizontally
) {
IconButton(
onClick = {
Log.i("CameraX", "onClick: ")
ZCameraUtils.takePhoto(
context = context,
imageCapture = imageCaptureInstance.value,
onImageCaptured = {
bitmap ->
capturedImage = bitmap
}
)
},
modifier = Modifier
.size(72.dp)
.padding(24.dp)
) {
Icon(
painter = painterResource(id = android.R.drawable.ic_menu_camera),
contentDescription = "Take photo",
modifier = Modifier.size(72.dp)
)
}
}
}


}

使用

1
2
3
4
5
6
7
8
import androidx.compose.runtime.Composable
import androidx.navigation.NavHostController
import com.xhkjedu.zxs_android.componts.camera.CameraScreen

@Composable
fun ScreenCamera(navController: NavHostController) {
CameraScreen()
}

预览和图像分析

这样既可以预览,也可以使用用于分析的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,可以调整其清晰度。

预览翻转

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 相机预览
Box(
modifier = Modifier
.fillMaxSize()
.padding(start = leftRightSpace, end = leftRightSpace)
.clip(RectangleShape)
) {
AndroidView(
factory = {
previewView.implementationMode = PreviewView.ImplementationMode.COMPATIBLE
previewView
},
modifier = Modifier
.fillMaxSize()
.graphicsLayer{
scaleY = -1f
}
)
}

要注意以下两个地方

必须设置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
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
// 计算照片需要旋转的角度(使照片与 PreviewView 方向一致)
fun getImageRotation(
cameraInfo: CameraInfo,
display: Display
): Int {
// 1. 获取相机传感器相对于设备自然方向的旋转角度(0/90/180/270)
val sensorRotationDegrees = cameraInfo.sensorRotationDegrees

// 2. 获取当前屏幕的旋转角度(相对于设备自然方向)
val displayRotationDegrees = when (display.rotation) {
Surface.ROTATION_0 -> 0
Surface.ROTATION_90 -> 90
Surface.ROTATION_180 -> 180
Surface.ROTATION_270 -> 270
else -> 0
}

// 3. 计算最终旋转角度(不同相机方向需调整符号)
val result = if (cameraInfo.lensFacing == CameraSelector.LENS_FACING_FRONT) {
// 前置摄像头需要额外处理镜像翻转
(360 - (sensorRotationDegrees + displayRotationDegrees) % 360) % 360
} else {
// 后置摄像头
(sensorRotationDegrees - displayRotationDegrees + 360) % 360
}

return when (result) {
0 -> Surface.ROTATION_0
90 -> Surface.ROTATION_90
180 -> Surface.ROTATION_180
270 -> Surface.ROTATION_270
else -> Surface.ROTATION_0
}
}