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 app-android/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ android {
compileSdk = 36
defaultConfig {
applicationId = "dev.sdkforge.camera.android"
minSdk = 21
minSdk = 23
targetSdk = 36
versionCode = 1
versionName = "1.0"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,16 @@ import androidx.activity.enableEdgeToEdge
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.mutableStateSetOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import dev.sdkforge.camera.app.App
import dev.sdkforge.camera.app.AppConstants
import dev.sdkforge.camera.domain.CameraConfig
import dev.sdkforge.camera.domain.Facing
import dev.sdkforge.camera.domain.Format
import dev.sdkforge.camera.domain.ScanResult
import dev.sdkforge.camera.ui.rememberCameraController

class MainActivity : ComponentActivity() {
Expand Down Expand Up @@ -52,16 +56,25 @@ class MainActivity : ComponentActivity() {
cameraFacing = Facing.BACK,
),
)

val scans = remember { mutableStateSetOf<ScanResult>() }
App(
cameraController = cameraController,
scans = scans,
modifier = Modifier
.fillMaxSize(),
)

LaunchedEffect(Unit) {
cameraController.scannedResults.collect { scanResult ->
toast("${scanResult.format.name}; value = ${scanResult.value}")
if (scans.contains(scanResult)) {
toast("${AppConstants.SUCCESS_TITLE} - ${AppConstants.ALREADY_SCANNED_MESSAGE}")
} else {
if (scans.size == AppConstants.HISTORY_SCANS_MAX_LENGTH) {
scans.remove(scans.first())
}
scans.add(scanResult)
toast("${AppConstants.SUCCESS_TITLE} - ${AppConstants.SCANNED_VALUE} ${scanResult.value}")
}
}
}

Expand Down
1 change: 1 addition & 0 deletions app-shared/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ kotlin {

api(compose.foundation)
api(compose.material3)
api(compose.materialIconsExtended)
}
}
}
Expand Down
14 changes: 12 additions & 2 deletions app-shared/src/commonMain/kotlin/dev/sdkforge/camera/app/App.kt
Original file line number Diff line number Diff line change
Expand Up @@ -5,21 +5,31 @@ import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import dev.sdkforge.camera.domain.ScanResult
import dev.sdkforge.camera.ui.CameraController
import dev.sdkforge.camera.ui.CameraView

