Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -408,7 +408,7 @@ Nothing specific (yet)

`Tiamat` overrides `LocalLifecycleOwner` for each destination (android only) and compatible with lifecycle-aware components

See an example of camera usage: [AndroidViewLifecycleScreen.kt](sample/composeApp/src/androidMain/kotlin/composegears/tiamat/sample/platform/AndroidViewLifecycleScreen.kt)
See an example of CameraX usage: [CameraXLifecycleScreen.kt](sample/composeApp/src/androidMain/kotlin/composegears/tiamat/sample/platform/CameraXLifecycleScreen.kt)

### iOS

Expand Down
8 changes: 5 additions & 3 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,18 @@ sample-compileSdk = "36"
sample-minSdk = "23"
sample-targetSdk = "36"

camerax = "1.5.0"
detekt = "1.23.8"
jetbrains-compose = "1.8.2"
lifecycle = "2.9.3"
kotlin = "2.2.20"

[libraries]
androidx-activity-compose = "androidx.activity:activity-compose:1.11.0"
androidx-camera-camera2 = "androidx.camera:camera-camera2:1.4.2"
androidx-camera-lifecycle = "androidx.camera:camera-lifecycle:1.4.2"
androidx-camera-view = "androidx.camera:camera-view:1.4.2"

androidx-camera-camera2 = { module = "androidx.camera:camera-camera2", version.ref = "camerax" }
androidx-camera-compose = { module = "androidx.camera:camera-compose", version.ref = "camerax" }
androidx-camera-lifecycle = { module = "androidx.camera:camera-lifecycle", version.ref = "camerax" }

