Home > Back-end >  How to stop camera from working when it no longer visible in the compisition?
How to stop camera from working when it no longer visible in the compisition?

Time:11-29

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):

Wrong Behavior(Check Log)

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:

Recompositions

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

CodePudding user response:

First You will create a var for camera state

var showCamera by remember {mutableStateOf(false)}

Then you will detect when the Bottom Sheet is dragged up and down

val modalBottomSheetState = rememberModalBottomSheetState(initialValue = ModalBottomSheetValue.Hidden)
val scope = rememberCoroutineScope()
LaunchedEffect(modalBottomSheetState.currentValue) {
    println(modalBottomSheetState.currentValue)
}
if (modalBottomSheetState.currentValue != ModalBottomSheetValue.Hidden) {
    DisposableEffect(Unit) {
        onDispose {

//now the bottom sheet is hidden showcamera=false

        }
    }
}

Finally you will check if showcamrea = true then request this camera or view like that

if(showcamera){
AndroidView(
        modifier = modifier.fillMaxSize(),
        factory = { androidViewContext -> initPreviewView(androidViewContext) },
        update = { previewView: PreviewView ->........
last of android view camera
}

I hope it helps you!

CodePudding user response:

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))
        }
    )


}
  • Related