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