Created
May 17, 2025 05:16
-
-
Save aaaa-zhen/3e7118e0db5a73cad856295c8012c82f to your computer and use it in GitHub Desktop.
jetpack compose live camera
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| package com.yourpackage // 替换成你的包名 | |
| import android.Manifest | |
| import android.content.Context | |
| import android.graphics.Bitmap | |
| import android.graphics.BitmapFactory | |
| import android.graphics.Matrix | |
| import android.util.Log | |
| import android.view.ViewGroup | |
| import androidx.camera.core.* | |
| 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.* | |
| import androidx.compose.material.icons.Icons | |
| import androidx.compose.material.icons.filled.Clear | |
| import androidx.compose.material.icons.filled.Settings | |
| import androidx.compose.material3.* | |
| import androidx.compose.runtime.* | |
| 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.layout.ContentScale | |
| import androidx.compose.ui.platform.LocalContext | |
| import androidx.compose.ui.platform.LocalLifecycleOwner | |
| import androidx.compose.ui.tooling.preview.Preview | |
| import androidx.compose.ui.unit.dp | |
| import androidx.compose.ui.viewinterop.AndroidView | |
| import androidx.core.content.ContextCompat | |
| import com.google.accompanist.permissions.ExperimentalPermissionsApi | |
| import com.google.accompanist.permissions.isGranted | |
| import com.google.accompanist.permissions.rememberPermissionState | |
| import com.google.accompanist.permissions.shouldShowRationale | |
| import com.google.common.util.concurrent.ListenableFuture | |
| import java.io.File | |
| import java.nio.ByteBuffer | |
| import java.text.SimpleDateFormat | |
| import java.util.* | |
| import java.util.concurrent.ExecutorService | |
| import java.util.concurrent.Executors | |
| // 文件名,用于保存照片 (可选,如果想保存到文件) | |
| private const val FILENAME_FORMAT = "yyyy-MM-dd-HH-mm-ss-SSS" | |
| @OptIn(ExperimentalPermissionsApi::class) | |
| @Composable | |
| fun CameraWithCaptureScreen() { // 重命名以区分 | |
| val cameraPermissionState = rememberPermissionState(permission = Manifest.permission.CAMERA) | |
| var showRationale by remember { mutableStateOf(false) } | |
| var capturedImageBitmap by remember { mutableStateOf<Bitmap?>(null) } | |
| val context = LocalContext.current | |
| // ImageCapture 用例的实例 | |
| val imageCapture = remember { ImageCapture.Builder().build() } | |
| Column(modifier = Modifier.fillMaxSize()) { | |
| Box( | |
| modifier = Modifier | |
| .weight(1f) // 让相机/图片预览区域占据大部分空间 | |
| .fillMaxWidth() | |
| .background(Color.Black), // 背景色,以防图片或预览有透明区域 | |
| contentAlignment = Alignment.Center | |
| ) { | |
| if (cameraPermissionState.status.isGranted) { | |
| if (capturedImageBitmap == null) { | |
| // 显示实时相机预览 | |
| CameraPreview( | |
| modifier = Modifier.fillMaxSize(), // 填满 Box | |
| imageCapture = imageCapture // 将 ImageCapture 实例传递下去 | |
| ) | |
| } else { | |
| // 显示捕获的图片 | |
| Image( | |
| bitmap = capturedImageBitmap!!.asImageBitmap(), | |
| contentDescription = "Captured Image", | |
| modifier = Modifier.fillMaxSize(), | |
| contentScale = ContentScale.Fit // 或者 Crop, FillBounds 等 | |
| ) | |
| } | |
| } else { | |
| // 权限请求UI | |
| Column( | |
| modifier = Modifier.fillMaxSize(), | |
| horizontalAlignment = Alignment.CenterHorizontally, | |
| verticalArrangement = Arrangement.Center | |
| ) { | |
| val text = if (cameraPermissionState.status.shouldShowRationale || showRationale) { | |
| "相机权限对于此功能是必需的。请在设置中授予权限。" | |
| } else { | |
| "需要相机权限才能显示预览和拍照。" | |
| } | |
| Text(text, color = Color.White) // 在黑色背景上用白色文字 | |
| Spacer(modifier = Modifier.height(8.dp)) | |
| Button(onClick = { | |
| if (cameraPermissionState.status.shouldShowRationale) { | |
| // 可以显示更详细的解释 | |
| } | |
| cameraPermissionState.launchPermissionRequest() | |
| }) { | |
| Text("请求权限") | |
| } | |
| } | |
| } | |
| } | |
| // 控制按钮区域 | |
| if (cameraPermissionState.status.isGranted) { | |
| Row( | |
| modifier = Modifier | |
| .fillMaxWidth() | |
| .padding(16.dp), | |
| horizontalArrangement = Arrangement.SpaceAround, | |
| verticalAlignment = Alignment.CenterVertically | |
| ) { | |
| if (capturedImageBitmap == null) { | |
| // “拍照”按钮 | |
| Button(onClick = { | |
| takePhoto( | |
| context = context, | |
| imageCapture = imageCapture, | |
| onImageCaptured = { bitmap -> capturedImageBitmap = bitmap }, | |
| onError = { Log.e("CameraWithCaptureScreen", "Image capture error: ${it.message}", it) } | |
| ) | |
| }) { | |
| Icon(Icons.Filled.Settings, contentDescription = "Take Picture") | |
| Spacer(Modifier.size(ButtonDefaults.IconSpacing)) | |
| Text("拍照") | |
| } | |
| } else { | |
| // “清除”按钮 | |
| Button(onClick = { capturedImageBitmap = null }) { | |
| Icon(Icons.Filled.Clear, contentDescription = "Clear Image") | |
| Spacer(Modifier.size(ButtonDefaults.IconSpacing)) | |
| Text("清除") | |
| } | |
| } | |
| } | |
| } | |
| } | |
| LaunchedEffect(cameraPermissionState.status) { | |
| if (!cameraPermissionState.status.isGranted && !cameraPermissionState.status.shouldShowRationale) { | |
| Log.d("CameraScreen", "Initial permission request or permission denied and 'don't ask again'.") | |
| } | |
| if (!cameraPermissionState.status.isGranted && cameraPermissionState.status.shouldShowRationale) { | |
| showRationale = true | |
| } | |
| } | |
| } | |
| // 修改 CameraPreview 以接收和绑定 ImageCapture | |
| @Composable | |
| fun CameraPreview( | |
| modifier: Modifier = Modifier, | |
| imageCapture: ImageCapture? = null // 可选的 ImageCapture 实例 | |
| ) { | |
| val context = LocalContext.current | |
| val lifecycleOwner = LocalLifecycleOwner.current | |
| val cameraProviderFuture: ListenableFuture<ProcessCameraProvider> = remember { | |
| ProcessCameraProvider.getInstance(context) | |
| } | |
| val previewView = remember { | |
| PreviewView(context).apply { | |
| layoutParams = ViewGroup.LayoutParams( | |
| ViewGroup.LayoutParams.MATCH_PARENT, | |
| ViewGroup.LayoutParams.MATCH_PARENT | |
| ) | |
| scaleType = PreviewView.ScaleType.FILL_CENTER | |
| } | |
| } | |
| AndroidView( | |
| factory = { previewView }, | |
| modifier = modifier, | |
| update = { view -> | |
| cameraProviderFuture.addListener({ | |
| val cameraProvider = cameraProviderFuture.get() | |
| val previewUseCase = androidx.camera.core.Preview.Builder() | |
| .build() | |
| .also { | |
| it.setSurfaceProvider(view.surfaceProvider) | |
| } | |
| val cameraSelector = CameraSelector.DEFAULT_BACK_CAMERA | |
| try { | |
| cameraProvider.unbindAll() | |
| if (imageCapture != null) { | |
| // 如果提供了 ImageCapture 实例,则一起绑定 | |
| cameraProvider.bindToLifecycle( | |
| lifecycleOwner, cameraSelector, previewUseCase, imageCapture | |
| ) | |
| } else { | |
| cameraProvider.bindToLifecycle( | |
| lifecycleOwner, cameraSelector, previewUseCase | |
| ) | |
| } | |
| } catch (exc: Exception) { | |
| Log.e("CameraPreview", "Use case binding failed", exc) | |
| } | |
| }, ContextCompat.getMainExecutor(context)) | |
| } | |
| ) | |
| } | |
| // 拍照的辅助函数 | |
| private fun takePhoto( | |
| context: Context, | |
| imageCapture: ImageCapture, | |
| onImageCaptured: (Bitmap) -> Unit, | |
| onError: (ImageCaptureException) -> Unit | |
| ) { | |
| // 创建一个用于拍照的 Executor | |
| val cameraExecutor: ExecutorService = Executors.newSingleThreadExecutor() | |
| // 这里我们直接在内存中处理图片,不保存到文件。 | |
| // 如果需要保存到文件,可以使用 ImageCapture.OutputFileOptions | |
| imageCapture.takePicture( | |
| cameraExecutor, // 或者 ContextCompat.getMainExecutor(context) 如果回调不需要耗时操作 | |
| object : ImageCapture.OnImageCapturedCallback() { | |
| override fun onCaptureSuccess(image: ImageProxy) { | |
| Log.d("CameraUtil", "Photo capture succeeded: ${image.width}x${image.height}") | |
| val bitmap = imageProxyToBitmap(image) | |
| // CameraX 返回的图像可能需要旋转 | |
| val rotatedBitmap = bitmap?.let { rotateBitmapIfNeeded(image, it) } | |
| // 回调在 cameraExecutor 线程,如果需要更新 UI,确保切换到主线程 | |
| // 但由于我们只是调用 onImageCaptured,它内部会触发 recomposition,Compose 会处理线程切换 | |
| rotatedBitmap?.let { onImageCaptured(it) } | |
| image.close() // 非常重要:关闭 ImageProxy 以释放资源 | |
| } | |
| override fun onError(exception: ImageCaptureException) { | |
| Log.e("CameraUtil", "Photo capture failed: ${exception.message}", exception) | |
| onError(exception) | |
| } | |
| } | |
| ) | |
| } | |
| // 将 ImageProxy 转换为 Bitmap | |
| private fun imageProxyToBitmap(image: ImageProxy): Bitmap? { | |
| val planeProxy = image.planes[0] | |
| val buffer: ByteBuffer = planeProxy.buffer | |
| val bytes = ByteArray(buffer.remaining()) | |
| buffer.get(bytes) | |
| return BitmapFactory.decodeByteArray(bytes, 0, bytes.size) | |
| } | |
| // 修正图片方向 (CameraX 图片通常是根据传感器方向,可能需要旋转以正确显示) | |
| private fun rotateBitmapIfNeeded(imageProxy: ImageProxy, bitmap: Bitmap): Bitmap { | |
| val rotationDegrees = imageProxy.imageInfo.rotationDegrees | |
| if (rotationDegrees == 0) return bitmap | |
| val matrix = Matrix() | |
| matrix.postRotate(rotationDegrees.toFloat()) | |
| return Bitmap.createBitmap(bitmap, 0, 0, bitmap.width, bitmap.height, matrix, true) | |
| } | |
| // 预览 Composable | |
| @Preview(showBackground = true, name = "Camera With Capture Screen") | |
| @Composable | |
| fun OriginalCameraComposableWithCapturePreview() { | |
| // 确保你的主题包裹了预览,否则按钮等可能样式不正确 | |
| // YourAppTheme { // 假设你有一个主题 | |
| CameraWithCaptureScreen() | |
| // } | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment