I am trying to rotate the output image flip horizontally but I am able to rotate it only flip vertically. I am using camerax for capturing the image of the front camera. I am successfully getting the mirror image from the front camera but I want the same image as the user sees.
here is my code for capturing the image:-
#OptIn(ExperimentalCoilApi::class)
#ExperimentalPermissionsApi
#ExperimentalCoroutinesApi
#Composable
fun CameraCapture(
modifier: Modifier = Modifier,
cameraSelector: CameraSelector = CameraSelector.DEFAULT_FRONT_CAMERA,
onImageFile: (File) -> Unit = { }
) {
val context = LocalContext.current
Permission(
permission = Manifest.permission.CAMERA,
rationale = "You said you wanted a picture, so I'm going to have to ask for permission.",
permissionNotAvailableContent = {
Column(modifier) {
Text("O noes! No Camera!")
Spacer(modifier = Modifier.height(8.dp))
Button(
onClick = {
context.startActivity(
Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS).apply {
data = Uri.fromParts("package", context.packageName, null)
}
)
}
) {
Text("Open Settings")
}
}
}
) {
Box(modifier = modifier) {
val lifecycleOwner = LocalLifecycleOwner.current
val coroutineScope = rememberCoroutineScope()
var previewUseCase by remember { mutableStateOf<UseCase>(Preview.Builder().build()) }
val imageCaptureUseCase by remember {
mutableStateOf(
ImageCapture.Builder()
.setCaptureMode(CAPTURE_MODE_MAXIMIZE_QUALITY)
.setTargetRotation(ROTATION_180)
.build()
)
}
Box {
CameraPreview(
modifier = Modifier.fillMaxSize(),
onUseCase = {
previewUseCase = it
}
)
Image(
modifier = Modifier.fillMaxSize(),
painter = rememberImagePainter(R.drawable.human_face_foreground),
contentDescription = stringResource(id = R.string.user_image)
)
Button(
modifier = Modifier
.wrapContentSize()
.padding(16.dp)
.align(Alignment.BottomCenter),
onClick = {
coroutineScope.launch {
imageCaptureUseCase.takePicture(context.executor).let {
onImageFile(it)
}
}
}
) {
Text("Click!")
}
}
LaunchedEffect(previewUseCase) {
val cameraProvider = context.getCameraProvider()
try {
// Must unbind the use-cases before rebinding them.
cameraProvider.unbindAll()
cameraProvider.bindToLifecycle(
lifecycleOwner, cameraSelector, previewUseCase, imageCaptureUseCase
)
} catch (ex: Exception) {
Log.e("CameraCapture", "Failed to bind camera use cases", ex)
}
}
}
}
}
Someone please help me solving this issue.
I am a newbie in Jetpack Compose want to achieve the below. User can click/tap the preview box and it enable or disable camera preview accordingly.
Currently I am using the below code to display the preview.
AndroidView(
factory = { context ->
val previewView = PreviewView(context)
val preview = Preview.Builder().build()
val selector = CameraSelector.Builder()
.requireLensFacing(CameraSelector.LENS_FACING_BACK)
.build()
preview.setSurfaceProvider(previewView.surfaceProvider)
val imageAnalysis = ImageAnalysis.Builder()
.setTargetResolution(
Size(
previewView.width,
previewView.height
)
)
.setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST)
.build()
imageAnalysis.setAnalyzer(
ContextCompat.getMainExecutor(context),
)
try {
cameraProviderFuture.get().bindToLifecycle(
lifecycleOwner,
selector,
preview,
imageAnalysis
)
} catch (e: Exception) {
e.printStackTrace()
}
previewView
},
modifier = Modifier.weight(1f)
)
Thanks in advance. Please help.
I've got a bottomSheetScaffold, which contains a BottomSheet
That BottomSheet uses device's Camera, where I use CameraX alongside with Google's MLkit for bar scanning
Let's consider permission is accepted
What happens (Not correct): once I expand the bottomsheet upward, I show the CameraPreview, show camera preview, and ImageAnalyzer which analyzes the preview image.
Now the bottomSheet is expanded, the camera preview is visible and working as expected
then I collapse the bottomSheet, but the camera is still working (analyzer as well,
imageAnalysis.clearAnalyzer() clear the analyzing part)
The outcome: is not correct behavior I intended
so How can I stop camera from working, and using resources once the bottomSheetState is collapsed, and only allow camera when bottomSheetState is Expanded
How it works(Wrong):
The problem I got is, camera is binded to the lifecycle of the activity, and not the composable itself, when re-composition happens, it still consider the camera live, since it's not attached to the composition lifecycle
How does Composition work:
Code:
#OptIn(ExperimentalMaterialApi::class)
#Composable
fun BottomSheetContent(
modifier: Modifier = Modifier,
bottomSheetState: BottomSheetState
) {
Column(
modifier = modifier
.fillMaxWidth()
.fillMaxHeight(0.8f)
) {
PeekBar()
ScanningSerialTextTitle(modifier)
if (bottomSheetState.isExpanded) {
CameraBox(modifier)
} else {
EmptyBox()
}
}
}
#Composable
fun EmptyBox(modifier: Modifier = Modifier) {
Box(
modifier = modifier
.fillMaxSize()
.background(color = Color.DarkGray)
)
}
#OptIn(ExperimentalPermissionsApi::class)
#Composable
fun CameraBox(modifier: Modifier = Modifier) {
val cameraPermissionState = rememberPermissionState(permission = Manifest.permission.CAMERA)
val lifeCycleOwner = LocalLifecycleOwner.current
DisposableEffect(key1 = lifeCycleOwner, effect = {
val observer = LifecycleEventObserver { _, event ->
if (event == Lifecycle.Event.ON_START) {
cameraPermissionState.launchPermissionRequest()
}
}
lifeCycleOwner.lifecycle.addObserver(observer)
onDispose { lifeCycleOwner.lifecycle.removeObserver(observer) }
})
cameraPermissionState.handlePermissionCases(
ShouldShowRationaleContent = {
ShouldShowRationaleContent(cameraPermissionState = cameraPermissionState)
},
PermissionDeniedPermanentlyContent = {
PermissionDeniedPermanentContent()
}) {
val context = LocalContext.current
val barCodeVal = remember { mutableStateOf("") }
CameraPreview(onBarcodeDetected = { barcodes ->
barcodes.forEach { barcode ->
barcode.rawValue?.let { barcodeValue ->
barCodeVal.value = barcodeValue
Toast.makeText(context, barcodeValue, Toast.LENGTH_SHORT).show()
}
}
}, onBarcodeFailed = {}, onBarcodeNotFound = {})
}
}
#Composable
fun CameraPreview(
modifier: Modifier = Modifier,
onBarcodeDetected: (barcodes: List<Barcode>) -> Unit,
onBarcodeFailed: (exception: Exception) -> Unit,
onBarcodeNotFound: (text: String) -> Unit,
) {
val context = LocalContext.current
val lifecycleOwner = LocalLifecycleOwner.current
AndroidView(
modifier = modifier.fillMaxSize(),
factory = { androidViewContext -> initPreviewView(androidViewContext) },
update = { previewView: PreviewView ->
val cameraSelector: CameraSelector = buildCameraSelector(CameraSelector.LENS_FACING_BACK)
val cameraExecutor: ExecutorService = Executors.newSingleThreadExecutor()
val cameraProviderFuture: ListenableFuture<ProcessCameraProvider> =
ProcessCameraProvider.getInstance(context)
val preview = buildPreview().also {
it.setSurfaceProvider(previewView.surfaceProvider)
}
val barcodeAnalyser = BarCodeAnalyser(
onBarcodeDetected = onBarcodeDetected,
onBarcodeFailed = onBarcodeFailed,
onBarCodeNotFound = onBarcodeNotFound
)
val imageAnalysis: ImageAnalysis =
buildImageAnalysis(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST).also {
it.setAnalyzer(cameraExecutor, barcodeAnalyser)
}
cameraProviderFuture.addListener({
val cameraProvider: ProcessCameraProvider = cameraProviderFuture.get()
try {
cameraProvider.unbindAll() //Make sure we only use 1 usecase related to camera
val camera = cameraProvider.bindToLifecycle(
lifecycleOwner,
cameraSelector,
preview,
imageAnalysis
)
camera.cameraControl.enableTorch(true)
} catch (e: Exception) {
Log.d("TAG", "CameraPreview: ${e.localizedMessage}")
}
}, ContextCompat.getMainExecutor(context))
}
)
}
private fun initPreviewView(androidViewContext: Context): PreviewView {
val previewView = PreviewView(androidViewContext).apply {
implementationMode = PreviewView.ImplementationMode.COMPATIBLE
}
return previewView
}
private fun buildPreview(): Preview {
return Preview.Builder().build()
}
private fun buildImageAnalysis(imageAnalysisStrategy: Int): ImageAnalysis {
return ImageAnalysis.Builder()
.setBackpressureStrategy(imageAnalysisStrategy)
.build()
}
private fun buildCameraSelector(cameraLens: Int): CameraSelector {
return CameraSelector.Builder()
.requireLensFacing(cameraLens)
.build()
}
What I tried:
I tried passing down the state of BottomSheetState to the composable, and checking for state, which should triggers re-composition, but since I'm using Android's Camera as View, this doesn't solve the problem
First on CameraPreview Composable function in your code, define a variable of type ProcessCameraProvider, and assign it to null value
var cameraProvider: ProcessCameraProvider? = null
Then you will define a DisposableEffect, with key of cameraProvider and when it de-compose, you'll close the camera
DisposableEffect(key1 = cameraProvider) {
onDispose {
cameraProvider?.let { it.unbindAll() } // closes the camera
}
}
Replace your old line of code
val cameraProvider: ProcessCameraProvider = cameraProviderFuture.get()
with our new cameraProvider
cameraProvider = cameraProviderFuture.get()
Then in your try-catch block, since we're using a null value, when need to check if it's null or not, so we'll use let
try {
cameraProvider?.let {
it.unbindAll() //Make sure we only use 1 usecase related to camera
val camera = it.bindToLifecycle(
lifecycleOwner, cameraSelector, preview, imageAnalysis
)
camera.cameraControl.enableTorch(true) // TODO: Debug mode only
}
} catch (e: Exception) {
Log.d("TAG", "CameraPreview: ${e.localizedMessage}")
}
Complete Code:
#Composable
fun CameraPreview(
modifier: Modifier = Modifier,
onBarcodeDetected: (barcodes: List<Barcode>) -> Unit,
onBarcodeFailed: (exception: Exception) -> Unit,
onBarcodeNotFound: (text: String) -> Unit,
) {
val context = LocalContext.current
val lifecycleOwner = LocalLifecycleOwner.current
var cameraProvider: ProcessCameraProvider? = null
DisposableEffect(key1 = cameraProvider) {
onDispose {
cameraProvider?.let { it.unbindAll() }
}
}
AndroidView(
modifier = modifier.fillMaxSize(),
factory = { androidViewContext -> initPreviewView(androidViewContext) },
update = { previewView: PreviewView ->
val cameraSelector: CameraSelector =
buildCameraSelector(CameraSelector.LENS_FACING_BACK)
val cameraExecutor: ExecutorService = Executors.newSingleThreadExecutor()
val cameraProviderFuture: ListenableFuture<ProcessCameraProvider> =
ProcessCameraProvider.getInstance(context)
cameraProviderFuture.addListener({
cameraProvider = cameraProviderFuture.get()
val preview = buildPreview().also {
it.setSurfaceProvider(previewView.surfaceProvider)
}
val barcodeAnalyser = BarCodeAnalyser(
onBarcodeDetected = onBarcodeDetected,
onBarcodeFailed = onBarcodeFailed,
onBarCodeNotFound = onBarcodeNotFound
)
val imageAnalysis: ImageAnalysis =
buildImageAnalysis(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST).also {
it.setAnalyzer(cameraExecutor, barcodeAnalyser)
}
try {
cameraProvider?.let {
it.unbindAll() //Make sure we only use 1 usecase related to camera
val camera = it.bindToLifecycle(
lifecycleOwner, cameraSelector, preview, imageAnalysis
)
camera.cameraControl.enableTorch(true) // TODO: Debug mode only
}
} catch (e: Exception) {
Log.d("TAG", "CameraPreview: ${e.localizedMessage}")
}
}, ContextCompat.getMainExecutor(context))
}
)
}
I found a solution, where I used DisposableEffect to shut the camera when composable is removed from composition
First on CameraPreview Composable function in your code, define a variable of type ProcessCameraProvider, and assign it to null value
var cameraProvider: ProcessCameraProvider? = null
Then you will define a DisposableEffect, with key of cameraProvider and when the composable de-compose, you'll close the camera
DisposableEffect(key1 = cameraProvider) {
onDispose {
cameraProvider?.let { it.unbindAll() } // closes the camera
}
}
Replace your old line of code
val cameraProvider: ProcessCameraProvider = cameraProviderFuture.get()
with our new cameraProvider
cameraProvider = cameraProviderFuture.get()
Then in your try-catch block, since we're using a null value, when need to check if it's null or not, so we'll use let
try {
cameraProvider?.let {
it.unbindAll() //Make sure we only use 1 usecase related to camera
val camera = it.bindToLifecycle(
lifecycleOwner, cameraSelector, preview, imageAnalysis
)
camera.cameraControl.enableTorch(true) // TODO: Debug mode only
}
} catch (e: Exception) {
Log.d("TAG", "CameraPreview: ${e.localizedMessage}")
}
Complete Code:
#Composable
fun CameraPreview(
modifier: Modifier = Modifier,
onBarcodeDetected: (barcodes: List<Barcode>) -> Unit,
onBarcodeFailed: (exception: Exception) -> Unit,
onBarcodeNotFound: (text: String) -> Unit,
) {
val context = LocalContext.current
val lifecycleOwner = LocalLifecycleOwner.current
var cameraProvider: ProcessCameraProvider? = null
DisposableEffect(key1 = cameraProvider) {
onDispose {
cameraProvider?.let { it.unbindAll() }
}
}
AndroidView(
modifier = modifier.fillMaxSize(),
factory = { androidViewContext -> initPreviewView(androidViewContext) },
update = { previewView: PreviewView ->
val cameraSelector: CameraSelector =
buildCameraSelector(CameraSelector.LENS_FACING_BACK)
val cameraExecutor: ExecutorService = Executors.newSingleThreadExecutor()
val cameraProviderFuture: ListenableFuture<ProcessCameraProvider> =
ProcessCameraProvider.getInstance(context)
cameraProviderFuture.addListener({
cameraProvider = cameraProviderFuture.get()
val preview = buildPreview().also {
it.setSurfaceProvider(previewView.surfaceProvider)
}
val barcodeAnalyser = BarCodeAnalyser(
onBarcodeDetected = onBarcodeDetected,
onBarcodeFailed = onBarcodeFailed,
onBarCodeNotFound = onBarcodeNotFound
)
val imageAnalysis: ImageAnalysis =
buildImageAnalysis(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST).also {
it.setAnalyzer(cameraExecutor, barcodeAnalyser)
}
try {
cameraProvider?.let {
it.unbindAll() //Make sure we only use 1 usecase related to camera
val camera = it.bindToLifecycle(
lifecycleOwner, cameraSelector, preview, imageAnalysis
)
camera.cameraControl.enableTorch(true) // TODO: Debug mode only
}
} catch (e: Exception) {
Log.d("TAG", "CameraPreview: ${e.localizedMessage}")
}
}, ContextCompat.getMainExecutor(context))
}
)
}
Trying to migrate barcode scanner to Jetpack compose and updating camera and ML Kit dependencies to latest version.
The current shows the camera view correctly, but it is not scanning the barcodes.
The ImageAnalysis analyzer runs only once.
Code
#Composable
fun CameraPreview(
data: CameraPreviewData,
) {
val context = LocalContext.current
val lifecycleOwner = LocalLifecycleOwner.current
AndroidView(
modifier = Modifier
.fillMaxSize(),
factory = { AndroidViewContext ->
PreviewView(AndroidViewContext).apply {
this.scaleType = PreviewView.ScaleType.FILL_CENTER
layoutParams = ViewGroup.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.MATCH_PARENT
)
// Preview is incorrectly scaled in Compose on some devices without this
implementationMode = PreviewView.ImplementationMode.COMPATIBLE
}
},
update = { previewView ->
val cameraSelector: CameraSelector = CameraSelector.Builder()
.requireLensFacing(CameraSelector.LENS_FACING_BACK)
.build()
val cameraExecutor: ExecutorService = Executors.newSingleThreadExecutor()
val cameraProviderFuture: ListenableFuture<ProcessCameraProvider> =
ProcessCameraProvider.getInstance(context)
cameraProviderFuture.addListener({
val preview: Preview = Preview.Builder()
.build()
.also {
// Attach the viewfinder's surface provider to preview use case
it.setSurfaceProvider(previewView.surfaceProvider)
}
val cameraProvider: ProcessCameraProvider = cameraProviderFuture.get()
val barcodeAnalyser = BarcodeAnalyser { barcodes ->
barcodes.forEach { barcode ->
barcode.rawValue?.let { barcodeValue ->
logError("Barcode value detected: ${barcodeValue}.")
// Other handling code
}
}
}
val imageAnalysis: ImageAnalysis = ImageAnalysis.Builder()
.setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST)
.build()
.also {
it.setAnalyzer(cameraExecutor, barcodeAnalyser)
}
try {
cameraProvider.unbindAll()
cameraProvider.bindToLifecycle(
lifecycleOwner,
cameraSelector,
preview,
imageAnalysis
)
} catch (exception: Exception) {
logError("Use case binding failed with exception : $exception")
}
}, ContextCompat.getMainExecutor(context))
},
)
}
BarcodeAnalyser
class BarcodeAnalyser(
private val onBarcodesDetected: (barcodes: List<Barcode>) -> Unit,
) : ImageAnalysis.Analyzer {
private var lastAnalyzedTimestamp = 0L
override fun analyze(
imageProxy: ImageProxy,
) {
logError("Inside analyze")
val currentTimestamp = System.currentTimeMillis()
if (currentTimestamp - lastAnalyzedTimestamp >= TimeUnit.SECONDS.toMillis(1)) {
imageProxy.image?.let { imageToAnalyze ->
val options = BarcodeScannerOptions.Builder()
.setBarcodeFormats(Barcode.FORMAT_ALL_FORMATS)
.build()
val barcodeScanner = BarcodeScanning.getClient(options)
val imageToProcess =
InputImage.fromMediaImage(imageToAnalyze, imageProxy.imageInfo.rotationDegrees)
barcodeScanner.process(imageToProcess)
.addOnSuccessListener { barcodes ->
if (barcodes.isNotEmpty()) {
logError("Scanned: $barcodes")
onBarcodesDetected(barcodes)
imageProxy.close()
} else {
logError("No barcode scanned")
}
}
.addOnFailureListener { exception ->
logError("BarcodeAnalyser: Something went wrong with exception: $exception")
imageProxy.close()
}
}
lastAnalyzedTimestamp = currentTimestamp
}
}
}
References
https://stackoverflow.com/a/66763853/9636037
Thanks to Adrian's comment.
It worked after the following changes.
In BarcodeAnalyser
Removed imageProxy.close() from addOnSuccessListener and addOnFailureListener. Added it to addOnCompleteListener.
Added imageProxy.close() in else condition as well.
class BarcodeAnalyser(
private val onBarcodesDetected: (barcodes: List<Barcode>) -> Unit,
) : ImageAnalysis.Analyzer {
private var lastAnalyzedTimestamp = 0L
override fun analyze(
imageProxy: ImageProxy,
) {
logError("Inside analyze")
val currentTimestamp = System.currentTimeMillis()
if (currentTimestamp - lastAnalyzedTimestamp >= TimeUnit.SECONDS.toMillis(1)) {
imageProxy.image?.let { imageToAnalyze ->
// ...Same code
barcodeScanner.process(imageToProcess)
.addOnSuccessListener { barcodes ->
if (barcodes.isNotEmpty()) {
logError("Scanned: $barcodes")
onBarcodesDetected(barcodes)
// imageProxy.close()
} else {
logError("No barcode scanned")
}
}
.addOnFailureListener { exception ->
logError("BarcodeAnalyser: Something went wrong with exception: $exception")
// imageProxy.close()
}
.addOnCompleteListener {
imageProxy.close()
}
}
lastAnalyzedTimestamp = currentTimestamp
} else {
imageProxy.close()
}
}
}
I create the CameraPreview in layout resource file, and i want to show this file in AndroidView by clicking the navigation icon button .
// navigation icon button
Icon(
vectorResource(id = R.drawable.ic_photo),
modifier = Modifier.clickable(onClick = /* show CameraPreview */)
)
// compose CameraPreview
#Composable
fun CameraPreview() {
val lifecycleOwner = LifecycleOwnerAmbient.current
val context = ContextAmbient.current
val cameraProviderFuture = remember { ProcessCameraProvider.getInstance(context) }
AndroidView(viewBlock = { ctx ->
/* get the CameraPreview */
})
}
// xml file
<androidx.camera.view.PreviewView
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="#+id/previewView"
android:layout_width="match_parent"
android:layout_height="match_parent" />
Create an instance of PreviewView in your composable itself:
#Composable
fun CameraPreview() {
val lifecycleOwner = LifecycleOwnerAmbient.current
val context = ContextAmbient.current
val cameraProviderFuture = remember { ProcessCameraProvider.getInstance(context) }
val previewView = remember { PreviewView(context) }
AndroidView(viewBlock = { previewView }) {
// Initialize your camera here
}
}