Home > Back-end >  CameraX not working with navigation library in jetpack compose
CameraX not working with navigation library in jetpack compose

Time:09-24

I'm facing issues with navigation-compose (2.4.0-alpha09) and camerax library (1.1.0-alpha08) in composable screen. If I only use camera view (wrapped in AndroidView), everything works fine and image capture is working. but if I use navController in composable to navigate, image capture is not working and app crash.

@ExperimentalCoilApi
@Composable
fun CameraScreen(navController: NavController) {
    val context = LocalContext.current
    val lifecycleOwner = LocalLifecycleOwner.current

    Scaffold {
        CameraPreviewScreen(
            modifier = Modifier.fillMaxSize(),
//                navController = navController,
            context = context,
            lifecycleOwner = lifecycleOwner,
            outPutDirectory = getFilesDirectory(context = context),
            onMediaCaptured = { uri ->
//                    navController.navigate(
//                        Screens.AddPostDetails.withArgs(
//                            URLEncoder.encode(
//                                uri.toString(),
//                                StandardCharsets.UTF_8.toString()
//                            )
//                        )
//                    )
            }
        )
    }
}

@ExperimentalCoilApi
@Composable
fun CameraPreviewScreen(
    modifier: Modifier = Modifier,
//    navController: NavController,
    context: Context,
    lifecycleOwner: LifecycleOwner,
    outPutDirectory: File,
    onMediaCaptured: (Uri?) -> Unit
) {
    val cameraProviderFuture = remember { ProcessCameraProvider.getInstance(context) }
    val cameraProvider = cameraProviderFuture.get()
    val executor = ContextCompat.getMainExecutor(context)
    var imageCapture: ImageCapture? = null
    var cameraSelector: CameraSelector?
    var camera: Camera? = null
    val previewView = PreviewView(context)
    val preview = Preview.Builder().build().also {
        it.setSurfaceProvider(previewView.surfaceProvider)
    }

    var lensFacing by remember { mutableStateOf(CameraSelector.LENS_FACING_BACK) }
    var flashEnabled by remember { mutableStateOf(false) }
    var flashRes by remember { mutableStateOf(R.drawable.ic_outlined_flash_on) }

    Box {
        AndroidView(
            factory = { ctx ->
                cameraProviderFuture.addListener(
                    {
                        cameraSelector = CameraSelector.Builder()
                            .requireLensFacing(lensFacing)
                            .build()
                        imageCapture = ImageCapture.Builder()
                            .setTargetRotation(previewView.display.rotation)
                            .build()

                        cameraProvider.unbindAll()
                        camera = cameraProvider.bindToLifecycle(
                            lifecycleOwner,
                            cameraSelector as CameraSelector,
                            imageCapture,
                            preview
                        )
                    }, executor
                )
                previewView
            },
            modifier = modifier
        )

        Row(
            verticalAlignment = Alignment.CenterVertically,
            modifier = Modifier
                .fillMaxWidth()
                .padding(16.dp)
                .align(Alignment.TopStart)
        ) {
            IconButton(onClick = {
                Log.d(TAG, "back clicked")
//                navController.popBackStack()
            }) {
                Icon(
                    modifier = Modifier.iconSize(),
                    imageVector = Icons.Filled.ArrowBack,
                    contentDescription = null,
                    tint = MaterialTheme.colors.surface
                )
            }
        }

        Row(
            horizontalArrangement = Arrangement.SpaceBetween,
            verticalAlignment = Alignment.CenterVertically,
            modifier = Modifier
                .fillMaxWidth()
                .padding(16.dp)
                .clip(RoundedCornerShape(16.dp))
                .background(background, RoundedCornerShape(16.dp))
                .padding(8.dp)
                .align(Alignment.BottomCenter)
        ) {
            IconButton(
                onClick = {
                    camera?.let {
                        if (it.cameraInfo.hasFlashUnit()) {
                            flashEnabled = !flashEnabled
                            flashRes =
                                if (flashEnabled) R.drawable.ic_outlined_flash_off else R.drawable.ic_outlined_flash_on
                            it.cameraControl.enableTorch(flashEnabled)
                        }
                    }
                }) {
                Icon(
                    modifier = Modifier.size(34.dp),
                    painter = painterResource(id = flashRes),
                    contentDescription = null,
                    tint = MaterialTheme.colors.surface
                )
            }

            Button(
                modifier = Modifier
                    .size(70.dp)
                    .background(captureIconColor, CircleShape)
                    .shadow(4.dp, CircleShape)
                    .clip(CircleShape)
                    .border(5.dp, Color.LightGray, CircleShape),
                colors = ButtonDefaults.buttonColors(backgroundColor = captureIconColor),
                onClick = {
                    val imgCapture = imageCapture ?: return@Button
                    val photoFile = File(
                        outPutDirectory,
                        SimpleDateFormat("yyyyMMdd-HHmmss-SSS", Locale.US)
                            .format(System.currentTimeMillis())   ".jpg"
                    )
                    val outputOptions = ImageCapture.OutputFileOptions.Builder(photoFile).build()
                    imgCapture.takePicture(
                        outputOptions,
                        executor,
                        object : ImageCapture.OnImageSavedCallback {
                            override fun onImageSaved(outputFileResults: ImageCapture.OutputFileResults) {
                                Log.d(TAG, "image saved: ${photoFile.path}")
                                onMediaCaptured(Uri.fromFile(photoFile))
                            }

                            override fun onError(exception: ImageCaptureException) {
                                Toast.makeText(
                                    context, "Something went wrong", Toast.LENGTH_SHORT
                                ).show()
                            }
                        }
                    )
                }
            ) {

            }

            IconButton(
                onClick = {
                    lensFacing =
                        if (lensFacing == CameraSelector.LENS_FACING_BACK) CameraSelector.LENS_FACING_FRONT else CameraSelector.LENS_FACING_BACK

                    cameraSelector = CameraSelector.Builder()
                        .requireLensFacing(lensFacing)
                        .build()
                    cameraProvider.unbindAll()
                    cameraProvider.bindToLifecycle(
                        lifecycleOwner,
                        cameraSelector as CameraSelector,
                        imageCapture,
                        preview
                    )
                }) {
                Icon(
                    modifier = Modifier.size(34.dp),
                    painter = painterResource(id = R.drawable.ic_outlined_rotate),
                    contentDescription = null,
                    tint = MaterialTheme.colors.surface
                )
            }
        }
    }
}