compose-material3-window-size = { module = "org.jetbrains.compose.material3:material3-window-size-class", version.ref = "jetbrains-compose" }
compose-ui-backhandler = { module = "org.jetbrains.compose.ui:ui-backhandler", version.ref = "jetbrains-compose" }
Expand Down
12 changes: 1 addition & 11 deletions renovate.json5
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,5 @@
],
"extends": [
"config:recommended"
],
"packageRules": [
{
"groupName": "CameraX",
"matchPackageNames": [
"androidx.camera:camera-view",
"androidx.camera:camera-lifecycle",
"androidx.camera:camera-camera2"
]
}
]
]
}
2 changes: 1 addition & 1 deletion sample/composeApp/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -64,8 +64,8 @@ kotlin {
implementation(libs.androidx.activity.compose)

implementation(libs.androidx.camera.camera2)
implementation(libs.androidx.camera.compose)
implementation(libs.androidx.camera.lifecycle)
implementation(libs.androidx.camera.view)
}
jvmMain.dependencies {
implementation(compose.desktop.currentOs)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,14 @@ import android.content.pm.PackageManager
import android.widget.Toast
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts
import androidx.camera.compose.CameraXViewfinder
import androidx.camera.core.CameraSelector
import androidx.camera.core.CameraSelector.LENS_FACING_BACK
import androidx.camera.core.CameraSelector.LENS_FACING_FRONT
import androidx.camera.core.CameraSelector.DEFAULT_BACK_CAMERA
import androidx.camera.core.CameraSelector.DEFAULT_FRONT_CAMERA
import androidx.camera.core.Preview
import androidx.camera.core.SurfaceRequest
import androidx.camera.lifecycle.ProcessCameraProvider
import androidx.camera.lifecycle.awaitInstance
import androidx.camera.view.PreviewView
import androidx.compose.foundation.border
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
Expand All @@ -27,20 +28,27 @@ import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.draw.clipToBounds
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.unit.dp
import androidx.compose.ui.viewinterop.AndroidView
import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.ViewModel
import androidx.lifecycle.compose.LocalLifecycleOwner
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.compose.currentStateAsState
import androidx.lifecycle.viewmodel.compose.viewModel
import com.composegears.tiamat.compose.navDestination
import composegears.tiamat.sample.ui.AppButton
import composegears.tiamat.sample.ui.Screen
import kotlinx.coroutines.awaitCancellation
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.update

val AndroidViewLifecycleScreen by navDestination {
val CameraXLifecycleScreen by navDestination {
val viewModel = viewModel<CameraPreviewViewModel>()
val context = LocalContext.current

var isPermissionGranted by remember { mutableStateOf(false) }
Expand All @@ -56,7 +64,7 @@ val AndroidViewLifecycleScreen by navDestination {
else -> requestPermissionLauncher.launch(Manifest.permission.CAMERA)
}
}
Screen("AndroidView + Lifecycle handle") {
Screen("CameraX + Lifecycle") {
if (isPermissionGranted) {
Column(
modifier = Modifier.fillMaxSize(),
Expand All @@ -67,11 +75,11 @@ val AndroidViewLifecycleScreen by navDestination {
modifier = Modifier.fillMaxSize(0.8f),
contentAlignment = Alignment.Center
) {
CameraView()
CameraView(viewModel)
}

val lf = LocalLifecycleOwner.current
Text("Lifecycle State: ${lf.lifecycle.currentStateAsState()}")
Text("Lifecycle State: ${lf.lifecycle.currentStateAsState().value}")
}
} else {
PermissionDeclined {
Expand All @@ -94,72 +102,54 @@ private fun PermissionDeclined(onRequest: () -> Unit) {
}

@Composable
private fun CameraView() {
private fun CameraView(viewModel: CameraPreviewViewModel) {
val context = LocalContext.current
val lifecycleOwner = LocalLifecycleOwner.current

var lensFacing by remember { mutableIntStateOf(LENS_FACING_BACK) }
var cameraSelector by remember { mutableStateOf(DEFAULT_BACK_CAMERA) }
val surfaceRequest by viewModel.surfaceRequest.collectAsStateWithLifecycle()

val preview = remember { Preview.Builder().build() }
val previewView = remember { PreviewView(context) }
val cameraSelector = remember(lensFacing) {
CameraSelector.Builder()
.requireLensFacing(lensFacing)
.build()
}
LaunchedEffect(lensFacing) {
val cameraProvider = ProcessCameraProvider.awaitInstance(context)
cameraProvider.unbindAll()
cameraProvider.bindToLifecycle(
lifecycleOwner,
cameraSelector,
preview,
LaunchedEffect(lifecycleOwner, cameraSelector) {
viewModel.bindToCamera(
appContext = context.applicationContext,
lifecycleOwner = lifecycleOwner,
cameraSelector = cameraSelector
)
preview.surfaceProvider = previewView.surfaceProvider
}
surfaceRequest?.let {
Box(modifier = Modifier.fillMaxSize()) {
CameraXViewfinder(surfaceRequest = it)

Box(
modifier = Modifier
.fillMaxSize()
.clipToBounds()
) {
AndroidView(
modifier = Modifier
.fillMaxSize()
.align(Alignment.Center),
factory = { previewView }
)
Icon(
modifier = Modifier
.align(Alignment.BottomCenter)
.padding(bottom = 24.dp)
.navigationBarsPadding()
.size(64.dp)
.padding(1.dp)
.border(1.dp, Color.White, CircleShape)
.clip(CircleShape)
.clickable {
Toast.makeText(context, "Take photo", Toast.LENGTH_SHORT).show()
},
imageVector = Icons.Sharp.Lens,
contentDescription = null
)
Icon(
modifier = Modifier
.align(Alignment.BottomEnd)
.navigationBarsPadding()
.padding(bottom = 36.dp, end = 24.dp)
.size(40.dp)
.clip(CircleShape)
.clickable {
lensFacing = when (lensFacing) {
LENS_FACING_BACK -> LENS_FACING_FRONT
else -> LENS_FACING_BACK
}
},
imageVector = Icons.Sharp.FlipCameraAndroid,
contentDescription = null
)
Icon(
modifier = Modifier
.align(Alignment.BottomCenter)
.padding(bottom = 24.dp)
.size(64.dp)
.padding(1.dp)
.border(1.dp, Color.White, CircleShape)
.clip(CircleShape)
.clickable {
Toast.makeText(context, "Take photo", Toast.LENGTH_SHORT).show()
},
imageVector = Icons.Sharp.Lens,
contentDescription = null
)
Icon(
modifier = Modifier
.align(Alignment.BottomEnd)
.padding(bottom = 36.dp, end = 24.dp)
.size(40.dp)
.clip(CircleShape)
.clickable {
cameraSelector = when (cameraSelector) {
DEFAULT_BACK_CAMERA -> DEFAULT_FRONT_CAMERA
else -> DEFAULT_BACK_CAMERA
}
},
imageVector = Icons.Sharp.FlipCameraAndroid,
contentDescription = null
)
}
}
}

Expand All @@ -173,4 +163,35 @@ private fun Context.shouldShowRationale(permission: String) =
ActivityCompat.shouldShowRequestPermissionRationale(
this as Activity,
permission
)
)

internal class CameraPreviewViewModel : ViewModel() {

private val _surfaceRequest = MutableStateFlow<SurfaceRequest?>(null)
val surfaceRequest = _surfaceRequest.asStateFlow()

private val cameraPreviewUseCase = Preview.Builder().build().apply {
setSurfaceProvider { newSurfaceRequest ->
_surfaceRequest.update { newSurfaceRequest }
}
}

suspend fun bindToCamera(
appContext: Context,
lifecycleOwner: LifecycleOwner,
cameraSelector: CameraSelector
) {
val processCameraProvider = ProcessCameraProvider.awaitInstance(appContext)
processCameraProvider.bindToLifecycle(
lifecycleOwner = lifecycleOwner,
cameraSelector = cameraSelector,
cameraPreviewUseCase
)

try {
awaitCancellation()
} finally {
processCameraProvider.unbindAll()
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@ actual fun Platform.name(): String = "Android"
actual fun Platform.platformFeatures(): List<AppFeature> = listOf(
AppFeature(
name = "CameraX",
description = "AndroidView + lifecycle handling",
destination = AndroidViewLifecycleScreen
description = "CameraX + Lifecycle",
destination = CameraXLifecycleScreen
),
AppFeature(
name = "Predictive back",
Expand Down