@@ -7,13 +7,14 @@ import android.content.pm.PackageManager
77import android.widget.Toast
88import androidx.activity.compose.rememberLauncherForActivityResult
99import androidx.activity.result.contract.ActivityResultContracts
10+ import androidx.camera.compose.CameraXViewfinder
1011import androidx.camera.core.CameraSelector
11- import androidx.camera.core.CameraSelector.LENS_FACING_BACK
12- import androidx.camera.core.CameraSelector.LENS_FACING_FRONT
12+ import androidx.camera.core.CameraSelector.DEFAULT_BACK_CAMERA
13+ import androidx.camera.core.CameraSelector.DEFAULT_FRONT_CAMERA
1314import androidx.camera.core.Preview
15+ import androidx.camera.core.SurfaceRequest
1416import androidx.camera.lifecycle.ProcessCameraProvider
1517import androidx.camera.lifecycle.awaitInstance
16- import androidx.camera.view.PreviewView
1718import androidx.compose.foundation.border
1819import androidx.compose.foundation.clickable
1920import androidx.compose.foundation.layout.*
@@ -27,20 +28,27 @@ import androidx.compose.runtime.*
2728import androidx.compose.ui.Alignment
2829import androidx.compose.ui.Modifier
2930import androidx.compose.ui.draw.clip
30- import androidx.compose.ui.draw.clipToBounds
3131import androidx.compose.ui.graphics.Color
3232import androidx.compose.ui.platform.LocalContext
3333import androidx.compose.ui.unit.dp
34- import androidx.compose.ui.viewinterop.AndroidView
3534import androidx.core.app.ActivityCompat
3635import androidx.core.content.ContextCompat
36+ import androidx.lifecycle.LifecycleOwner
37+ import androidx.lifecycle.ViewModel
3738import androidx.lifecycle.compose.LocalLifecycleOwner
39+ import androidx.lifecycle.compose.collectAsStateWithLifecycle
3840import androidx.lifecycle.compose.currentStateAsState
41+ import androidx.lifecycle.viewmodel.compose.viewModel
3942import com.composegears.tiamat.compose.navDestination
4043import composegears.tiamat.sample.ui.AppButton
4144import composegears.tiamat.sample.ui.Screen
45+ import kotlinx.coroutines.awaitCancellation
46+ import kotlinx.coroutines.flow.MutableStateFlow
47+ import kotlinx.coroutines.flow.asStateFlow
48+ import kotlinx.coroutines.flow.update
4249
43- val AndroidViewLifecycleScreen by navDestination {
50+ val CameraXLifecycleScreen by navDestination {
51+ val viewModel = viewModel<CameraPreviewViewModel >()
4452 val context = LocalContext .current
4553
4654 var isPermissionGranted by remember { mutableStateOf(false ) }
@@ -56,7 +64,7 @@ val AndroidViewLifecycleScreen by navDestination {
5664 else -> requestPermissionLauncher.launch(Manifest .permission.CAMERA )
5765 }
5866 }
59- Screen (" AndroidView + Lifecycle handle " ) {
67+ Screen (" CameraX + Lifecycle" ) {
6068 if (isPermissionGranted) {
6169 Column (
6270 modifier = Modifier .fillMaxSize(),
@@ -67,11 +75,11 @@ val AndroidViewLifecycleScreen by navDestination {
6775 modifier = Modifier .fillMaxSize(0.8f ),
6876 contentAlignment = Alignment .Center
6977 ) {
70- CameraView ()
78+ CameraView (viewModel )
7179 }
7280
7381 val lf = LocalLifecycleOwner .current
74- Text (" Lifecycle State: ${lf.lifecycle.currentStateAsState()} " )
82+ Text (" Lifecycle State: ${lf.lifecycle.currentStateAsState().value } " )
7583 }
7684 } else {
7785 PermissionDeclined {
@@ -94,72 +102,54 @@ private fun PermissionDeclined(onRequest: () -> Unit) {
94102}
95103
96104@Composable
97- private fun CameraView () {
105+ private fun CameraView (viewModel : CameraPreviewViewModel ) {
98106 val context = LocalContext .current
99107 val lifecycleOwner = LocalLifecycleOwner .current
100108
101- var lensFacing by remember { mutableIntStateOf(LENS_FACING_BACK ) }
109+ var cameraSelector by remember { mutableStateOf(DEFAULT_BACK_CAMERA ) }
110+ val surfaceRequest by viewModel.surfaceRequest.collectAsStateWithLifecycle()
102111
103- val preview = remember { Preview .Builder ().build() }
104- val previewView = remember { PreviewView (context) }
105- val cameraSelector = remember(lensFacing) {
106- CameraSelector .Builder ()
107- .requireLensFacing(lensFacing)
108- .build()
109- }
110- LaunchedEffect (lensFacing) {
111- val cameraProvider = ProcessCameraProvider .awaitInstance(context)
112- cameraProvider.unbindAll()
113- cameraProvider.bindToLifecycle(
114- lifecycleOwner,
115- cameraSelector,
116- preview,
112+ LaunchedEffect (lifecycleOwner, cameraSelector) {
113+ viewModel.bindToCamera(
114+ appContext = context.applicationContext,
115+ lifecycleOwner = lifecycleOwner,
116+ cameraSelector = cameraSelector
117117 )
118- preview.surfaceProvider = previewView.surfaceProvider
119118 }
119+ surfaceRequest?.let {
120+ Box (modifier = Modifier .fillMaxSize()) {
121+ CameraXViewfinder (surfaceRequest = it)
120122
121- Box (
122- modifier = Modifier
123- .fillMaxSize()
124- .clipToBounds()
125- ) {
126- AndroidView (
127- modifier = Modifier
128- .fillMaxSize()
129- .align(Alignment .Center ),
130- factory = { previewView }
131- )
132- Icon (
133- modifier = Modifier
134- .align(Alignment .BottomCenter )
135- .padding(bottom = 24 .dp)
136- .navigationBarsPadding()
137- .size(64 .dp)
138- .padding(1 .dp)
139- .border(1 .dp, Color .White , CircleShape )
140- .clip(CircleShape )
141- .clickable {
142- Toast .makeText(context, " Take photo" , Toast .LENGTH_SHORT ).show()
143- },
144- imageVector = Icons .Sharp .Lens ,
145- contentDescription = null
146- )
147- Icon (
148- modifier = Modifier
149- .align(Alignment .BottomEnd )
150- .navigationBarsPadding()
151- .padding(bottom = 36 .dp, end = 24 .dp)
152- .size(40 .dp)
153- .clip(CircleShape )
154- .clickable {
155- lensFacing = when (lensFacing) {
156- LENS_FACING_BACK -> LENS_FACING_FRONT
157- else -> LENS_FACING_BACK
158- }
159- },
160- imageVector = Icons .Sharp .FlipCameraAndroid ,
161- contentDescription = null
162- )
123+ Icon (
124+ modifier = Modifier
125+ .align(Alignment .BottomCenter )
126+ .padding(bottom = 24 .dp)
127+ .size(64 .dp)
128+ .padding(1 .dp)
129+ .border(1 .dp, Color .White , CircleShape )
130+ .clip(CircleShape )
131+ .clickable {
132+ Toast .makeText(context, " Take photo" , Toast .LENGTH_SHORT ).show()
133+ },
134+ imageVector = Icons .Sharp .Lens ,
135+ contentDescription = null
136+ )
137+ Icon (
138+ modifier = Modifier
139+ .align(Alignment .BottomEnd )
140+ .padding(bottom = 36 .dp, end = 24 .dp)
141+ .size(40 .dp)
142+ .clip(CircleShape )
143+ .clickable {
144+ cameraSelector = when (cameraSelector) {
145+ DEFAULT_BACK_CAMERA -> DEFAULT_FRONT_CAMERA
146+ else -> DEFAULT_BACK_CAMERA
147+ }
148+ },
149+ imageVector = Icons .Sharp .FlipCameraAndroid ,
150+ contentDescription = null
151+ )
152+ }
163153 }
164154}
165155
@@ -173,4 +163,35 @@ private fun Context.shouldShowRationale(permission: String) =
173163 ActivityCompat .shouldShowRequestPermissionRationale(
174164 this as Activity ,
175165 permission
176- )
166+ )
167+
168+ internal class CameraPreviewViewModel : ViewModel () {
169+
170+ private val _surfaceRequest = MutableStateFlow <SurfaceRequest ?>(null )
171+ val surfaceRequest = _surfaceRequest .asStateFlow()
172+
173+ private val cameraPreviewUseCase = Preview .Builder ().build().apply {
174+ setSurfaceProvider { newSurfaceRequest ->
175+ _surfaceRequest .update { newSurfaceRequest }
176+ }
177+ }
178+
179+ suspend fun bindToCamera (
180+ appContext : Context ,
181+ lifecycleOwner : LifecycleOwner ,
182+ cameraSelector : CameraSelector
183+ ) {
184+ val processCameraProvider = ProcessCameraProvider .awaitInstance(appContext)
185+ processCameraProvider.bindToLifecycle(
186+ lifecycleOwner = lifecycleOwner,
187+ cameraSelector = cameraSelector,
188+ cameraPreviewUseCase
189+ )
190+
191+ try {
192+ awaitCancellation()
193+ } finally {
194+ processCameraProvider.unbindAll()
195+ }
196+ }
197+ }
0 commit comments