CodePudding user response:

Make sure you have camera permission. You can do it manually in Settings -> apps -> YOUR APP -> Permissions -> turn on camera permission.

If you don't see camera in this list, that means you haven't added needed permission in your AndroidMainfest.xml, check out this codelab.

To ask user to enable permission from your app, use Accompanist Permissions.


Now to your code. All composable functions are views builder, which means that they can be called many times during app lifetime. You're creating many of your variables without remember, which means that they'll loose it state on the first recomposition.

AndroidView factory is called when the view is created, and you should only create android views inside that block.

In your code you're creating it outside, which means that after the first recomposition preview will have an invalid surfaceProvider.

I suggest you start with state in compose documentation, including this youtube video which explains the basic principles.

Your code should be updated to something like this:

val cameraProviderFuture = remember(context) { ProcessCameraProvider.getInstance(context) }
val cameraProvider = remember(cameraProviderFuture) { cameraProviderFuture.get() }
val executor = remember(context) { ContextCompat.getMainExecutor(context) }
var imageCapture: ImageCapture? by remember { mutableStateOf(null) }
var cameraSelector: CameraSelector? by remember { mutableStateOf(null) }
var camera: Camera? by remember { mutableStateOf(null) }
var preview by remember { mutableStateOf<Preview?>(null) }

var lensFacing by remember { mutableStateOf(CameraSelector.LENS_FACING_BACK) }
var flashEnabled by remember { mutableStateOf(false) }
var flashRes by remember { mutableStateOf(R.drawable.ic_redo) }

Box {
    AndroidView(
        factory = { ctx ->
            val previewView = PreviewView(ctx)
            cameraProviderFuture.addListener(
                {
                    cameraSelector = CameraSelector.Builder()
                        .requireLensFacing(lensFacing)
                        .build()
                    imageCapture = ImageCapture.Builder()
                        .setTargetRotation(previewView.display.rotation)
                        .build()

                    cameraProvider.unbindAll()
                    camera = cameraProvider.bindToLifecycle(
                        lifecycleOwner,
                        cameraSelector as CameraSelector,
                        imageCapture,
                        preview
                    )
                }, executor
            )
            preview = Preview.Builder().build().also {
                it.setSurfaceProvider(previewView.surfaceProvider)
            }
            previewView
        },
        modifier = modifier
    )
    ...
}
  • Related