Skip to content

Commit c490b44

Browse files
committed
Migrate to CameraXViewfinder
1 parent 437fda7 commit c490b44

File tree

6 files changed

+100
-87
lines changed

6 files changed

+100
-87
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -408,7 +408,7 @@ Nothing specific (yet)
408408
409409
`Tiamat` overrides `LocalLifecycleOwner` for each destination (android only) and compatible with lifecycle-aware components
410410
411-
See an example of camera usage: [AndroidViewLifecycleScreen.kt](sample/composeApp/src/androidMain/kotlin/composegears/tiamat/sample/platform/AndroidViewLifecycleScreen.kt)
411+
See an example of CameraX usage: [CameraXLifecycleScreen.kt](sample/composeApp/src/androidMain/kotlin/composegears/tiamat/sample/platform/CameraXLifecycleScreen.kt)
412412
413413
### iOS
414414

gradle/libs.versions.toml

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,16 +5,18 @@ sample-compileSdk = "36"
55
sample-minSdk = "23"
66
sample-targetSdk = "36"
77

8+
camerax = "1.5.0"
89
detekt = "1.23.8"
910
jetbrains-compose = "1.8.2"
1011
lifecycle = "2.9.3"
1112
kotlin = "2.2.20"
1213

1314
[libraries]
1415
androidx-activity-compose = "androidx.activity:activity-compose:1.11.0"
15-
androidx-camera-camera2 = "androidx.camera:camera-camera2:1.4.2"
16-
androidx-camera-lifecycle = "androidx.camera:camera-lifecycle:1.4.2"
17-
androidx-camera-view = "androidx.camera:camera-view:1.4.2"
16+
17+
androidx-camera-camera2 = { module = "androidx.camera:camera-camera2", version.ref = "camerax" }
18+
androidx-camera-compose = { module = "androidx.camera:camera-compose", version.ref = "camerax" }
19+
androidx-camera-lifecycle = { module = "androidx.camera:camera-lifecycle", version.ref = "camerax" }
1820

1921
compose-material3-window-size = { module = "org.jetbrains.compose.material3:material3-window-size-class", version.ref = "jetbrains-compose" }
2022
compose-ui-backhandler = { module = "org.jetbrains.compose.ui:ui-backhandler", version.ref = "jetbrains-compose" }

renovate.json5

Lines changed: 1 addition & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -5,15 +5,5 @@
55
],
66
"extends": [
77
"config:recommended"
8-
],
9-
"packageRules": [
10-
{
11-
"groupName": "CameraX",
12-
"matchPackageNames": [
13-
"androidx.camera:camera-view",
14-
"androidx.camera:camera-lifecycle",
15-
"androidx.camera:camera-camera2"
16-
]
17-
}
18-
]
8+
]
199
}

sample/composeApp/build.gradle.kts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,8 +64,8 @@ kotlin {
6464
implementation(libs.androidx.activity.compose)
6565

6666
implementation(libs.androidx.camera.camera2)
67+
implementation(libs.androidx.camera.compose)
6768
implementation(libs.androidx.camera.lifecycle)
68-
implementation(libs.androidx.camera.view)
6969
}
7070
jvmMain.dependencies {
7171
implementation(compose.desktop.currentOs)

sample/composeApp/src/androidMain/kotlin/composegears/tiamat/sample/platform/AndroidViewLifecycleScreen.kt renamed to sample/composeApp/src/androidMain/kotlin/composegears/tiamat/sample/platform/CameraXLifecycleScreen.kt

Lines changed: 90 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,14 @@ import android.content.pm.PackageManager
77
import android.widget.Toast
88
import androidx.activity.compose.rememberLauncherForActivityResult
99
import androidx.activity.result.contract.ActivityResultContracts
10+
import androidx.camera.compose.CameraXViewfinder
1011
import 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
1314
import androidx.camera.core.Preview
15+
import androidx.camera.core.SurfaceRequest
1416
import androidx.camera.lifecycle.ProcessCameraProvider
1517
import androidx.camera.lifecycle.awaitInstance
16-
import androidx.camera.view.PreviewView
1718
import androidx.compose.foundation.border
1819
import androidx.compose.foundation.clickable
1920
import androidx.compose.foundation.layout.*
@@ -27,20 +28,27 @@ import androidx.compose.runtime.*
2728
import androidx.compose.ui.Alignment
2829
import androidx.compose.ui.Modifier
2930
import androidx.compose.ui.draw.clip
30-
import androidx.compose.ui.draw.clipToBounds
3131
import androidx.compose.ui.graphics.Color
3232
import androidx.compose.ui.platform.LocalContext
3333
import androidx.compose.ui.unit.dp
34-
import androidx.compose.ui.viewinterop.AndroidView
3534
import androidx.core.app.ActivityCompat
3635
import androidx.core.content.ContextCompat
36+
import androidx.lifecycle.LifecycleOwner
37+
import androidx.lifecycle.ViewModel
3738
import androidx.lifecycle.compose.LocalLifecycleOwner
39+
import androidx.lifecycle.compose.collectAsStateWithLifecycle
3840
import androidx.lifecycle.compose.currentStateAsState
41+
import androidx.lifecycle.viewmodel.compose.viewModel
3942
import com.composegears.tiamat.compose.navDestination
4043
import composegears.tiamat.sample.ui.AppButton
4144
import 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+
}

sample/composeApp/src/androidMain/kotlin/composegears/tiamat/sample/platform/Platform.android.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,8 @@ actual fun Platform.name(): String = "Android"
88
actual fun Platform.platformFeatures(): List<AppFeature> = listOf(
99
AppFeature(
1010
name = "CameraX",
11-
description = "AndroidView + lifecycle handling",
12-
destination = AndroidViewLifecycleScreen
11+
description = "CameraX + Lifecycle",
12+
destination = CameraXLifecycleScreen
1313
),
1414
AppFeature(
1515
name = "Predictive back",

0 commit comments

Comments
 (0)