@Composable
fun App(
cameraController: CameraController,
scans: Set<ScanResult>,
modifier: Modifier = Modifier,
) = ApplicationTheme {
Surface(
modifier = modifier,
color = MaterialTheme.colorScheme.background,
) {
CameraView(
ScannerScreen(
cameraController = cameraController,
scans = scans,
modifier = Modifier.fillMaxSize(),
)
}
}

object AppConstants {
const val HISTORY_SCANS_MAX_LENGTH = 5
const val ERROR_TITLE = "Error"
const val SUCCESS_TITLE = "Success"
const val ALREADY_SCANNED_MESSAGE = "Already scanned, check scans history"
const val SCANNED_VALUE = "Scanned value:"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
package dev.sdkforge.camera.app

import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.FlashOff
import androidx.compose.material.icons.filled.FlashOn
import androidx.compose.material.icons.filled.FlipCameraAndroid
import androidx.compose.material.icons.filled.History
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.ModalBottomSheet
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.material3.rememberModalBottomSheetState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import dev.sdkforge.camera.domain.ScanResult
import dev.sdkforge.camera.ui.CameraController
import dev.sdkforge.camera.ui.CameraView

@Composable
fun ScannerScreen(
cameraController: CameraController,
scans: Set<ScanResult>,
modifier: Modifier = Modifier,
) {
var isHistoryDialogShown by remember { mutableStateOf(false) }

Scaffold(
modifier = Modifier.fillMaxSize(),
) { innerPadding ->
CameraView(
cameraController = cameraController,
modifier = Modifier.fillMaxSize(),
)
ButtonsOverlay(
controller = cameraController,
modifier = modifier.padding(top = innerPadding.calculateTopPadding()),
onHistoryClicked = { isHistoryDialogShown = !isHistoryDialogShown },
)
ScansHistoryDialog(
showDialog = isHistoryDialogShown,
scans = scans,
onDismiss = { isHistoryDialogShown = false },
)
}
}

@Composable
fun ButtonsOverlay(
controller: CameraController,
modifier: Modifier = Modifier,
onHistoryClicked: () -> Unit,
) {
var isFlashOn by remember { mutableStateOf(false) }
val targetIcon = if (isFlashOn) Icons.Default.FlashOff else Icons.Default.FlashOn

Column {
Row(
horizontalArrangement = Arrangement.SpaceEvenly,
verticalAlignment = Alignment.Top,
modifier = modifier.fillMaxWidth(),
) {
IconButton(
onClick = {
onHistoryClicked.invoke()
},
) {
Icon(
imageVector = Icons.Default.History,
contentDescription = "Show scans history",
modifier = Modifier.clickable {
onHistoryClicked.invoke()
},
)
}
IconButton(
onClick = {
controller.toggleFlash()
isFlashOn = controller.isFlashIsOn()
},
) {
Icon(
imageVector = targetIcon,
contentDescription = "Flash toggle",
)
}
IconButton(
onClick = {
controller.toggleActiveCamera()
},
) {
Icon(
imageVector = Icons.Default.FlipCameraAndroid,
contentDescription = "Flip between front and back active cameras",
)
}
}
}
}

@Composable
@OptIn(ExperimentalMaterial3Api::class)
fun ScansHistoryDialog(
showDialog: Boolean,
scans: Set<ScanResult>,
onDismiss: () -> Unit,
) {
val sheetState = rememberModalBottomSheetState()
if (showDialog) {
ModalBottomSheet(
onDismissRequest = {
onDismiss.invoke()
},
sheetState = sheetState,
modifier = Modifier.fillMaxSize(),
) {
if (scans.isEmpty()) {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier.fillMaxWidth(),
) {
Text(
text = "Scans history is empty",
)
}
} else {
LazyColumn(
modifier = Modifier.padding(8.dp).fillMaxWidth(),
) {
itemsIndexed(items = scans.toList()) { index, item ->
Text(
text = "${index + 1}. ${item.value}",
modifier = Modifier.padding(top = 12.dp),
)
}
}
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,15 @@ package dev.sdkforge.camera.app

import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.mutableStateSetOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.uikit.LocalUIViewController
import androidx.compose.ui.window.ComposeUIViewController
import dev.sdkforge.camera.domain.CameraConfig
import dev.sdkforge.camera.domain.Facing
import dev.sdkforge.camera.domain.Format
import dev.sdkforge.camera.domain.ScanResult
import dev.sdkforge.camera.ui.rememberCameraController
import kotlin.experimental.ExperimentalObjCName
import platform.AVFoundation.AVCaptureDevice
Expand Down Expand Up @@ -37,18 +40,28 @@ fun ComposeAppViewController() = ComposeUIViewController(
cameraFacing = Facing.BACK,
),
)

val scans = remember { mutableStateSetOf<ScanResult>() }
App(
cameraController = cameraController,
scans = scans,
modifier = Modifier
.fillMaxSize(),
)

LaunchedEffect(Unit) {
cameraController.scannedResults.collect { scanResult ->
val alertText = if (scans.contains(scanResult)) {
AppConstants.ERROR_TITLE to AppConstants.ALREADY_SCANNED_MESSAGE
} else {
if (scans.size == AppConstants.HISTORY_SCANS_MAX_LENGTH) {
scans.remove(scans.first())
}
scans.add(scanResult)
AppConstants.SUCCESS_TITLE to "${AppConstants.SCANNED_VALUE} ${scanResult.value}"
}
val alert = UIAlertController.alertControllerWithTitle(
title = "Scanned format: ${scanResult.format}",
message = "Scanned value: ${scanResult.value}",
title = alertText.first,
message = alertText.second,
preferredStyle = UIAlertControllerStyleAlert,
).apply {
addAction(
Expand Down
7 changes: 7 additions & 0 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@ runner = "1.7.0"
junit = "1.3.0"
junitVersion = "4.13.2"
benchmarkJunit4 = "1.4.0"
androidxCamera = "1.5.0"
barcodeScanning = "17.3.0"
barcodeServices = "18.3.1"

[libraries]
android-gradle-plugin = { group = "com.android.tools.build", name = "gradle", version.ref = "agp" }
Expand All @@ -30,6 +33,10 @@ androidx-runner = { group = "androidx.test", name = "runner", version.ref = "run
androidx-junit = { group = "androidx.test.ext", name = "junit", version.ref = "junit" }
junit = { group = "junit", name = "junit", version.ref = "junitVersion" }
androidx-benchmark-junit4 = { group = "androidx.benchmark", name = "benchmark-junit4", version.ref = "benchmarkJunit4" }
androidx-camera-camera = { group = "androidx.camera", name="camera-camera2", version.ref = "androidxCamera" }
androidx-camera-view = { group = "androidx.camera", name = "camera-view",version.ref = "androidxCamera" }
barcode-scanning = { group = "com.google.mlkit", name = "barcode-scanning", version.ref = "barcodeScanning" }
barcode-services = { group = "com.google.android.gms", name = "play-services-mlkit-barcode-scanning", version.ref = "barcodeServices" }

[plugins]
androidApplication = { id = "com.android.application", version.ref = "agp" }
Expand Down
3 changes: 3 additions & 0 deletions shared-ui/api/shared-ui.api
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@ public abstract class dev/sdkforge/camera/ui/CameraController {
public fun <init> ()V
public abstract fun getCameraState ()Ldev/sdkforge/camera/ui/CameraState;
public abstract fun getScannedResults ()Lkotlinx/coroutines/flow/Flow;
public abstract fun isFlashIsOn ()Z
public abstract fun toggleActiveCamera ()V
public abstract fun toggleFlash ()V
}

public abstract interface class dev/sdkforge/camera/ui/CameraState {
Expand Down
8 changes: 4 additions & 4 deletions shared-ui/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -28,10 +28,10 @@ kotlin {

androidMain {
dependencies {
implementation("androidx.camera:camera-camera2:1.4.2")
implementation("androidx.camera:camera-view:1.4.2")
implementation("com.google.mlkit:barcode-scanning:17.3.0")
implementation("com.google.android.gms:play-services-mlkit-barcode-scanning:18.3.1")
implementation(libs.androidx.camera.camera)
implementation(libs.androidx.camera.view)
implementation(libs.barcode.scanning)
implementation(libs.barcode.services)
}
}
}
Expand Down
19 changes: 10 additions & 9 deletions shared-ui/dependencies/releaseRuntimeClasspath.txt
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,12 @@ androidx.appcompat:appcompat:1.6.1
androidx.arch.core:core-common:2.2.0
androidx.arch.core:core-runtime:2.2.0
androidx.autofill:autofill:1.0.0
androidx.camera:camera-camera2:1.4.2
androidx.camera:camera-core:1.4.2
androidx.camera:camera-lifecycle:1.4.2
androidx.camera:camera-video:1.4.2
androidx.camera:camera-view:1.4.2
androidx.camera.featurecombinationquery:featurecombinationquery:1.5.0
androidx.camera:camera-camera2:1.5.0
androidx.camera:camera-core:1.5.0
androidx.camera:camera-lifecycle:1.5.0
androidx.camera:camera-video:1.5.0
androidx.camera:camera-view:1.5.0
androidx.collection:collection-jvm:1.5.0
androidx.collection:collection-ktx:1.5.0
androidx.collection:collection:1.5.0
Expand Down Expand Up @@ -133,9 +134,9 @@ org.jetbrains.compose.ui:ui-unit:1.8.2
org.jetbrains.compose.ui:ui-util:1.8.2
org.jetbrains.compose.ui:ui:1.8.2
org.jetbrains.kotlin:kotlin-stdlib:2.2.20-Beta2
org.jetbrains.kotlinx:kotlinx-coroutines-android:1.8.0
org.jetbrains.kotlinx:kotlinx-coroutines-bom:1.8.0
org.jetbrains.kotlinx:kotlinx-coroutines-core-jvm:1.8.0
org.jetbrains.kotlinx:kotlinx-coroutines-core:1.8.0
org.jetbrains.kotlinx:kotlinx-coroutines-android:1.8.1
org.jetbrains.kotlinx:kotlinx-coroutines-bom:1.8.1
org.jetbrains.kotlinx:kotlinx-coroutines-core-jvm:1.8.1
org.jetbrains.kotlinx:kotlinx-coroutines-core:1.8.1
org.jetbrains:annotations:23.0.0
org.jspecify:jspecify:1.0.0
Loading