1- package com.getcode.view.main.home.components
1+ package com.getcode.view.main.camera
22
33import android.content.Context
4+ import android.view.GestureDetector
5+ import android.view.MotionEvent
6+ import android.view.ScaleGestureDetector
7+ import androidx.camera.core.Camera
8+ import androidx.camera.core.CameraControl
9+ import androidx.camera.core.CameraInfo
410import androidx.camera.core.CameraSelector
11+ import androidx.camera.core.FocusMeteringAction
512import androidx.camera.core.ImageAnalysis
613import androidx.camera.core.Preview
714import androidx.camera.lifecycle.ProcessCameraProvider
@@ -15,14 +22,14 @@ import androidx.compose.foundation.background
1522import androidx.compose.foundation.layout.Box
1623import androidx.compose.foundation.layout.fillMaxSize
1724import androidx.compose.runtime.Composable
18- import androidx.compose.runtime.DisposableEffect
1925import androidx.compose.runtime.LaunchedEffect
2026import androidx.compose.runtime.getValue
2127import androidx.compose.runtime.mutableStateOf
2228import androidx.compose.runtime.remember
2329import androidx.compose.runtime.rememberCoroutineScope
2430import androidx.compose.runtime.setValue
2531import androidx.compose.ui.Modifier
32+ import androidx.compose.ui.geometry.Offset
2633import androidx.compose.ui.platform.LocalContext
2734import androidx.compose.ui.platform.LocalLifecycleOwner
2835import androidx.compose.ui.viewinterop.AndroidView
@@ -45,10 +52,13 @@ import kotlinx.coroutines.launch
4552import kotlinx.coroutines.withContext
4653import timber.log.Timber
4754import java.util.concurrent.Executors
55+ import java.util.concurrent.TimeUnit
4856
4957@Composable
5058fun CodeScanner (
5159 scanningEnabled : Boolean ,
60+ cameraAFEnabled : Boolean ,
61+ cameraPinchZoomEnabled : Boolean ,
5262 onPreviewStateChanged : (Boolean ) -> Unit ,
5363 onCodeScanned : (ScannableKikCode ) -> Unit
5464) {
@@ -68,7 +78,9 @@ fun CodeScanner(
6878
6979 val cameraSelector = remember {
7080 val lensFacing = CameraSelector .LENS_FACING_BACK
71- CameraSelector .Builder ().requireLensFacing(lensFacing).build()
81+ CameraSelector .Builder ()
82+ .requireLensFacing(lensFacing)
83+ .build()
7284 }
7385
7486 val imageAnalysis = remember {
@@ -84,20 +96,23 @@ fun CodeScanner(
8496 KikCodeAnalyzer (scanner, onCodeScanned)
8597 }
8698
87- var bound by remember {
88- mutableStateOf(false )
89- }
90-
9199 val biometricsState = LocalBiometricsState .current
92100
101+ var camera by remember { mutableStateOf<Camera ?>(null ) }
102+ var autoFocusPoint by remember { mutableStateOf(Offset .Unspecified ) }
103+
93104 val scope = rememberCoroutineScope()
94105 LaunchedEffect (scanner, biometricsState.isAwaitingAuthentication, Biometrics .promptActive) {
95106 val active = Biometrics .promptActive || biometricsState.isAwaitingAuthentication
96107 val cameraProvider = context.getCameraProvider()
97108 if (! active) {
98109 cameraProvider.unbindAll()
99- cameraProvider.bindToLifecycle(lifecycleOwner, cameraSelector, preview, imageAnalysis)
100- bound = true
110+ camera = cameraProvider.bindToLifecycle(
111+ lifecycleOwner,
112+ cameraSelector,
113+ preview,
114+ imageAnalysis
115+ )
101116 } else {
102117 cameraProvider.unbindAll()
103118 }
@@ -108,26 +123,41 @@ fun CodeScanner(
108123 scope.launch {
109124 val cameraProvider = context.getCameraProvider()
110125 cameraProvider.unbindAll()
111- bound = false
126+ camera = null
112127 }
113128 } else if (event == Lifecycle .Event .ON_RESUME ) {
114129 scope.launch {
115- if (! bound ) {
130+ if (camera == null ) {
116131 if (! biometricsState.isAwaitingAuthentication) {
117132 val cameraProvider = context.getCameraProvider()
118133 cameraProvider.unbindAll()
119- cameraProvider.bindToLifecycle(
134+ camera = cameraProvider.bindToLifecycle(
120135 lifecycleOwner,
121136 cameraSelector,
122137 preview,
123138 imageAnalysis
124139 )
125- bound = true
126140 }
127141 }
128142 }
129143 }
144+ }
145+
146+ LaunchedEffect (camera, cameraAFEnabled, cameraPinchZoomEnabled) {
147+ camera?.let {
148+ val cameraControl = it.cameraControl
149+ val cameraInfo = it.cameraInfo
130150
151+ setupInteractionControls(
152+ previewView,
153+ cameraControl,
154+ cameraInfo,
155+ cameraAFEnabled,
156+ cameraPinchZoomEnabled
157+ ) { point ->
158+ autoFocusPoint = point
159+ }
160+ }
131161 }
132162
133163 var streamState by remember(previewView) {
@@ -161,6 +191,10 @@ fun CodeScanner(
161191
162192 AndroidView (factory = { previewView }, modifier = Modifier .fillMaxSize())
163193
194+ FocusIndicator (autoFocusPoint) {
195+ autoFocusPoint = Offset .Unspecified
196+ }
197+
164198 AnimatedVisibility (
165199 modifier = Modifier .fillMaxSize(),
166200 visible = streamState != PreviewView .StreamState .STREAMING ,
@@ -179,4 +213,66 @@ private suspend fun Context.getCameraProvider(): ProcessCameraProvider {
179213 return withContext(Dispatchers .IO ) {
180214 ProcessCameraProvider .getInstance(this @getCameraProvider).get()
181215 }
216+ }
217+
218+ private fun setupInteractionControls (
219+ previewView : PreviewView ,
220+ cameraControl : CameraControl ,
221+ cameraInfo : CameraInfo ,
222+ autoFocusEnabled : Boolean ,
223+ pinchZoomEnabled : Boolean ,
224+ onTap : (Offset ) -> Unit ,
225+ ) {
226+ val scaleGestureDetector = ScaleGestureDetector (
227+ previewView.context,
228+ object : ScaleGestureDetector .SimpleOnScaleGestureListener () {
229+ override fun onScale (detector : ScaleGestureDetector ): Boolean {
230+ val currentZoomRatio = cameraInfo.zoomState.value?.zoomRatio ? : 1f
231+ val delta = detector.scaleFactor
232+ val newZoomRatio = currentZoomRatio * delta
233+
234+ // Clamp the new zoom ratio between the minimum and maximum zoom ratio
235+ val clampedZoomRatio = newZoomRatio.coerceIn(
236+ cameraInfo.zoomState.value?.minZoomRatio ? : 1f ,
237+ cameraInfo.zoomState.value?.maxZoomRatio ? : currentZoomRatio
238+ )
239+
240+ // Apply the zoom to the camera control
241+ cameraControl.setZoomRatio(clampedZoomRatio)
242+ return true
243+ }
244+ })
245+
246+ // Create a gesture detector to detect tap gestures
247+ val gestureDetector =
248+ GestureDetector (previewView.context, object : GestureDetector .SimpleOnGestureListener () {
249+ override fun onSingleTapUp (event : MotionEvent ): Boolean {
250+ // Get the tap location
251+ val point = previewView.meteringPointFactory.createPoint(event.x, event.y)
252+ onTap(Offset (event.x, event.y))
253+
254+ // Prepare focus action
255+ val action = FocusMeteringAction .Builder (point, FocusMeteringAction .FLAG_AF )
256+ .setAutoCancelDuration(
257+ 5 ,
258+ TimeUnit .SECONDS
259+ ) // Optional: Auto-cancel after 5 seconds
260+ .build()
261+
262+ // Trigger focus and metering at the tapped location
263+ cameraControl.startFocusAndMetering(action)
264+
265+ return true
266+ }
267+ })
268+
269+ previewView.setOnTouchListener { _, event ->
270+ if (pinchZoomEnabled) {
271+ scaleGestureDetector.onTouchEvent(event)
272+ }
273+ if (autoFocusEnabled) {
274+ gestureDetector.onTouchEvent(event)
275+ }
276+ true
277+ }
182278}
0 commit comments