@@ -13,6 +13,7 @@ import androidx.camera.core.resolutionselector.ResolutionStrategy
1313import androidx.camera.lifecycle.ProcessCameraProvider
1414import androidx.camera.view.PreviewView
1515import androidx.camera.view.TransformExperimental
16+ import androidx.compose.foundation.layout.add
1617import androidx.compose.runtime.Composable
1718import androidx.compose.runtime.DisposableEffect
1819import androidx.compose.runtime.getValue
@@ -31,6 +32,8 @@ import kotlinx.coroutines.runBlocking
3132import kotlinx.coroutines.withContext
3233import org.multipaz.util.Logger
3334import java.util.concurrent.Executors
35+ import kotlin.collections.isNotEmpty
36+ import kotlin.collections.toTypedArray
3437
3538private const val TAG = " Camera"
3639
@@ -47,7 +50,7 @@ actual fun Camera(
4750 val lifecycleOwner = LocalLifecycleOwner .current
4851 val cameraProviderFuture = remember { ProcessCameraProvider .getInstance(context) }
4952 val executor = remember { Executors .newSingleThreadExecutor() }
50- val previewView = remember { mutableStateOf<PreviewView ?>(if (showCameraPreview) PreviewView (context) else null ) }
53+ var activePreviewView by remember { mutableStateOf<PreviewView ?>(null ) }
5154 var currentDisplayRotation by remember { mutableIntStateOf(Surface .ROTATION_0 ) }
5255
5356 DisposableEffect (Unit ) { // Keyed by Unit to run once and clean up with the composable
@@ -73,81 +76,113 @@ actual fun Camera(
7376 }
7477 }
7578
76- DisposableEffect (cameraSelection) {
77- var preview: Preview ? = null
78- if (showCameraPreview) {
79- preview = Preview .Builder ().build()
80- // COMPATIBLE is needed b/c we're using `outputTransform.matrix`
81- previewView.value!! .implementationMode = PreviewView .ImplementationMode .COMPATIBLE
82- preview.setSurfaceProvider(previewView.value!! .surfaceProvider)
83- }
84-
79+ DisposableEffect (cameraSelection, showCameraPreview, activePreviewView, currentDisplayRotation) {
8580 val cameraProvider = cameraProviderFuture.get()
8681
87- val resolutionStrategy = ResolutionStrategy (
88- captureResolution.getDimensions(),
89- ResolutionStrategy .FALLBACK_RULE_CLOSEST_LOWER
90- )
91- val resolutionSelector = ResolutionSelector .Builder ()
92- .setResolutionStrategy(resolutionStrategy)
93- .build()
94-
95- val imageAnalysis = ImageAnalysis .Builder ()
96- .setResolutionSelector(resolutionSelector)
97- .setBackpressureStrategy(ImageAnalysis .STRATEGY_KEEP_ONLY_LATEST )
98- .setOutputImageFormat(ImageAnalysis .OUTPUT_IMAGE_FORMAT_YUV_420_888 )
99- .build()
100-
101- imageAnalysis.setAnalyzer(executor) { imageProxy ->
102- runBlocking {
103- val transformationProxy = if (showCameraPreview) {
104- withContext(Dispatchers .Main ) {
105- getCorrectionMatrix(imageProxy, previewView.value!! )
82+ if (! cameraProviderFuture.isDone) {
83+ onDispose { /* * Not bound. */ }
84+ } else {
85+ cameraProvider.unbindAll()
86+
87+ val resolutionStrategy = ResolutionStrategy (
88+ captureResolution.getDimensions(),
89+ ResolutionStrategy .FALLBACK_RULE_CLOSEST_LOWER
90+ )
91+ val resolutionSelector = ResolutionSelector .Builder ()
92+ .setResolutionStrategy(resolutionStrategy)
93+ .build()
94+
95+ val imageAnalysis = ImageAnalysis .Builder ()
96+ .setResolutionSelector(resolutionSelector)
97+ .setBackpressureStrategy(ImageAnalysis .STRATEGY_KEEP_ONLY_LATEST )
98+ .setOutputImageFormat(ImageAnalysis .OUTPUT_IMAGE_FORMAT_YUV_420_888 )
99+ .setTargetRotation(currentDisplayRotation)
100+ .build()
101+
102+ imageAnalysis.setAnalyzer(executor) { imageProxy ->
103+ runBlocking {
104+ val transformationProxy = if (showCameraPreview && activePreviewView != null ) {
105+ withContext(Dispatchers .Main ) {
106+ if (activePreviewView?.surfaceProvider != null && activePreviewView?.outputTransform != null ) {
107+ getCorrectionMatrix(imageProxy, activePreviewView!! )
108+ } else {
109+ Matrix () // Fallback if preview is not ready.
110+ }
111+ }
112+ } else {
113+ Matrix ()
106114 }
107- } else {
108- Matrix ()
115+ val frame = CameraFrame (
116+ cameraImage = CameraImage (imageProxy),
117+ width = imageProxy.width,
118+ height = imageProxy.height,
119+ rotation = calculateDetectorAngle(currentDisplayRotation, cameraSelection),
120+ previewTransformation = transformationProxy
121+ )
122+ onFrameCaptured(frame)
109123 }
110- val frame = CameraFrame (
111- cameraImage = CameraImage (imageProxy),
112- width = imageProxy.width,
113- height = imageProxy.height,
114- rotation = calculateDetectorAngle(currentDisplayRotation, cameraSelection),
115- previewTransformation = transformationProxy
116- )
117- onFrameCaptured(frame)
124+ imageProxy.close()
118125 }
119- imageProxy.close()
120- }
121126
122- cameraProviderFuture.get().unbindAll()
123- if (showCameraPreview) {
124- cameraProviderFuture.get().bindToLifecycle(
125- lifecycleOwner,
126- cameraSelection.toAndroidCameraSelector(),
127- preview,
128- imageAnalysis
129- )
130- } else {
131- cameraProviderFuture.get().bindToLifecycle(
132- lifecycleOwner,
133- cameraSelection.toAndroidCameraSelector(),
134- imageAnalysis
135- )
136- }
127+ try {
128+ if (showCameraPreview && activePreviewView != null ) {
129+ val preview = Preview .Builder ()
130+ .build()
131+ .also {
132+ activePreviewView!! .implementationMode = PreviewView .ImplementationMode .COMPATIBLE
133+ it.setSurfaceProvider(activePreviewView!! .surfaceProvider)
134+ }
135+ cameraProvider.bindToLifecycle(
136+ lifecycleOwner,
137+ cameraSelection.toAndroidCameraSelector(),
138+ preview,
139+ imageAnalysis
140+ )
141+ } else {
142+ // Image Analysis only if preview is not shown.
143+ cameraProvider.bindToLifecycle(
144+ lifecycleOwner,
145+ cameraSelection.toAndroidCameraSelector(),
146+ imageAnalysis
147+ )
148+ }
149+ } catch (exc: Exception ) {
150+ Logger .e(TAG , " Use case binding failed" , exc)
151+ }
137152
138- onDispose {
139- cameraProvider.unbindAll()
140- executor.shutdown()
153+ onDispose {
154+ cameraProvider.unbindAll()
155+ }
141156 }
142157 }
143158
144159 if (showCameraPreview) {
145160 AndroidView (
146161 modifier = modifier,
147- factory = { context ->
148- previewView.value!!
162+ factory = { ctx ->
163+ PreviewView (ctx).apply {
164+ implementationMode = PreviewView .ImplementationMode .COMPATIBLE
165+ activePreviewView = this
166+ }
167+ },
168+ onRelease = {
169+ activePreviewView = null
149170 }
150171 )
172+ } else {
173+ if (activePreviewView != null ) {
174+ // This ensures the DisposableEffect re-runs if the view was previously shown.
175+ activePreviewView = null
176+ }
177+ }
178+
179+ DisposableEffect (Unit ) {
180+ onDispose {
181+ Logger .d(TAG , " Shutting down camera executor." )
182+ if (! executor.isShutdown) {
183+ executor.shutdown()
184+ }
185+ }
151186 }
152187}
153188
@@ -177,7 +212,7 @@ private fun CameraSelection.toAndroidCameraSelector() =
177212 }
178213
179214@OptIn(TransformExperimental ::class )
180- private fun getCorrectionMatrix (imageProxy : ImageProxy , previewView : PreviewView ) : Matrix {
215+ private fun getCorrectionMatrix (imageProxy : ImageProxy , previewView : PreviewView ): Matrix {
181216
182217 // This matrix maps (-1, -1) -> (1, 1) space to preview-coordinate system. This includes
183218 // any scaling, rotation, or cropping that's done in the preview.
@@ -188,8 +223,8 @@ private fun getCorrectionMatrix(imageProxy: ImageProxy, previewView: PreviewView
188223 // By the scale and translate below, we modify the matrix so it maps
189224 // (0, 0) -> (width, height) of the frame to analyze to the preview
190225 // coordinate system
191- composeMatrix.scale(2f / imageProxy.width, 2f / imageProxy.height, 1f )
192- composeMatrix.translate(- 0.5f * imageProxy.width, - 0.5f * imageProxy.height, 1.0f )
226+ composeMatrix.scale(2f / imageProxy.width, 2f / imageProxy.height, 1f )
227+ composeMatrix.translate(- 0.5f * imageProxy.width, - 0.5f * imageProxy.height, 1.0f )
193228
194229 return composeMatrix
195230}
0 commit comments