Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Select an option

  • Save aaaa-zhen/3e7118e0db5a73cad856295c8012c82f to your computer and use it in GitHub Desktop.

Select an option

Save aaaa-zhen/3e7118e0db5a73cad856295c8012c82f to your computer and use it in GitHub Desktop.
jetpack compose live camera
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