diff --git a/compose-customisable-ui-example/.idea/codeStyles/Project.xml b/compose-customisable-ui-example/.idea/codeStyles/Project.xml
new file mode 100644
index 00000000..113c8620
--- /dev/null
+++ b/compose-customisable-ui-example/.idea/codeStyles/Project.xml
@@ -0,0 +1,161 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ xmlns:android
+
+ ^$
+
+
+
+
+
+
+
+
+ xmlns:.*
+
+ ^$
+
+
+ BY_NAME
+
+
+
+
+
+
+ .*:id
+
+ http://schemas.android.com/apk/res/android
+
+
+
+
+
+
+
+
+ .*:name
+
+ http://schemas.android.com/apk/res/android
+
+
+
+
+
+
+
+
+ name
+
+ ^$
+
+
+
+
+
+
+
+
+ style
+
+ ^$
+
+
+
+
+
+
+
+
+ .*
+
+ ^$
+
+
+ BY_NAME
+
+
+
+
+
+
+ .*
+
+ http://schemas.android.com/apk/res/android
+
+
+ ANDROID_ATTRIBUTE_ORDER
+
+
+
+
+
+
+ .*
+
+ .*
+
+
+ BY_NAME
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/compose-customisable-ui-example/.idea/codeStyles/codeStyleConfig.xml b/compose-customisable-ui-example/.idea/codeStyles/codeStyleConfig.xml
new file mode 100644
index 00000000..79ee123c
--- /dev/null
+++ b/compose-customisable-ui-example/.idea/codeStyles/codeStyleConfig.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/compose-customisable-ui-example/Libraries.txt b/compose-customisable-ui-example/Libraries.txt
new file mode 120000
index 00000000..9cc9caf9
--- /dev/null
+++ b/compose-customisable-ui-example/Libraries.txt
@@ -0,0 +1 @@
+../Libraries.txt
\ No newline at end of file
diff --git a/compose-customisable-ui-example/app/build.gradle b/compose-customisable-ui-example/app/build.gradle
new file mode 100644
index 00000000..201819c7
--- /dev/null
+++ b/compose-customisable-ui-example/app/build.gradle
@@ -0,0 +1,105 @@
+plugins {
+ id("com.android.application")
+ id("org.jetbrains.kotlin.android")
+ id("org.jetbrains.kotlin.kapt")
+ id("org.jetbrains.kotlin.plugin.compose")
+}
+
+android {
+ namespace = "io.scanbot.example.compose"
+ compileSdk = 36
+
+ defaultConfig {
+ applicationId = "io.scanbot.example.compose"
+ targetSdk = 36
+ minSdk = 21
+ versionCode = 1
+ versionName = "1.0"
+
+ ndk {
+ abiFilters "armeabi-v7a", "arm64-v8a"
+ // Please add "x86" and "x86_64" if you would like to test on an emulator
+ // or if you need to support some rare devices with the Intel Atom architecture.
+ }
+ }
+
+ buildTypes {
+ named("debug") {
+ // set this to `false` to allow debugging and run a "non-release" build
+ minifyEnabled = false
+ debuggable = true
+ }
+ named("release") {
+ // set this to `false` to allow debugging and run a "non-release" build
+ minifyEnabled = true
+ debuggable = true
+ }
+ }
+
+ kotlin {
+ jvmToolchain(17)
+ }
+
+ buildFeatures {
+ buildConfig = true
+ compose = true
+ }
+
+ packagingOptions {
+ exclude "META-INF/LICENSE.txt"
+ exclude "META-INF/LICENSE"
+ exclude "META-INF/NOTICE.txt"
+ exclude "META-INF/NOTICE"
+ exclude "META-INF/DEPENDENCIES"
+ }
+}
+
+kapt {
+ generateStubs = true
+}
+
+configurations {
+ compile.exclude group: "org.jetbrains", module: "annotations"
+}
+
+dependencies {
+ implementation("androidx.appcompat:appcompat:1.7.1")
+ implementation("com.google.android.material:material:1.13.0")
+ // we are using compose dependencies that come transitively via Scanbot SDK.
+ // If you need additional compose dependencies, please make sure to use the same versions as Scanbot SDK to avoid version conflicts.
+ def coroutines_version = "1.10.2"
+ implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutines_version")
+ implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutines_version")
+
+ def scanbotSdkVersion = "8.1.0.78-STAGING-SNAPSHOT"
+
+ implementation("io.scanbot:sdk-package-4:$scanbotSdkVersion")
+ implementation("io.scanbot:rtu-ui-v2-bundle:$scanbotSdkVersion")
+
+ // This dependency is only needed if you plan to use the Generic Document Recognizer feature
+ implementation("io.scanbot:sdk-documentdata-assets:$scanbotSdkVersion")
+
+ // This dependency is only needed if you plan to use the Document Quality Analyzer feature
+ implementation("io.scanbot:sdk-multitasktext-assets:$scanbotSdkVersion")
+
+ // This dependency is only needed if you plan to use data scanner feature
+ implementation("io.scanbot:sdk-textpattern-assets:$scanbotSdkVersion")
+
+ // This dependency is only needed if you plan to use Medical Certificate scanner feature
+ implementation("io.scanbot:sdk-mc-assets:$scanbotSdkVersion")
+
+ // This dependency is only needed if you plan to use Credit Card Scanner feature
+ implementation("io.scanbot:sdk-creditcard-assets:$scanbotSdkVersion")
+
+ // This dependency is only needed if you plan to use MRZ scanner feature
+ implementation("io.scanbot:sdk-mrz-assets:$scanbotSdkVersion")
+
+ // This dependency is only needed if you plan to use Check recognizer feature
+ implementation("io.scanbot:sdk-check-assets:$scanbotSdkVersion")
+
+ // This dependency is only needed if you plan to use Pdfium processor for import export of pdfs. See comment in Application class.
+ implementation("io.scanbot:bundle-sdk-pdfium:$scanbotSdkVersion")
+
+ // This dependency is only needed if you plan to use the encryption feature
+ implementation("io.scanbot:bundle-sdk-crypto-persistence:$scanbotSdkVersion")
+}
diff --git a/compose-customisable-ui-example/app/src/main/AndroidManifest.xml b/compose-customisable-ui-example/app/src/main/AndroidManifest.xml
new file mode 100644
index 00000000..8ad7bb62
--- /dev/null
+++ b/compose-customisable-ui-example/app/src/main/AndroidManifest.xml
@@ -0,0 +1,36 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/compose-customisable-ui-example/app/src/main/assets/ocr_blobs/deu.traineddata b/compose-customisable-ui-example/app/src/main/assets/ocr_blobs/deu.traineddata
new file mode 100644
index 00000000..97ed7b2b
Binary files /dev/null and b/compose-customisable-ui-example/app/src/main/assets/ocr_blobs/deu.traineddata differ
diff --git a/compose-customisable-ui-example/app/src/main/assets/ocr_blobs/eng.traineddata b/compose-customisable-ui-example/app/src/main/assets/ocr_blobs/eng.traineddata
new file mode 100755
index 00000000..bbef4675
Binary files /dev/null and b/compose-customisable-ui-example/app/src/main/assets/ocr_blobs/eng.traineddata differ
diff --git a/compose-customisable-ui-example/app/src/main/assets/ocr_blobs/osd.traineddata b/compose-customisable-ui-example/app/src/main/assets/ocr_blobs/osd.traineddata
new file mode 100644
index 00000000..183644aa
Binary files /dev/null and b/compose-customisable-ui-example/app/src/main/assets/ocr_blobs/osd.traineddata differ
diff --git a/compose-customisable-ui-example/app/src/main/java/io/scanbot/example/compose/Application.kt b/compose-customisable-ui-example/app/src/main/java/io/scanbot/example/compose/Application.kt
new file mode 100644
index 00000000..d269f3b2
--- /dev/null
+++ b/compose-customisable-ui-example/app/src/main/java/io/scanbot/example/compose/Application.kt
@@ -0,0 +1,112 @@
+package io.scanbot.example.compose
+
+import android.app.Application
+import android.util.Log
+import android.widget.Toast
+import io.scanbot.example.compose.BuildConfig
+import io.scanbot.sap.IScanbotSDKLicenseErrorHandler
+import io.scanbot.sdk.ScanbotSDK
+import io.scanbot.sdk.ScanbotSDKInitializer
+import io.scanbot.sdk.licensing.LicenseStatus
+import io.scanbot.sdk.persistence.CameraImageFormat
+import io.scanbot.sdk.persistence.page.PageStorageSettings
+import io.scanbot.sdk.persistence.fileio.AESEncryptedFileIOProcessor
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.launch
+import java.io.File
+import kotlin.coroutines.CoroutineContext
+
+class Application : Application(), CoroutineScope {
+
+ private var job: Job = Job()
+ override val coroutineContext: CoroutineContext
+ get() = Dispatchers.IO + job
+
+ companion object {
+ /*
+ * TODO Add the Scanbot SDK license key here.
+ * Please note: The Scanbot SDK will run without a license key for one minute per session!
+ * After the trial period is over all Scanbot SDK functions as well as the UI components will stop working.
+ * You can get an unrestricted "no-strings-attached" 30 day trial license key for free.
+ * Please submit the trial license form (https://scanbot.io/sdk/trial.html) on our website by using
+ * the app identifier "io.scanbot.example.sdk.rtu.android" of this example app.
+ */
+ const val LICENSE_KEY = ""
+
+ // TODO: you can enable encryption of all the image files and generated PDFs by changing this property
+ const val USE_ENCRYPTION = false
+
+ // TODO: you should store a password in a secure place or let the user enter it manually
+ private const val ENCRYPTION_PASSWORD = "password"
+
+ // TODO: you can select an encryption method
+ private val ENCRYPTION_METHOD = AESEncryptedFileIOProcessor.AESEncrypterMode.AES256
+ }
+
+ override fun onCreate() {
+ super.onCreate()
+ val sdkLicenseInfo = ScanbotSDKInitializer()
+ .withLogging(BuildConfig.DEBUG)
+ // Optional, custom SDK files directory. Please see the comments below!
+ .sdkFilesDirectory(this, customStorageDirectory())
+ .usePageStorageSettings(
+ PageStorageSettings.Builder()
+ .imageFormat(CameraImageFormat.JPG)
+ .imageQuality(80)
+ .previewTargetMax(1500)
+ .build()
+ )
+ .prepareOCRLanguagesBlobs(true)
+ .useFileEncryption(USE_ENCRYPTION, AESEncryptedFileIOProcessor(ENCRYPTION_PASSWORD, ENCRYPTION_METHOD))
+ .licenseErrorHandler(IScanbotSDKLicenseErrorHandler { status, feature, statusMessage ->
+ // Optional license failure handler implementation. Handle license issues here.
+ // A license issue can either be an invalid or expired license key
+ // or missing SDK feature (see SDK feature packages on https://scanbot.io).
+ val errorMsg = if (status != LicenseStatus.OKAY && status != LicenseStatus.TRIAL) {
+ "License Error! License status: ${status.name}. $statusMessage"
+ } else {
+ "License Error! Missing SDK feature in license: ${feature.name}. $statusMessage"
+ }
+ Log.d("ScanbotSDKExample", errorMsg)
+ Toast.makeText(this@Application, errorMsg, Toast.LENGTH_LONG).show()
+ })
+
+ // Uncomment to switch back to the legacy camera approach in Ready-To-Use UI screens
+ // .useCameraXRtuUi(false)
+ .license(this, LICENSE_KEY)
+ .initialize(this)
+
+ // Check the Scanbot SDK license status:
+ Log.d("ScanbotSDKExample", "Is license valid: " + sdkLicenseInfo.isValid)
+ Log.d("ScanbotSDKExample", "License status " + sdkLicenseInfo.status.name)
+
+ launch {
+ // Leaving as is to clean end-users' storage for next several app updates.
+ ScanbotSDK(this@Application).documentApi.deleteAllDocuments()
+ }
+ }
+
+ private fun customStorageDirectory(): File {
+ // !! Please note !!
+ // It is strongly recommended to use the default (secure) storage directory of the Scanbot SDK.
+ // However, for demo purposes we use a custom external(!) storage directory here, which is a public(!) folder.
+ // All image files and export files (PDF, TIFF, etc) created by the Scanbot SDK in this demo app will be stored
+ // in this public storage directory and will be accessible for every(!) app having external storage permissions!
+ // Again, this is only for demo purposes, which allows us to easily fetch and check the generated files
+ // via Android "adb" CLI tools, Android File Transfer app, Android Studio, etc.
+ //
+ // For more details about the storage system of the Scanbot SDK please see our docs:
+ // https://github.com/doo/scanbot-sdk-example-android/wiki/Storage
+ //
+ // For more details about the file system on Android we also highly recommend to check out:
+ // - https://developer.android.com/guide/topics/data/data-storage
+ // - https://developer.android.com/training/data-storage/files
+
+ val customDir =
+ File(this.getExternalFilesDir(null) ?: this.filesDir, "my-custom-storage-folder")
+ customDir.mkdirs()
+ return customDir
+ }
+}
diff --git a/compose-customisable-ui-example/app/src/main/java/io/scanbot/example/compose/BarcodeDetailScreen.kt b/compose-customisable-ui-example/app/src/main/java/io/scanbot/example/compose/BarcodeDetailScreen.kt
new file mode 100644
index 00000000..0347a684
--- /dev/null
+++ b/compose-customisable-ui-example/app/src/main/java/io/scanbot/example/compose/BarcodeDetailScreen.kt
@@ -0,0 +1,33 @@
+package io.scanbot.example.compose
+
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.padding
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Surface
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.unit.dp
+
+
+@Composable
+fun BarcodeDetailScreen(data: String, format: String) {
+ Surface(modifier = Modifier.fillMaxSize()) {
+ Column(
+ modifier = Modifier.padding(32.dp),
+ verticalArrangement = Arrangement.Center,
+ horizontalAlignment = Alignment.CenterHorizontally
+ ) {
+ Text("Barcode Data:", style = MaterialTheme.typography.titleLarge)
+ Text(
+ data,
+ style = MaterialTheme.typography.bodyLarge,
+ modifier = Modifier.padding(vertical = 8.dp)
+ )
+ Text("Format: $format", style = MaterialTheme.typography.bodyMedium)
+ }
+ }
+}
\ No newline at end of file
diff --git a/compose-customisable-ui-example/app/src/main/java/io/scanbot/example/compose/BarcodeFindAndPick.kt b/compose-customisable-ui-example/app/src/main/java/io/scanbot/example/compose/BarcodeFindAndPick.kt
new file mode 100644
index 00000000..57cb1648
--- /dev/null
+++ b/compose-customisable-ui-example/app/src/main/java/io/scanbot/example/compose/BarcodeFindAndPick.kt
@@ -0,0 +1,169 @@
+package io.scanbot.example.compose
+
+import android.util.Log
+import androidx.annotation.OptIn
+import androidx.camera.camera2.interop.ExperimentalCamera2Interop
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.PaddingValues
+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.layout.systemBarsPadding
+import androidx.compose.material.Icon
+import androidx.compose.material.TopAppBar
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.automirrored.filled.ArrowBack
+import androidx.compose.material3.Button
+import androidx.compose.material3.IconButton
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Scaffold
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.mutableFloatStateOf
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.geometry.Rect
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.layout.layout
+import androidx.compose.ui.platform.LocalDensity
+import androidx.compose.ui.text.style.TextAlign
+import androidx.compose.ui.unit.dp
+import androidx.navigation.NavHostController
+import androidx.navigation.NavOptions
+import io.scanbot.common.*
+import io.scanbot.demo.composeui.ui.theme.sbBrandColor
+import io.scanbot.example.compose.components.*
+import io.scanbot.sdk.barcode.textWithExtension
+import io.scanbot.sdk.geometry.*
+import io.scanbot.sdk.ui_v2.barcode.*
+import io.scanbot.sdk.ui_v2.barcode.components.ar_tracking.ScanbotBarcodesArOverlay
+import io.scanbot.sdk.ui_v2.common.*
+import io.scanbot.sdk.ui_v2.common.components.*
+import kotlin.random.*
+
+@OptIn(ExperimentalCamera2Interop::class)
+@Composable
+fun BarcodeFindAndPick(navController: NavHostController) {
+ val density = LocalDensity.current
+ // Use these states to control camera, torch and zoom
+ val zoom = remember { mutableFloatStateOf(1.0f) }
+ val torchEnabled = remember { mutableStateOf(false) }
+ val cameraEnabled = remember { mutableStateOf(true) }
+
+ // Unused in this example, but you may use it to
+ // enable/disable barcode scanning dynamically
+ val barcodeScanningEnabled = remember { mutableStateOf(true) }
+ val expectedBarcodeValue = "Scanbot" // Expected barcode value to find
+ Scaffold(
+ modifier = Modifier.systemBarsPadding(),
+ topBar = {
+ TopAppBar(
+ title = {
+ Text(
+ text = "Barcode Single Scan",
+ style = MaterialTheme.typography.titleLarge,
+ color = Color.White
+ )
+ },
+ backgroundColor = sbBrandColor,
+ navigationIcon = {
+ IconButton(onClick = {
+ navController.popBackStack()
+ }) {
+ Icon(
+ tint = Color.White,
+ imageVector = Icons.AutoMirrored.Filled.ArrowBack,
+ contentDescription = ""
+ )
+ }
+ })
+ },
+ content = { paddingValues ->
+ Column(
+ modifier = Modifier
+ .fillMaxSize()
+ .padding(paddingValues)
+ ) {
+ // @Tag("Find And Pick Single Barcode")
+ BarcodeScannerCustomUI(
+ // Modify Size here:
+ modifier = Modifier
+ .fillMaxWidth()
+ .weight(1.0f),
+ cameraEnabled = cameraEnabled.value,
+ barcodeScanningEnabled = barcodeScanningEnabled.value,
+ torchEnabled = torchEnabled.value,
+ zoomLevel = zoom.floatValue,
+ permissionView = {
+ // View that will be shown while camera permission is not granted
+ Box(modifier = Modifier.fillMaxSize()) {
+ Text(
+ "Camera permission is required to scan barcodes.",
+ modifier = Modifier.padding(16.dp),
+ color = Color.White
+ )
+ }
+ },
+ arPolygonView = { barcodesFlow ->
+ // Configure AR overlay polygon appearance inside CustomBarcodesArView if needed
+ ScanbotBarcodesArOverlay(
+ barcodesFlow,
+ getData = { barcodeItem -> barcodeItem.textWithExtension },
+ getPolygonStyle = { defaultStyle, barcodeItem ->
+ // Customize polygon style here.
+ // You may use barcodeItem to apply different styles for different barcode types, etc.
+ defaultStyle.copy(
+ drawPolygon = true,
+ useFill = true,
+ useFillHighlighted = true,
+ cornerRadius = density.run { 20.dp.toPx() },
+ cornerHighlightedRadius = density.run { 20.dp.toPx() },
+ strokeWidth = density.run { 5.dp.toPx() },
+ strokeHighlightedWidth = density.run { 5.dp.toPx() },
+ strokeColor = Color.Red,
+ strokeHighlightedColor = Color.Green,
+ fillColor = Color.Red.copy(alpha = 0.3f),
+ fillHighlightedColor = Color.Green.copy(alpha = 0.3f),
+ shouldDrawShadows = false
+ )
+ },
+ shouldHighlight = { barcodeItem ->
+ // Here you can implement any custom logic.
+ barcodeItem.text == expectedBarcodeValue
+ },
+ // Customize AR view for barcode data here if needed
+ view = { path, barcodeItem, data, shouldHighlight ->
+ // Implement custom view for barcode polygon if needed
+ // See CustomBarcodesArView.kt for details
+ },
+ onClick = {
+ // Handle barcode click on barcode from AR overlay if needed
+ },
+ )
+ },
+ onBarcodeScanningResult = { result ->
+ result.onSuccess { data ->
+ // Navigate to detail screen for the first barcode
+ val firstBarcode = data.barcodes.firstOrNull()
+ if(firstBarcode?.text == expectedBarcodeValue){
+ // handle the found barcode if needed
+ }
+ }.onFailure { error ->
+ Log.e(
+ "BarcodeScannerScreen3",
+ "Barcode scanning error: ${error.message}",
+ error
+ )
+ }
+ },
+ )
+ // @EndTag("Find And Pick Single Barcode")
+ }
+ }
+ )
+}
\ No newline at end of file
diff --git a/compose-customisable-ui-example/app/src/main/java/io/scanbot/example/compose/BarcodeScannerBatchScan.kt b/compose-customisable-ui-example/app/src/main/java/io/scanbot/example/compose/BarcodeScannerBatchScan.kt
new file mode 100644
index 00000000..06752f78
--- /dev/null
+++ b/compose-customisable-ui-example/app/src/main/java/io/scanbot/example/compose/BarcodeScannerBatchScan.kt
@@ -0,0 +1,148 @@
+package io.scanbot.example.compose
+
+import android.util.Log
+import androidx.annotation.OptIn
+import androidx.camera.camera2.interop.ExperimentalCamera2Interop
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.systemBarsPadding
+import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.foundation.lazy.items
+import androidx.compose.material.Icon
+import androidx.compose.material.TopAppBar
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.automirrored.filled.ArrowBack
+import androidx.compose.material3.IconButton
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Scaffold
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.mutableFloatStateOf
+import androidx.compose.runtime.mutableStateListOf
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.rememberCoroutineScope
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.unit.dp
+import androidx.navigation.NavHostController
+import io.scanbot.common.*
+import io.scanbot.demo.composeui.ui.theme.sbBrandColor
+import io.scanbot.example.compose.components.*
+import io.scanbot.sdk.barcode.*
+import io.scanbot.sdk.geometry.*
+import io.scanbot.sdk.ui_v2.barcode.*
+import io.scanbot.sdk.ui_v2.common.components.*
+import io.scanbot.sdk.util.snap.*
+import kotlinx.coroutines.launch
+
+@OptIn(ExperimentalCamera2Interop::class)
+@Composable
+fun BarcodeScannerBatchScan(navController: NavHostController) {
+ val zoom = remember { mutableFloatStateOf(1.0f) }
+ val torchEnabled = remember { mutableStateOf(false) }
+ val cameraEnabled = remember { mutableStateOf(true) }
+ val barcodeScanningEnabled = remember { mutableStateOf(true) }
+ val scannedBarcodes = remember { mutableStateListOf() }
+ val context = LocalContext.current
+ val soundController = remember {
+ SoundControllerImpl(context).apply {
+ // Prepare Scanbot beep sound controller:
+ setUp()
+ setVibrationEnabled(true)
+ setBleepEnabled(true)
+ }
+ }
+ val scope = rememberCoroutineScope()
+ Scaffold(
+ modifier = Modifier.systemBarsPadding(),
+ topBar = {
+ TopAppBar(
+ title = {
+ Text(
+ text = "Batch Barcodes Scan",
+ style = MaterialTheme.typography.titleLarge,
+ color = Color.White
+ )
+ },
+ backgroundColor = sbBrandColor,
+ navigationIcon = {
+ IconButton(onClick = {
+ navController.popBackStack()
+ }) {
+ Icon(
+ tint = Color.White,
+ imageVector = Icons.AutoMirrored.Filled.ArrowBack,
+ contentDescription = ""
+ )
+ }
+ })
+ },
+ content = { paddingValues ->
+ Column(
+ modifier = Modifier
+ .fillMaxSize()
+ .padding(paddingValues)
+ ) {
+ // @Tag("Batch Scanning")
+ BarcodeScannerCustomUI(
+ modifier = Modifier.weight(1f),
+ cameraEnabled = cameraEnabled.value,
+ barcodeScanningEnabled = barcodeScanningEnabled.value,
+ torchEnabled = torchEnabled.value,
+ zoomLevel = zoom.floatValue,
+ finderConfiguration = FinderConfiguration(
+ aspectRatio = AspectRatio(2.0, 1.0),
+ overlayColor = Color(0x5500FF00),
+ strokeColor = Color.Transparent,
+ finderContent = {
+ CorneredFinder()
+ }
+ ),
+ permissionView = {
+ Box(modifier = Modifier.fillMaxSize()) {
+ Text(
+ "Camera permission is required to scan barcodes.",
+ modifier = Modifier.padding(16.dp),
+ color = Color.White
+ )
+ }
+ },
+ onBarcodeScanningResult = { result ->
+ result.onSuccess { result ->
+ result.barcodes.forEach { data ->
+ if (scannedBarcodes.none { it.text == data.text && it.format == data.format }) {
+ scope.launch {
+ // Provide sound and vibration feedback on scan:
+ soundController.playBleepSound()
+ }
+ scannedBarcodes.add(data)
+ }
+ }
+ }.onFailure { error ->
+ Log.e(
+ "BarcodeScannerScreen2",
+ "Barcode scanning error: ${error.message}",
+ error
+ )
+ }
+ }
+ )
+ // @EndTag("Batch Scanning")
+ LazyColumn(
+ modifier = Modifier
+ .fillMaxWidth()
+ .weight(1f)
+ .padding(8.dp),
+ ) {
+ items(scannedBarcodes.reversed()) { barcode ->
+ BarcodeItem(barcode)
+ }
+ }
+ }
+ })
+}
diff --git a/compose-customisable-ui-example/app/src/main/java/io/scanbot/example/compose/BarcodeScannerDistantScan.kt b/compose-customisable-ui-example/app/src/main/java/io/scanbot/example/compose/BarcodeScannerDistantScan.kt
new file mode 100644
index 00000000..87ea7d94
--- /dev/null
+++ b/compose-customisable-ui-example/app/src/main/java/io/scanbot/example/compose/BarcodeScannerDistantScan.kt
@@ -0,0 +1,203 @@
+package io.scanbot.example.compose
+
+import android.util.Log
+import androidx.annotation.OptIn
+import androidx.camera.camera2.interop.ExperimentalCamera2Interop
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.PaddingValues
+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.layout.systemBarsPadding
+import androidx.compose.material.Icon
+import androidx.compose.material.TopAppBar
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.automirrored.filled.ArrowBack
+import androidx.compose.material3.Button
+import androidx.compose.material3.IconButton
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Scaffold
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.mutableFloatStateOf
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.text.style.TextAlign
+import androidx.compose.ui.unit.dp
+import androidx.navigation.NavHostController
+import androidx.navigation.NavOptions
+import io.scanbot.common.*
+import io.scanbot.demo.composeui.ui.theme.sbBrandColor
+import io.scanbot.example.compose.components.*
+import io.scanbot.sdk.geometry.*
+import io.scanbot.sdk.ui_v2.barcode.*
+import io.scanbot.sdk.ui_v2.common.*
+import io.scanbot.sdk.ui_v2.common.components.*
+import kotlin.random.*
+
+@OptIn(ExperimentalCamera2Interop::class)
+@Composable
+fun BarcodeScannerDistantScan(navController: NavHostController) {
+ // Use these states to control camera, torch and zoom
+
+ // THIS IS IMPORTANT FOR DISTANT SCAN USECASE
+ val zoom = remember { mutableFloatStateOf(20.0f) }
+ val torchEnabled = remember { mutableStateOf(false) }
+ val cameraEnabled = remember { mutableStateOf(true) }
+
+ // Unused in this example, but you may use it to
+ // enable/disable barcode scanning dynamically
+ val barcodeScanningEnabled = remember { mutableStateOf(true) }
+
+ Scaffold(
+ modifier = Modifier.systemBarsPadding(),
+ topBar = {
+ TopAppBar(
+ title = {
+ Text(
+ text = "Distant Barcode Scan",
+ style = MaterialTheme.typography.titleLarge,
+ color = Color.White
+ )
+ },
+ backgroundColor = sbBrandColor,
+ navigationIcon = {
+ IconButton(onClick = {
+ navController.popBackStack()
+ }) {
+ Icon(
+ tint = Color.White,
+ imageVector = Icons.AutoMirrored.Filled.ArrowBack,
+ contentDescription = ""
+ )
+ }
+ })
+ },
+ content = { paddingValues ->
+ Column(
+ modifier = Modifier
+ .fillMaxSize()
+ .padding(paddingValues)
+ ) {
+ // @Tag("Scanning distant barcodes")
+ BarcodeScannerCustomUI(
+ // Modify Size here:
+ modifier = Modifier
+ .fillMaxWidth()
+ .weight(1.0f),
+ finderConfiguration = FinderConfiguration(
+ verticalAlignment = Alignment.Top,
+ previewInsets = PaddingValues(
+ top = 32.dp,
+ bottom = 32.dp,
+ start = 16.dp,
+ end = 16.dp
+ ),
+ // Modify aspect ratio of the viewfinder here:
+ aspectRatio = AspectRatio(1.0, 1.0),
+ // Change viewfinder overlay color here:
+ overlayColor = Color.Transparent,
+ // Change viewfinder stroke color here:
+ strokeColor = Color.Transparent,
+
+ // Alternatively, it is possible to provide a completely custom viewfinder content:
+ finderContent = {
+ // Custom cornered viewfinder. Can be replaced with any custom Composable
+ CorneredFinder()
+ },
+ topContent = {
+ androidx.compose.material.Text(
+ "Custom Top Content",
+ color = Color.White,
+ textAlign = TextAlign.Center,
+ modifier = Modifier
+ .fillMaxWidth()
+ )
+ },
+ bottomContent = {
+ // You may add custom buttons and other elements here:
+ androidx.compose.material.Text(
+ "Custom Bottom Content",
+ color = Color.White,
+ textAlign = TextAlign.Center,
+ modifier = Modifier
+ .fillMaxWidth()
+ )
+ }
+ ),
+ cameraEnabled = cameraEnabled.value,
+ barcodeScanningEnabled = barcodeScanningEnabled.value,
+ torchEnabled = torchEnabled.value,
+ zoomLevel = zoom.floatValue,
+ permissionView = {
+ // View that will be shown while camera permission is not granted
+ Box(modifier = Modifier.fillMaxSize()) {
+ Text(
+ "Camera permission is required to scan barcodes.",
+ modifier = Modifier.padding(16.dp),
+ color = Color.White
+ )
+ }
+ },
+ arPolygonView = { barcodesFlow ->
+ // Configure AR overlay polygon appearance inside CustomBarcodesArView if needed
+ CustomBarcodesArView(
+ barcodesFlow = barcodesFlow,
+ onBarcodeClick = {
+ // Handle barcode click on barcode from AR overlay if needed
+ }
+ )
+ },
+ onBarcodeScanningResult = { result ->
+ result.onSuccess { data ->
+ // Navigate to detail screen for the first barcode
+ val firstBarcode = data.barcodes.firstOrNull()
+ if (firstBarcode != null) {
+ navController.navigate(
+ Screen.BarcodeDetail.createRoute(
+ firstBarcode.text,
+ firstBarcode.format.name
+ ),
+ navOptions = NavOptions.Builder().setLaunchSingleTop(true)
+ .build()
+ )
+ }
+ }.onFailure { error ->
+ Log.e(
+ "BarcodeScannerScreen3",
+ "Barcode scanning error: ${error.message}",
+ error
+ )
+ }
+ },
+ )
+ // @EndTag("Scanning distant barcodes")
+ Row {
+ androidx.compose.material.Button(modifier = Modifier.weight(1f), onClick = {
+ zoom.floatValue = 1.0f + Random.nextFloat()
+ }) {
+ Text("Zoom")
+ }
+
+ Button(modifier = Modifier.weight(1f), onClick = {
+ torchEnabled.value = !torchEnabled.value
+ }) {
+ Text("Flash")
+ }
+
+ Button(modifier = Modifier.weight(1f), onClick = {
+ cameraEnabled.value = !cameraEnabled.value
+ }) {
+ Text("Visibility")
+ }
+ }
+
+ }
+ }
+ )
+}
\ No newline at end of file
diff --git a/compose-customisable-ui-example/app/src/main/java/io/scanbot/example/compose/BarcodeScannerMicroScan.kt b/compose-customisable-ui-example/app/src/main/java/io/scanbot/example/compose/BarcodeScannerMicroScan.kt
new file mode 100644
index 00000000..ad32f549
--- /dev/null
+++ b/compose-customisable-ui-example/app/src/main/java/io/scanbot/example/compose/BarcodeScannerMicroScan.kt
@@ -0,0 +1,204 @@
+package io.scanbot.example.compose
+
+import android.util.Log
+import androidx.annotation.OptIn
+import androidx.camera.camera2.interop.ExperimentalCamera2Interop
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.PaddingValues
+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.layout.systemBarsPadding
+import androidx.compose.material.Icon
+import androidx.compose.material.TopAppBar
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.automirrored.filled.ArrowBack
+import androidx.compose.material3.Button
+import androidx.compose.material3.IconButton
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Scaffold
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.mutableFloatStateOf
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.text.style.TextAlign
+import androidx.compose.ui.unit.dp
+import androidx.navigation.NavHostController
+import androidx.navigation.NavOptions
+import io.scanbot.common.*
+import io.scanbot.demo.composeui.ui.theme.sbBrandColor
+import io.scanbot.example.compose.components.*
+import io.scanbot.sdk.geometry.*
+import io.scanbot.sdk.ui_v2.barcode.*
+import io.scanbot.sdk.ui_v2.common.*
+import io.scanbot.sdk.ui_v2.common.components.*
+import kotlin.random.*
+
+@OptIn(ExperimentalCamera2Interop::class)
+@Composable
+fun BarcodeScannerMicroScan(navController: NavHostController) {
+
+ // Use these states to control camera, torch and zoom
+ val zoom = remember { mutableFloatStateOf(1.0f) }
+ val torchEnabled = remember { mutableStateOf(false) }
+ val cameraEnabled = remember { mutableStateOf(true) }
+
+ // Unused in this example, but you may use it to
+ // enable/disable barcode scanning dynamically
+ val barcodeScanningEnabled = remember { mutableStateOf(true) }
+
+ Scaffold(
+ modifier = Modifier.systemBarsPadding(),
+ topBar = {
+ TopAppBar(
+ title = {
+ Text(
+ text = "Micro Barcode Scan",
+ style = MaterialTheme.typography.titleLarge,
+ color = Color.White
+ )
+ },
+ backgroundColor = sbBrandColor,
+ navigationIcon = {
+ IconButton(onClick = {
+ navController.popBackStack()
+ }) {
+ Icon(
+ tint = Color.White,
+ imageVector = Icons.AutoMirrored.Filled.ArrowBack,
+ contentDescription = ""
+ )
+ }
+ })
+ },
+ content = { paddingValues ->
+ Column(
+ modifier = Modifier
+ .fillMaxSize()
+ .padding(paddingValues)
+ ) {
+ // @Tag("Scanning tiny barcodes")
+ BarcodeScannerCustomUI(
+ // Modify Size here:
+ modifier = Modifier
+ .fillMaxWidth()
+ .weight(1.0f),
+ // THIS IS IMPORTANT FOR MICR0 SCAN USECASE
+ minFocusDistanceLock = true,
+ finderConfiguration = FinderConfiguration(
+ verticalAlignment = Alignment.Top,
+ previewInsets = PaddingValues(
+ top = 32.dp,
+ bottom = 32.dp,
+ start = 16.dp,
+ end = 16.dp
+ ),
+ // Modify aspect ratio of the viewfinder here:
+ aspectRatio = AspectRatio(1.0, 1.0),
+ // Change viewfinder overlay color here:
+ overlayColor = Color.Transparent,
+ // Change viewfinder stroke color here:
+ strokeColor = Color.Transparent,
+
+ // Alternatively, it is possible to provide a completely custom viewfinder content:
+ finderContent = {
+ // Custom cornered viewfinder. Can be replaced with any custom Composable
+ CorneredFinder()
+ },
+ topContent = {
+ androidx.compose.material.Text(
+ "Custom Top Content",
+ color = Color.White,
+ textAlign = TextAlign.Center,
+ modifier = Modifier
+ .fillMaxWidth()
+ )
+ },
+ bottomContent = {
+ // You may add custom buttons and other elements here:
+ androidx.compose.material.Text(
+ "Custom Bottom Content",
+ color = Color.White,
+ textAlign = TextAlign.Center,
+ modifier = Modifier
+ .fillMaxWidth()
+ )
+ }
+ ),
+ cameraEnabled = cameraEnabled.value,
+ barcodeScanningEnabled = barcodeScanningEnabled.value,
+ torchEnabled = torchEnabled.value,
+ zoomLevel = zoom.floatValue,
+ permissionView = {
+ // View that will be shown while camera permission is not granted
+ Box(modifier = Modifier.fillMaxSize()) {
+ Text(
+ "Camera permission is required to scan barcodes.",
+ modifier = Modifier.padding(16.dp),
+ color = Color.White
+ )
+ }
+ },
+ arPolygonView = { barcodesFlow ->
+ // Configure AR overlay polygon appearance inside CustomBarcodesArView if needed
+ CustomBarcodesArView(
+ barcodesFlow = barcodesFlow,
+ onBarcodeClick = {
+ // Handle barcode click on barcode from AR overlay if needed
+ }
+ )
+ },
+ onBarcodeScanningResult = { result ->
+ result.onSuccess { data ->
+ // Navigate to detail screen for the first barcode
+ val firstBarcode = data.barcodes.firstOrNull()
+ if (firstBarcode != null) {
+ navController.navigate(
+ Screen.BarcodeDetail.createRoute(
+ firstBarcode.text,
+ firstBarcode.format.name
+ ),
+ navOptions = NavOptions.Builder().setLaunchSingleTop(true)
+ .build()
+ )
+ }
+ }.onFailure { error ->
+ Log.e(
+ "BarcodeScannerScreen3",
+ "Barcode scanning error: ${error.message}",
+ error
+ )
+ }
+ },
+ )
+ // @EndTag("Scanning tiny barcodes")
+ Row {
+ androidx.compose.material.Button(modifier = Modifier.weight(1f), onClick = {
+ zoom.floatValue = 1.0f + Random.nextFloat()
+ }) {
+ Text("Zoom")
+ }
+
+ Button(modifier = Modifier.weight(1f), onClick = {
+ torchEnabled.value = !torchEnabled.value
+ }) {
+ Text("Flash")
+ }
+
+ Button(modifier = Modifier.weight(1f), onClick = {
+ cameraEnabled.value = !cameraEnabled.value
+ }) {
+ Text("Visibility")
+ }
+ }
+
+ }
+ }
+ )
+}
\ No newline at end of file
diff --git a/compose-customisable-ui-example/app/src/main/java/io/scanbot/example/compose/BarcodeScannerMultiScan.kt b/compose-customisable-ui-example/app/src/main/java/io/scanbot/example/compose/BarcodeScannerMultiScan.kt
new file mode 100644
index 00000000..e366e9b3
--- /dev/null
+++ b/compose-customisable-ui-example/app/src/main/java/io/scanbot/example/compose/BarcodeScannerMultiScan.kt
@@ -0,0 +1,152 @@
+package io.scanbot.example.compose
+
+import android.util.Log
+import androidx.annotation.OptIn
+import androidx.camera.camera2.interop.ExperimentalCamera2Interop
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.systemBarsPadding
+import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.foundation.lazy.items
+import androidx.compose.material.Icon
+import androidx.compose.material.TopAppBar
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.automirrored.filled.ArrowBack
+import androidx.compose.material3.IconButton
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Scaffold
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.mutableFloatStateOf
+import androidx.compose.runtime.mutableStateListOf
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.rememberCoroutineScope
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.unit.dp
+import androidx.navigation.NavHostController
+import io.scanbot.common.*
+import io.scanbot.demo.composeui.ui.theme.*
+import io.scanbot.example.compose.components.BarcodeItem
+import io.scanbot.sdk.barcode.*
+import io.scanbot.sdk.ui_v2.barcode.*
+import io.scanbot.sdk.util.snap.*
+import kotlinx.coroutines.launch
+
+@OptIn(ExperimentalCamera2Interop::class)
+@Composable
+fun BarcodeScannerMultiScan(navController: NavHostController) {
+ val zoom = remember { mutableFloatStateOf(1.0f) }
+ val torchEnabled = remember { mutableStateOf(false) }
+ val cameraEnabled = remember { mutableStateOf(true) }
+ val barcodeScanningEnabled = remember { mutableStateOf(true) }
+ val scannedBarcodes = remember { mutableStateListOf() }
+ val context = LocalContext.current
+ val soundController = remember {
+ SoundControllerImpl(context).apply {
+ // Prepare Scanbot beep sound controller:
+ setUp()
+ setVibrationEnabled(true)
+ setBleepEnabled(true)
+ }
+ }
+ val scope = rememberCoroutineScope()
+ Scaffold(
+ modifier = Modifier.systemBarsPadding(),
+ topBar = {
+ TopAppBar(
+ title = {
+ Text(
+ text = "Multiple Barcodes Scan",
+ style = MaterialTheme.typography.titleLarge,
+ color = Color.White
+ )
+ },
+ backgroundColor = sbBrandColor,
+ navigationIcon = {
+ IconButton(onClick = {
+ navController.popBackStack()
+ }) {
+ Icon(
+ tint = Color.White,
+ imageVector = Icons.AutoMirrored.Filled.ArrowBack,
+ contentDescription = ""
+ )
+ }
+ })
+ },
+ content = { paddingValues ->
+ Column(
+ modifier = Modifier
+ .fillMaxSize()
+ .padding(paddingValues)
+ ) {
+ // @Tag("Scanning multiple barcodes")
+ BarcodeScannerCustomUI(
+ modifier = Modifier.weight(1f),
+ cameraEnabled = cameraEnabled.value,
+ barcodeScanningEnabled = barcodeScanningEnabled.value,
+ torchEnabled = torchEnabled.value,
+ zoomLevel = zoom.floatValue,
+ finderConfiguration = null,
+ arPolygonView = { dataFlow ->
+ CustomBarcodesArView(dataFlow, { barcode ->
+ if (scannedBarcodes.none { it.textWithExtension == barcode.textWithExtension }) {
+ scannedBarcodes.add(barcode)
+ } else {
+ scannedBarcodes.removeAll { it.textWithExtension == barcode.textWithExtension }
+ }
+ }, onShouldHighlight = { barcode ->
+ scannedBarcodes.any {
+ it.textWithExtension == barcode.textWithExtension
+ }
+ })
+ },
+ permissionView = {
+ Box(modifier = Modifier.fillMaxSize()) {
+ Text(
+ "Camera permission is required to scan barcodes.",
+ modifier = Modifier.padding(16.dp),
+ color = Color.White
+ )
+ }
+ },
+ onBarcodeScanningResult = { result ->
+ result.onSuccess { result ->
+ result.barcodes.forEach { data ->
+ if (scannedBarcodes.none { it.text == data.text && it.format == data.format }) {
+ scope.launch {
+ // Provide sound and vibration feedback on scan:
+ soundController.playBleepSound()
+ }
+ scannedBarcodes.add(data)
+ }
+ }
+ }.onFailure { error ->
+ Log.e(
+ "BarcodeScannerScreen2",
+ "Barcode scanning error: ${error.message}",
+ error
+ )
+ }
+ }
+ )
+ // @EndTag("Scanning multiple barcodes")
+ LazyColumn(
+ modifier = Modifier
+ .fillMaxWidth()
+ .weight(1f)
+ .padding(8.dp),
+ ) {
+ items(scannedBarcodes.reversed()) { barcode ->
+ BarcodeItem(barcode)
+ }
+ }
+ }
+ })
+}
diff --git a/compose-customisable-ui-example/app/src/main/java/io/scanbot/example/compose/BarcodeScannerSingleScan.kt b/compose-customisable-ui-example/app/src/main/java/io/scanbot/example/compose/BarcodeScannerSingleScan.kt
new file mode 100644
index 00000000..f25b6f20
--- /dev/null
+++ b/compose-customisable-ui-example/app/src/main/java/io/scanbot/example/compose/BarcodeScannerSingleScan.kt
@@ -0,0 +1,202 @@
+package io.scanbot.example.compose
+
+import android.util.Log
+import androidx.annotation.OptIn
+import androidx.camera.camera2.interop.ExperimentalCamera2Interop
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.PaddingValues
+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.layout.systemBarsPadding
+import androidx.compose.material.Icon
+import androidx.compose.material.TopAppBar
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.automirrored.filled.ArrowBack
+import androidx.compose.material3.Button
+import androidx.compose.material3.IconButton
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Scaffold
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.mutableFloatStateOf
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.text.style.TextAlign
+import androidx.compose.ui.unit.dp
+import androidx.navigation.NavHostController
+import androidx.navigation.NavOptions
+import io.scanbot.common.*
+import io.scanbot.demo.composeui.ui.theme.sbBrandColor
+import io.scanbot.example.compose.components.*
+import io.scanbot.sdk.geometry.*
+import io.scanbot.sdk.ui_v2.barcode.*
+import io.scanbot.sdk.ui_v2.common.*
+import io.scanbot.sdk.ui_v2.common.components.*
+import kotlin.random.*
+
+@OptIn(ExperimentalCamera2Interop::class)
+@Composable
+fun BarcodeScannerSingleScan(navController: NavHostController) {
+ // Use these states to control camera, torch and zoom
+ val zoom = remember { mutableFloatStateOf(1.0f) }
+ val torchEnabled = remember { mutableStateOf(false) }
+ val cameraEnabled = remember { mutableStateOf(true) }
+
+ // Unused in this example, but you may use it to
+ // enable/disable barcode scanning dynamically
+ val barcodeScanningEnabled = remember { mutableStateOf(true) }
+
+ Scaffold(
+ modifier = Modifier.systemBarsPadding(),
+ topBar = {
+ TopAppBar(
+ title = {
+ Text(
+ text = "Barcode Single Scan",
+ style = MaterialTheme.typography.titleLarge,
+ color = Color.White
+ )
+ },
+ backgroundColor = sbBrandColor,
+ navigationIcon = {
+ IconButton(onClick = {
+ navController.popBackStack()
+ }) {
+ Icon(
+ tint = Color.White,
+ imageVector = Icons.AutoMirrored.Filled.ArrowBack,
+ contentDescription = ""
+ )
+ }
+ })
+ },
+ content = { paddingValues ->
+ Column(
+ modifier = Modifier
+ .fillMaxSize()
+ .padding(paddingValues)
+ ) {
+ // @Tag("Scanning single barcode")
+ BarcodeScannerCustomUI(
+ // Modify Size here:
+ modifier = Modifier
+ .fillMaxWidth()
+ .weight(1.0f),
+ finderConfiguration = FinderConfiguration(
+ verticalAlignment = Alignment.Top,
+ previewInsets = PaddingValues(
+ top = 32.dp,
+ bottom = 32.dp,
+ start = 16.dp,
+ end = 16.dp
+ ),
+ // Modify aspect ratio of the viewfinder here:
+ aspectRatio = AspectRatio(1.0, 1.0),
+ // Change viewfinder overlay color here:
+ overlayColor = Color.Transparent,
+ // Change viewfinder stroke color here:
+ strokeColor = Color.Transparent,
+
+ // Alternatively, it is possible to provide a completely custom viewfinder content:
+ finderContent = {
+ // Custom cornered viewfinder. Can be replaced with any custom Composable
+ CorneredFinder()
+ },
+ topContent = {
+ androidx.compose.material.Text(
+ "Custom Top Content",
+ color = Color.White,
+ textAlign = TextAlign.Center,
+ modifier = Modifier
+ .fillMaxWidth()
+ )
+ },
+ bottomContent = {
+ // You may add custom buttons and other elements here:
+ androidx.compose.material.Text(
+ "Custom Bottom Content",
+ color = Color.White,
+ textAlign = TextAlign.Center,
+ modifier = Modifier
+ .fillMaxWidth()
+ )
+ }
+ ),
+ cameraEnabled = cameraEnabled.value,
+ barcodeScanningEnabled = barcodeScanningEnabled.value,
+ torchEnabled = torchEnabled.value,
+ zoomLevel = zoom.floatValue,
+ permissionView = {
+ // View that will be shown while camera permission is not granted
+ Box(modifier = Modifier.fillMaxSize()) {
+ Text(
+ "Camera permission is required to scan barcodes.",
+ modifier = Modifier.padding(16.dp),
+ color = Color.White
+ )
+ }
+ },
+ arPolygonView = { barcodesFlow ->
+ // Configure AR overlay polygon appearance inside CustomBarcodesArView if needed
+ CustomBarcodesArView(
+ barcodesFlow = barcodesFlow,
+ onBarcodeClick = {
+ // Handle barcode click on barcode from AR overlay if needed
+ }
+ )
+ },
+ onBarcodeScanningResult = { result ->
+ result.onSuccess { data ->
+ // Navigate to detail screen for the first barcode
+ val firstBarcode = data.barcodes.firstOrNull()
+ if (firstBarcode != null) {
+ navController.navigate(
+ Screen.BarcodeDetail.createRoute(
+ firstBarcode.text,
+ firstBarcode.format.name
+ ),
+ navOptions = NavOptions.Builder().setLaunchSingleTop(true)
+ .build()
+ )
+ }
+ }.onFailure { error ->
+ Log.e(
+ "BarcodeScannerScreen3",
+ "Barcode scanning error: ${error.message}",
+ error
+ )
+ }
+ },
+ )
+ // @EndTag("Scanning single barcode")
+
+ Row {
+ androidx.compose.material.Button(modifier = Modifier.weight(1f), onClick = {
+ zoom.floatValue = 1.0f + Random.nextFloat()
+ }) {
+ Text("Zoom")
+ }
+
+ Button(modifier = Modifier.weight(1f), onClick = {
+ torchEnabled.value = !torchEnabled.value
+ }) {
+ Text("Flash")
+ }
+
+ Button(modifier = Modifier.weight(1f), onClick = {
+ cameraEnabled.value = !cameraEnabled.value
+ }) {
+ Text("Visibility")
+ }
+ }
+
+ }
+ }
+ )
+}
\ No newline at end of file
diff --git a/compose-customisable-ui-example/app/src/main/java/io/scanbot/example/compose/CustomBarcodesArView.kt b/compose-customisable-ui-example/app/src/main/java/io/scanbot/example/compose/CustomBarcodesArView.kt
new file mode 100644
index 00000000..810e4707
--- /dev/null
+++ b/compose-customisable-ui-example/app/src/main/java/io/scanbot/example/compose/CustomBarcodesArView.kt
@@ -0,0 +1,86 @@
+package io.scanbot.example.compose
+
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.padding
+import androidx.compose.material.MaterialTheme
+import androidx.compose.material.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.geometry.Rect
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.layout.layout
+import androidx.compose.ui.platform.LocalDensity
+import androidx.compose.ui.unit.Density
+import androidx.compose.ui.unit.dp
+import io.scanbot.sdk.barcode.BarcodeItem
+import io.scanbot.sdk.barcode.BarcodeScannerResult
+import io.scanbot.sdk.barcode.textWithExtension
+import io.scanbot.sdk.camera.FrameHandler
+import io.scanbot.sdk.ui_v2.barcode.components.ar_tracking.ScanbotBarcodesArOverlay
+import kotlinx.coroutines.flow.SharedFlow
+
+@Composable
+fun CustomBarcodesArView(
+ barcodesFlow: SharedFlow?>,
+ onBarcodeClick: (BarcodeItem) -> Unit = {},
+ onShouldHighlight: (BarcodeItem) -> Boolean = { false },
+) {
+ val density = LocalDensity.current
+
+ ScanbotBarcodesArOverlay(
+ barcodesFlow,
+ getData = { barcodeItem -> barcodeItem.textWithExtension },
+ getPolygonStyle = { defaultStyle, barcodeItem ->
+ // Customize polygon style here.
+ // You may use barcodeItem to apply different styles for different barcode types, etc.
+ defaultStyle.copy(
+ drawPolygon = true,
+ useFill = true,
+ useFillHighlighted = true,
+ cornerRadius = density.run { 20.dp.toPx() },
+ cornerHighlightedRadius = density.run { 20.dp.toPx() },
+ strokeWidth = density.run { 5.dp.toPx() },
+ strokeHighlightedWidth = density.run { 5.dp.toPx() },
+ strokeColor = Color.Green,
+ strokeHighlightedColor = Color.Red,
+ fillColor = Color.Green.copy(alpha = 0.3f),
+ fillHighlightedColor = Color.Red.copy(alpha = 0.3f),
+ shouldDrawShadows = false
+ )
+ },
+ shouldHighlight = { barcodeItem ->
+ // Here you can implement any custom logic.
+ // Return true to highlight a barcode with different style.
+ onShouldHighlight(barcodeItem)
+ },
+ // Customize AR view for barcode data here if needed
+ view = { path, barcodeItem, data, shouldHighlight ->
+ // Implement custom view for barcode polygon if needed
+ Box(modifier = Modifier.layout { measurable, constraints ->
+ val placeable = measurable.measure(constraints);
+
+ var rectF: Rect
+ path.getBounds().also { rectF = it }
+
+ val width = placeable.width
+ val height = placeable.height
+ val x = rectF.center.x - width / 2
+ val y = rectF.center.y + rectF.height / 2 + 10.dp.toPx() // place below the polygon
+ layout(width, height) {
+ placeable.placeRelative(x.toInt(), y.toInt())
+ }
+ }) {
+ Text(
+ text = data,
+ color = if (shouldHighlight) Color.Red else Color.Green,
+ style = MaterialTheme.typography.body2,
+ modifier = Modifier
+ .background(Color.Black.copy(alpha = 0.5f))
+ .padding(4.dp)
+ )
+ }
+ },
+ onClick = onBarcodeClick,
+ )
+}
diff --git a/compose-customisable-ui-example/app/src/main/java/io/scanbot/example/compose/DocumentTestScreens.kt b/compose-customisable-ui-example/app/src/main/java/io/scanbot/example/compose/DocumentTestScreens.kt
new file mode 100644
index 00000000..7ce3d679
--- /dev/null
+++ b/compose-customisable-ui-example/app/src/main/java/io/scanbot/example/compose/DocumentTestScreens.kt
@@ -0,0 +1,279 @@
+package io.scanbot.example.compose
+
+import android.graphics.Bitmap
+import android.util.Log
+import androidx.annotation.OptIn
+import androidx.camera.camera2.interop.ExperimentalCamera2Interop
+import androidx.compose.foundation.layout.Box
+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.height
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.layout.systemBarsPadding
+import androidx.compose.material3.Button
+import androidx.compose.material3.Surface
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.MutableState
+import androidx.compose.runtime.mutableFloatStateOf
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.rememberCoroutineScope
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.platform.LocalDensity
+import androidx.compose.ui.unit.dp
+import androidx.navigation.NavHostController
+import coil.compose.AsyncImage
+import io.scanbot.common.onFailure
+import io.scanbot.common.onSuccess
+import io.scanbot.sdk.ScanbotSDK
+import io.scanbot.sdk.documentscanner.DocumentDetectionStatus
+import io.scanbot.sdk.documentscanner.DocumentScannerConfiguration
+import io.scanbot.sdk.documentscanner.DocumentScannerParameters
+import io.scanbot.sdk.geometry.AspectRatio
+import io.scanbot.sdk.imagemanipulation.ScanbotSdkImageManipulator
+import io.scanbot.sdk.imageprocessing.ScanbotSdkImageProcessor
+import io.scanbot.sdk.ui_v2.common.CameraPermissionScreen
+import io.scanbot.sdk.ui_v2.common.camera.TakePictureActionController
+import io.scanbot.sdk.ui_v2.common.components.FinderConfiguration
+import io.scanbot.sdk.ui_v2.common.components.ScanbotCameraPermissionView
+import io.scanbot.sdk.ui_v2.common.components.ScanbotSnapButton
+import io.scanbot.sdk.ui_v2.document.DocumentScannerCustomUI
+import io.scanbot.sdk.ui_v2.document.components.camera.ScanbotDocumentArOverlay
+import io.scanbot.sdk.ui_v2.document.screen.AutoSnappingConfiguration
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.launch
+import kotlin.random.Random
+
+@Composable
+@OptIn(ExperimentalCamera2Interop::class)
+fun DocumentScannerScreen1(navController: NavHostController) {
+ val density = LocalDensity.current
+ Column(modifier = Modifier.systemBarsPadding()) {
+ val context = LocalContext.current
+ val sdk = remember { ScanbotSDK(context) }
+ val imageProcessor = remember { ScanbotSdkImageProcessor.create() }
+ val documentScanner = remember { sdk.createDocumentScanner().getOrNull() }
+ // Use these states to control camera, torch and zoom
+ val zoom = remember { mutableFloatStateOf(1.0f) }
+ val torchEnabled = remember { mutableStateOf(false) }
+ val cameraEnabled = remember { mutableStateOf(true) }
+ val scope = rememberCoroutineScope()
+ // Unused in this example, but you may use it to
+ // enable/disable barcode scanning dynamically
+ val scanningEnabled = remember { mutableStateOf(true) }
+ val autosnappingEnabled = remember { mutableStateOf(true) }
+ val cameraInProcessingState = remember { mutableStateOf(false) }
+ val scannedImage = remember { mutableStateOf(null) }
+ val takePictureActionController =
+ remember { mutableStateOf(null) }
+ val documentScanningStatus =
+ remember { mutableStateOf(DocumentDetectionStatus.ERROR_NOTHING_DETECTED) }
+ Box(
+ modifier = Modifier
+ .fillMaxWidth()
+ .weight(1.0f),
+ ) {
+ DocumentScannerCustomUI(
+ // Modify Size here:
+ modifier = Modifier.fillMaxSize(),
+ cameraEnabled = cameraEnabled.value,
+ documentScanningEnabled = scanningEnabled.value,
+ autoSnappingConfiguration = AutoSnappingConfiguration(
+ enabled = autosnappingEnabled.value
+ ),
+ torchEnabled = torchEnabled.value,
+ zoomLevel = zoom.floatValue,
+ documentScannerConfiguration = DocumentScannerConfiguration(
+ parameters = DocumentScannerParameters(
+ ignoreOrientationMismatch = true
+ )
+ ),
+ /* finderConfiguration = FinderConfiguration(
+ //strokeColor = Color.Cyan,
+ verticalAlignment = Alignment.Top,
+ aspectRatio = AspectRatio(
+ 21.0,
+ 29.0
+ ) // Use default aspect ratio matching document size
+ ),*/
+ permissionView = {
+ // View that will be shown while camera permission is not granted
+ ScanbotCameraPermissionView(
+ modifier = Modifier.fillMaxSize(),
+ bottomContentPadding = 0.dp,
+ permissionConfig = CameraPermissionScreen(),
+ onClose = {
+ // Handle permission screen close if needed
+ })
+ },
+ arPolygonView = { dataFlow ->
+ ScanbotDocumentArOverlay(
+ dataFlow = dataFlow,
+ getProgressPolygonStyle = { defaultStyle ->
+ // Customize polygon style if needed
+ defaultStyle.copy(strokeWidth = 8f, strokeColor = Color.Green)
+ })
+ },
+ onTakePictureCalled = {
+ Log.d("DocumentScannerScreen1", "Take picture called")
+ cameraInProcessingState.value = true
+ },
+ onTakePictureCanceled = {
+ Log.d("DocumentScannerScreen1", "Take picture canceled")
+ cameraInProcessingState.value = false
+ },
+ onPictureSnapped = { imageRef, captureInfo ->
+ // WARNING: move all processing operation to view model with proper coroutine scope in real apps to avoid data loss during recompositions
+ scope.launch(Dispatchers.Default) {
+ // See https://docs.scanbot.io/android/data-capture-modules/detailed-setup-guide/result-api/ for details of result handling
+ // run detection and cropping on the captured image
+ documentScanner?.run(imageRef)?.onSuccess { documentData ->
+ val croppedImage =
+ imageProcessor.crop(imageRef, documentData.pointsNormalized)
+ .getOrReturn() // get the result of cropping operation or leave onSuccess if cropping failed
+ imageRef.close() // clear image ref resources
+ scannedImage.value =
+ imageProcessor.resize(croppedImage, 300).getOrReturn().toBitmap()
+ .getOrReturn() // get the result of cropping operation or leave onSuccess if cropping failed
+ croppedImage.close() // clear image ref resources
+ }?.onFailure { error ->
+ Log.e(
+ "DocumentScannerScreen",
+ "Document scanning error: ${error.message}"
+ )
+ }
+ delay(1000)
+ cameraInProcessingState.value =
+ false // Picture is received, allow auto-snapping again or proceed further and allow image snap after some additional processing
+ }
+ },
+ onTakePictureControllerCreated = {
+ takePictureActionController.value = it
+ },
+ onAutoSnapping = {
+ // return true if auto-snapping should be consumed and not proceed to take picture
+ cameraInProcessingState.value // Disable auto-snapping while awaiting picture result after snap is triggered
+ },
+ onDocumentScanningResult = { result ->
+ // Update document scanning status to show feedback in the UI if needed
+ documentScanningStatus.value =
+ result.getOrNull()?.status ?: DocumentDetectionStatus.ERROR_NOTHING_DETECTED
+ result.onSuccess { data ->
+ Log.d(
+ "BarcodeComposeClassic",
+ "Scanned polygon: ${
+ data.pointsNormalized.map {
+ with(density) {
+ "(${it.x.toDp().value.toInt()}, ${it.y.toDp().value.toInt()})"
+ }
+ }
+ }",
+ )
+ }
+
+ },
+ )
+ ScanbotSnapButton(
+ modifier = Modifier
+ .height(100.dp)
+ .align(Alignment.BottomCenter),
+ // Disable button when scanning or auto-snapping is disabled
+ clickable = scanningEnabled.value && !cameraInProcessingState.value,
+ // Show indicator when camera is processing the last taken picture
+ autoCapture = autosnappingEnabled.value,
+ // animate progress when camera is processing the last taken picture
+ animateProgress = cameraInProcessingState.value,
+ // rotation speed of the outer big arc in auto-capture mode
+ bigArcSpeed = 3000,
+ // speed of the progress arc during processing
+ progressSpeed = 500,
+ // color of buttons inner component
+ innerColor = Color.Red,
+ // outer color of buttons outer component
+ outerColor = Color.White,
+ // outer circle line width
+ lineWidth = 1.dp,
+ // size of the empty space between inner and outer components
+ emptyLineWidth = 10.dp,
+ // initial angle of the 360 degrees rotating big arc
+ bigArcInitialAngle = 200f
+ ) {
+ takePictureActionController.value?.invoke()
+ }
+
+ Box(
+ modifier = Modifier
+ .align(Alignment.Center)
+ ) {
+ Surface(color = Color.Green.copy(alpha = 0.3f)) {
+ Text(
+ text = instructionString(documentScanningStatus.value),
+ color = Color.White
+ )
+ }
+ }
+
+ if (scannedImage.value != null) {
+ AsyncImage(
+ model = scannedImage.value,
+ contentDescription = "ScannedImage",
+ modifier = Modifier
+ .height(100.dp)
+ .align(Alignment.BottomEnd)
+ )
+ }
+ }
+ Column() {
+ Row {
+ androidx.compose.material.Button(modifier = Modifier.weight(1f), onClick = {
+ zoom.floatValue = 1.0f + Random.nextFloat()
+ }) {
+ Text("Zoom")
+ }
+
+ Button(modifier = Modifier.weight(1f), onClick = {
+ torchEnabled.value = !torchEnabled.value
+ }) {
+ Text("Flash")
+ }
+
+ Button(modifier = Modifier.weight(1f), onClick = {
+ cameraEnabled.value = !cameraEnabled.value
+ }) {
+ Text("Visibility")
+ }
+ }
+ Row {
+ androidx.compose.material.Button(modifier = Modifier.weight(1f), onClick = {
+ autosnappingEnabled.value = !autosnappingEnabled.value
+ }) {
+ Text("Autosnapping: ${autosnappingEnabled.value}")
+ }
+ }
+ }
+ }
+}
+
+@Composable
+private fun instructionString(status: DocumentDetectionStatus): String = when (status) {
+ DocumentDetectionStatus.NOT_ACQUIRED -> "Point the camera at a document"
+ DocumentDetectionStatus.OK -> "Hold still..."
+ DocumentDetectionStatus.OK_BUT_TOO_SMALL -> "Please move closer"
+ DocumentDetectionStatus.OK_BUT_BAD_ANGLES -> "Please align document with the preview edges"
+ DocumentDetectionStatus.OK_BUT_BAD_ASPECT_RATIO -> "Document aspect ratio mismatch"
+ DocumentDetectionStatus.OK_BUT_ORIENTATION_MISMATCH -> "Please rotate the device"
+ DocumentDetectionStatus.OK_BUT_OFF_CENTER -> "Please center the document in the camera preview"
+ DocumentDetectionStatus.OK_BUT_TOO_DARK -> " Please turn on more light"
+ DocumentDetectionStatus.ERROR_NOTHING_DETECTED -> "Document not detected"
+ DocumentDetectionStatus.ERROR_PARTIALLY_VISIBLE -> "Please fit the document fully in the preview"
+ DocumentDetectionStatus.ERROR_PARTIALLY_VISIBLE_TOO_CLOSE -> "Please move the device away from the document"
+ DocumentDetectionStatus.ERROR_TOO_DARK -> "Please turn on more light"
+ DocumentDetectionStatus.ERROR_TOO_NOISY -> "Image is too noisy"
+}
diff --git a/compose-customisable-ui-example/app/src/main/java/io/scanbot/example/compose/MainActivity.kt b/compose-customisable-ui-example/app/src/main/java/io/scanbot/example/compose/MainActivity.kt
new file mode 100644
index 00000000..b9927da8
--- /dev/null
+++ b/compose-customisable-ui-example/app/src/main/java/io/scanbot/example/compose/MainActivity.kt
@@ -0,0 +1,234 @@
+package io.scanbot.example.compose
+
+import android.os.Bundle
+import androidx.activity.ComponentActivity
+import androidx.activity.compose.setContent
+import androidx.activity.enableEdgeToEdge
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.systemBarsPadding
+import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.foundation.lazy.items
+import androidx.compose.material.Divider
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Surface
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.remember
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.semantics.contentDescription
+import androidx.compose.ui.semantics.semantics
+import androidx.compose.ui.text.font.FontFamily
+import androidx.compose.ui.text.style.TextAlign
+import androidx.compose.ui.unit.dp
+import androidx.navigation.NavHostController
+import androidx.navigation.NavType
+import androidx.navigation.compose.NavHost
+import androidx.navigation.compose.composable
+import androidx.navigation.compose.rememberNavController
+import androidx.navigation.navArgument
+import io.scanbot.demo.composeui.ui.theme.ScanbotsdkandroidTheme
+import io.scanbot.demo.composeui.ui.theme.sbBrandColor
+import io.scanbot.sdk.ScanbotSDK
+import io.scanbot.sdk.licensing.LicenseStatus
+import java.net.URLDecoder
+import java.net.URLEncoder
+import java.nio.charset.StandardCharsets
+
+class MainActivity : ComponentActivity() {
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ enableEdgeToEdge()
+ setContent {
+ MainApp()
+ }
+ }
+
+ @Composable
+ fun MainApp() {
+ ScanbotsdkandroidTheme {
+ Surface(
+ modifier = Modifier.fillMaxSize(),
+ color = MaterialTheme.colorScheme.background
+ ) {
+ val navController = rememberNavController()
+ AppNavHost(navController)
+ }
+ }
+ }
+}
+
+sealed class Screen(val route: String) {
+ object Menu : Screen("menu")
+ object BarcodeScannerSingle : Screen("BarcodeScannerSingle")
+ object BarcodeScannerMulti : Screen("BarcodeScannerMulti")
+ object BarcodeScannerBatch : Screen("BarcodeScannerBatch")
+ object BarcodeScannerMicro : Screen("BarcodeScannerMicro")
+ object BarcodeScannerDistant : Screen("BarcodeScannerDistant")
+ object BarcodeFindAndPick : Screen("BarcodeFindAndPick")
+
+ object DocumentScanner1 : Screen("DocumentScanner1")
+ object MrzScanner1 : Screen("MrzScanner1")
+ data class BarcodeDetail(val data: String, val format: String) :
+ Screen("barcodeDetail/{data}/{format}") {
+ companion object {
+ fun createRoute(data: String, format: String): String {
+ val encodedData = URLEncoder.encode(data, StandardCharsets.UTF_8.toString())
+ val encodedFormat = URLEncoder.encode(format, StandardCharsets.UTF_8.toString())
+ return "barcodeDetail/$encodedData/$encodedFormat"
+ }
+ }
+ }
+}
+
+@Composable
+fun AppNavHost(navController: NavHostController) {
+ NavHost(navController = navController, startDestination = Screen.Menu.route) {
+ composable(Screen.Menu.route) { MenuScreen(navController) }
+ composable(Screen.BarcodeScannerSingle.route) { BarcodeScannerSingleScan(navController) }
+ composable(Screen.BarcodeScannerMulti.route) { BarcodeScannerMultiScan(navController) }
+ composable(Screen.BarcodeScannerBatch.route) { BarcodeScannerBatchScan(navController) }
+ composable(Screen.BarcodeScannerMicro.route) { BarcodeScannerMicroScan(navController) }
+ composable(Screen.BarcodeScannerDistant.route) { BarcodeScannerDistantScan(navController) }
+ composable(Screen.BarcodeFindAndPick.route) { BarcodeFindAndPick(navController) }
+ composable(Screen.DocumentScanner1.route) { DocumentScannerScreen1(navController) }
+ composable(Screen.MrzScanner1.route) { MrzScannerScreen1(navController) }
+ composable(
+ route = "barcodeDetail/{data}/{format}",
+ arguments = listOf(
+ navArgument("data") { type = NavType.StringType },
+ navArgument("format") { type = NavType.StringType }
+ )
+ ) { backStackEntry ->
+ val data =
+ backStackEntry.arguments?.getString("data")
+ ?.let { URLDecoder.decode(it, StandardCharsets.UTF_8.toString()) }
+ ?: ""
+ val format =
+ backStackEntry.arguments?.getString("format")
+ ?.let { URLDecoder.decode(it, StandardCharsets.UTF_8.toString()) }
+ ?: ""
+ BarcodeDetailScreen(data, format)
+ }
+ }
+}
+
+@Composable
+fun MenuScreen(navController: NavHostController) {
+ val context = navController.context
+ val scanbotSdk: ScanbotSDK = remember { ScanbotSDK(context) }
+
+ val menuItems = listOf(
+ Triple(
+ "Barcode Single Mode",
+ Screen.BarcodeScannerSingle.route,
+ "Barcode scanner with AR overlay"
+ ),
+ Triple(
+ "Barcodes Multi Mode",
+ Screen.BarcodeScannerMulti.route,
+ "Barcode multi scan mode without finder"
+ ),
+ Triple(
+ "Barcodes Batch Mode",
+ Screen.BarcodeScannerBatch.route,
+ "Barcode batch scan mode"
+ ),
+ Triple(
+ "Barcodes Micro Barcode Mode",
+ Screen.BarcodeScannerMicro.route,
+ "Barcode micro barcode scan mode"
+ ),
+ Triple(
+ "Barcodes Distant Barcode Mode",
+ Screen.BarcodeScannerDistant.route,
+ "Barcode distant barcode scan mode"
+ ), Triple(
+ "Barcodes Find and Pick Mode",
+ Screen.BarcodeFindAndPick.route,
+ "Find Specific barcode and pick it"
+ ),
+ Triple(
+ "Document Default Scanner",
+ Screen.DocumentScanner1.route,
+ ""
+ ),
+ Triple(
+ "Mrz Default Scanner",
+ Screen.MrzScanner1.route,
+ ""
+ )
+ )
+ LazyColumn(
+ modifier = Modifier
+ .fillMaxSize()
+ .padding(horizontal = 16.dp)
+ .systemBarsPadding(),
+ verticalArrangement = Arrangement.Top,
+ horizontalAlignment = Alignment.Start
+ ) {
+ item() {
+ Text(
+ "Scanbot SDK Compose Customisable UI Demo",
+ style = MaterialTheme.typography.headlineMedium,
+ modifier = Modifier.padding(bottom = 24.dp)
+ )
+ }
+ if (scanbotSdk.licenseInfo.status != LicenseStatus.OKAY) {
+ item() {
+ Surface(
+ modifier = Modifier.padding(bottom = 24.dp),
+ color = sbBrandColor,
+ shape = MaterialTheme.shapes.medium
+ ) {
+ Text(
+ "Warning: Scanbot SDK License is not valid! Current status: ${scanbotSdk.licenseInfo.status}",
+ style = MaterialTheme.typography.titleLarge.copy(color = Color.White),
+ modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp)
+ )
+ }
+ }
+ }
+
+ item() {
+ Text(
+ "Scanners Examples".uppercase(),
+ textAlign = TextAlign.Start,
+ style = MaterialTheme.typography.titleLarge.copy(fontFamily = FontFamily.Monospace),
+ modifier = Modifier.padding(bottom = 16.dp)
+ )
+ }
+ items(menuItems) { (title, route, description) ->
+ Box(
+ modifier = Modifier
+ .fillMaxWidth()
+ .clickable { navController.navigate(route) }
+ .semantics {
+ this.contentDescription = description
+ },
+ contentAlignment = Alignment.CenterStart
+ ) {
+ Text(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(vertical = 24.dp),
+ text = title,
+ style = MaterialTheme.typography.titleMedium
+ )
+ Divider(
+ Modifier
+ .fillMaxWidth()
+ .align(Alignment.BottomCenter),
+ color = Color.LightGray.copy(alpha = 0.3f)
+ )
+ }
+ }
+ }
+}
+
diff --git a/compose-customisable-ui-example/app/src/main/java/io/scanbot/example/compose/MrzTestScreens.kt b/compose-customisable-ui-example/app/src/main/java/io/scanbot/example/compose/MrzTestScreens.kt
new file mode 100644
index 00000000..3b246b01
--- /dev/null
+++ b/compose-customisable-ui-example/app/src/main/java/io/scanbot/example/compose/MrzTestScreens.kt
@@ -0,0 +1,135 @@
+package io.scanbot.example.compose
+
+import android.util.Log
+import androidx.annotation.OptIn
+import androidx.camera.camera2.interop.ExperimentalCamera2Interop
+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.systemBarsPadding
+import androidx.compose.material3.Button
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.mutableFloatStateOf
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.unit.dp
+import androidx.navigation.NavHostController
+import io.scanbot.common.onSuccess
+import io.scanbot.sdk.ui_v2.common.CameraPermissionScreen
+import io.scanbot.sdk.ui_v2.common.components.ScanbotCameraPermissionView
+import io.scanbot.sdk.ui_v2.mrz.MrzScannerCustomUI
+import kotlin.random.Random
+
+@OptIn(ExperimentalCamera2Interop::class)
+@Composable
+fun MrzScannerScreen1(navController: NavHostController) {
+
+ Column(modifier = Modifier.systemBarsPadding()) {
+ // Use these states to control camera, torch and zoom
+ val zoom = remember { mutableFloatStateOf(1.0f) }
+ val torchEnabled = remember { mutableStateOf(false) }
+ val cameraEnabled = remember { mutableStateOf(true) }
+
+ // Unused in this example, but you may use it to
+ // enable/disable barcode scanning dynamically
+ val scanningEnabled = remember { mutableStateOf(true) }
+
+ MrzScannerCustomUI(
+ // Modify Size here:
+ modifier = Modifier
+ .fillMaxWidth()
+ .weight(1.0f),
+ /* finderConfiguration = FinderConfiguration(
+ verticalAlignment = Alignment.Top,
+ // Modify aspect ratio of the viewfinder here:
+ aspectRatio = AspectRatio(adjustedMrzThreeLinedFinderAspectRatio, 1.0),
+ // Alternatively, it is possible to provide a completely custom viewfinder content:
+ finderContent = {
+ // Box with border stroke color as an example of custom viewfinder content
+ Box(
+ modifier = Modifier
+ .fillMaxSize()
+ .background(Color.Transparent)
+ // Same but with rounded corners
+ .border(
+ 4.dp,
+ Color.Cyan,
+ shape = RoundedCornerShape(
+ 16.dp
+ )
+ )
+ ) {
+
+ }
+ },
+ topContent = {
+ androidx.compose.material.Text(
+ "Custom Top Content",
+ color = Color.White,
+ textAlign = TextAlign.Center,
+ modifier = Modifier
+ .fillMaxWidth()
+ )
+ },
+ bottomContent = {
+ // You may add custom buttons and other elements here:
+ androidx.compose.material.Text(
+ "Custom Bottom Content",
+ color = Color.White,
+ textAlign = TextAlign.Center,
+ modifier = Modifier
+ .fillMaxWidth()
+ )
+ }
+ ),*/
+ cameraEnabled = cameraEnabled.value,
+ mrzScanningEnabled = scanningEnabled.value,
+ torchEnabled = torchEnabled.value,
+ zoomLevel = zoom.floatValue,
+ permissionView = {
+ // View that will be shown while camera permission is not granted
+ ScanbotCameraPermissionView(
+ modifier = Modifier.fillMaxSize(),
+ bottomContentPadding = 0.dp,
+ permissionConfig = CameraPermissionScreen(),
+ onClose = {
+ // Handle permission screen close if needed
+ })
+ },
+ onMrzScanningResult = { result ->
+ result.onSuccess { data ->
+ // Apply feedback, sound, vibration here if needed
+ // ...
+
+ // Handle scanned barcodes here (for example, show a dialog)
+ Log.d(
+ "MrzScannerScreen", "Scanned mrz: ${data.rawMRZ}"
+ )
+ }
+
+ },
+ )
+ Row {
+ androidx.compose.material.Button(modifier = Modifier.weight(1f), onClick = {
+ zoom.floatValue = 1.0f + Random.nextFloat()
+ }) {
+ Text("Zoom")
+ }
+
+ Button(modifier = Modifier.weight(1f), onClick = {
+ torchEnabled.value = !torchEnabled.value
+ }) {
+ Text("Flash")
+ }
+
+ Button(modifier = Modifier.weight(1f), onClick = {
+ cameraEnabled.value = !cameraEnabled.value
+ }) {
+ Text("Visibility")
+ }
+ }
+ }
+}
diff --git a/compose-customisable-ui-example/app/src/main/java/io/scanbot/example/compose/components/BarcodeItem.kt b/compose-customisable-ui-example/app/src/main/java/io/scanbot/example/compose/components/BarcodeItem.kt
new file mode 100644
index 00000000..214518d3
--- /dev/null
+++ b/compose-customisable-ui-example/app/src/main/java/io/scanbot/example/compose/components/BarcodeItem.kt
@@ -0,0 +1,57 @@
+package io.scanbot.example.compose.components
+
+import androidx.compose.foundation.Image
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Surface
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.asImageBitmap
+import androidx.compose.ui.layout.ContentScale
+import androidx.compose.ui.unit.dp
+import io.scanbot.sdk.barcode.BarcodeItem
+
+@Composable
+fun BarcodeItem(barcode: BarcodeItem) {
+ Surface(color = MaterialTheme.colorScheme.surface) {
+ Row() {
+ Box(
+ modifier = Modifier
+ .padding(8.dp)
+ .size(64.dp)
+ ) {
+ val image = barcode.sourceImage?.toBitmap()?.getOrNull()?.asImageBitmap()
+ image?.let {
+ Image(
+ modifier = Modifier.size(48.dp),
+ bitmap = it,
+ contentDescription = "Barcode Thumbnail",
+ contentScale = ContentScale.Inside
+ )
+ }
+ Column(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(vertical = 4.dp)
+ ) {
+ Text(
+ "Data: ${barcode.text}",
+ style = MaterialTheme.typography.bodyLarge.copy(color = MaterialTheme.colorScheme.onSurface)
+ )
+ Text(
+ "Format: ${barcode.format}",
+ style = MaterialTheme.typography.bodySmall.copy(
+ color = MaterialTheme.colorScheme.onSurface
+ )
+ )
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/compose-customisable-ui-example/app/src/main/java/io/scanbot/example/compose/components/FinderCornered.kt b/compose-customisable-ui-example/app/src/main/java/io/scanbot/example/compose/components/FinderCornered.kt
new file mode 100644
index 00000000..5ea04196
--- /dev/null
+++ b/compose-customisable-ui-example/app/src/main/java/io/scanbot/example/compose/components/FinderCornered.kt
@@ -0,0 +1,86 @@
+package io.scanbot.example.compose.components
+
+import androidx.compose.foundation.Canvas
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.geometry.CornerRadius
+import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.geometry.Rect
+import androidx.compose.ui.geometry.Size
+import androidx.compose.ui.graphics.BlendMode
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.Paint
+import androidx.compose.ui.graphics.Path
+import androidx.compose.ui.graphics.StrokeCap
+import androidx.compose.ui.graphics.drawscope.Stroke
+import androidx.compose.ui.graphics.withSaveLayer
+import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.dp
+import kotlin.math.min
+
+@Composable
+fun CorneredFinder(
+ cornerRadius: Dp = 16.dp,
+ strokeWidth: Dp = 4.dp,
+ strokeColor: Color = Color.White,
+) {
+ Canvas(modifier = Modifier.fillMaxSize(), onDraw = {
+
+ val maxWidth = this.size.width
+ val maxHeight = this.size.height
+ val cornerRadiusPx = cornerRadius.toPx()
+ val strokeWidthPx = strokeWidth.toPx()
+ val cornerSize = min(
+ cornerRadiusPx + cornerRadiusPx / 2 + strokeWidthPx / 2,
+ min(maxWidth, maxHeight) / 2f
+ )
+
+
+ val clearPath = Path().apply {
+ moveTo(cornerSize, 0f)
+
+ lineTo(maxWidth - cornerSize, 0f)
+ moveTo(maxWidth, 0f)
+ moveTo(maxWidth, cornerSize)
+
+ lineTo(maxWidth, maxHeight - cornerSize)
+ moveTo(maxWidth, maxHeight)
+ moveTo(maxWidth - cornerSize, maxHeight)
+
+ lineTo(cornerSize, maxHeight)
+ moveTo(0f, maxHeight)
+ moveTo(0f, maxHeight - cornerSize)
+ lineTo(0f, cornerSize)
+ }
+
+ this.drawContext.canvas.withSaveLayer(
+ bounds = Rect(
+ 0f,
+ 0f,
+ maxWidth,
+ maxHeight
+ ),
+ paint = Paint(),
+ ) {
+ drawRoundRect(
+ color = strokeColor,
+ topLeft = Offset(strokeWidthPx / 2, strokeWidthPx / 2),
+ size = Size(maxWidth - strokeWidthPx, maxHeight - strokeWidthPx),
+ style = Stroke(
+ width = strokeWidthPx
+ ),
+ cornerRadius = CornerRadius(cornerRadiusPx),
+ )
+
+ drawPath(
+ path = clearPath, color = Color.Black, style = Stroke(
+ width = strokeWidthPx * 4,
+ cap = StrokeCap.Butt,
+ ),
+ blendMode = BlendMode.Clear
+ )
+ }
+
+ })
+}
diff --git a/compose-customisable-ui-example/app/src/main/java/io/scanbot/example/compose/doc_code_snippet/barcode/BarcodeArOverlaySnippet.kt b/compose-customisable-ui-example/app/src/main/java/io/scanbot/example/compose/doc_code_snippet/barcode/BarcodeArOverlaySnippet.kt
new file mode 100644
index 00000000..eb6c8a4a
--- /dev/null
+++ b/compose-customisable-ui-example/app/src/main/java/io/scanbot/example/compose/doc_code_snippet/barcode/BarcodeArOverlaySnippet.kt
@@ -0,0 +1,95 @@
+package io.scanbot.example.compose.doc_code_snippet.barcode
+
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.padding
+import androidx.compose.material.MaterialTheme
+import androidx.compose.material.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.geometry.Rect
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.layout.layout
+import androidx.compose.ui.platform.LocalDensity
+import androidx.compose.ui.unit.dp
+import io.scanbot.sdk.barcode.*
+import io.scanbot.sdk.camera.*
+import io.scanbot.sdk.ui_v2.barcode.components.ar_tracking.*
+import kotlinx.coroutines.flow.SharedFlow
+
+
+// @Tag("Customisable barcode AR view")
+@Composable
+fun BarcodeArOverlaySnippet(
+ barcodesFlow: SharedFlow?>,
+) {
+ val density = LocalDensity.current
+
+ ScanbotBarcodesArOverlay(
+ barcodesFlow,
+ getData = { barcodeItem -> barcodeItem.textWithExtension },
+ shouldHighlight = { barcodeItem ->
+ // Here you can implement any custom logic to decide whether to highlight a barcode with second style or not.
+ false
+ },
+ getPolygonStyle = { defaultStyle, barcodeItem ->
+ // Customize polygon style here.
+ // You may use barcodeItem to apply different styles for different barcode types, etc.
+ defaultStyle.copy(
+ drawPolygon = true,
+ // Control whether to fill the polygon with fill color
+ useFill = true,
+ // Control whether to fill the polygon with fill color when highlighted
+ useFillHighlighted = true,
+ // Radius of the polygon corners in px
+ cornerRadius = density.run { 20.dp.toPx() },
+ cornerHighlightedRadius = density.run { 20.dp.toPx() },
+ // Width of the polygon stroke in px
+ strokeWidth = density.run { 5.dp.toPx() },
+ // Width of the polygon stroke when highlighted in px
+ strokeHighlightedWidth = density.run { 5.dp.toPx() },
+ // Color of the polygon stroke
+ strokeColor = Color.Green,
+ // Color of the polygon stroke when highlighted
+ strokeHighlightedColor = Color.Red,
+ // Fill color of the polygon
+ fillColor = Color.Green.copy(alpha = 0.3f),
+ // Fill color of the polygon when highlighted
+ fillHighlightedColor = Color.Red.copy(alpha = 0.3f),
+ shouldDrawShadows = false
+ )
+ },
+ // Customize AR view for barcode polygon here if needed
+ // For example, show barcode data below the polygon or display an icon, image etc.
+ view = { path, barcodeItem, data, shouldHighlight ->
+ // Implement custom view for barcode polygon if needed
+ Box(modifier = Modifier.layout { measurable, constraints ->
+ val placeable = measurable.measure(constraints);
+
+ var rectF: Rect
+ path.getBounds().also { rectF = it }
+
+ val width = placeable.width
+ val height = placeable.height
+ val x = rectF.center.x - width / 2
+ val y = rectF.center.y + rectF.height / 2 + 10.dp.toPx() // place below the polygon
+ layout(width, height) {
+ placeable.placeRelative(x.toInt(), y.toInt())
+ }
+ }) {
+ Text(
+ text = data,
+ color = if (shouldHighlight) Color.Red else Color.Green,
+ style = MaterialTheme.typography.body2,
+ modifier = Modifier
+ .background(Color.Black.copy(alpha = 0.5f))
+ .padding(4.dp)
+ )
+ }
+ },
+ onClick = {
+ //handle click on polygon area representing a barcode
+ },
+ )
+}
+// @EndTag("Customisable barcode AR view")
\ No newline at end of file
diff --git a/compose-customisable-ui-example/app/src/main/java/io/scanbot/example/compose/doc_code_snippet/barcode/BarcodeScannerSnippet.kt b/compose-customisable-ui-example/app/src/main/java/io/scanbot/example/compose/doc_code_snippet/barcode/BarcodeScannerSnippet.kt
new file mode 100644
index 00000000..eefa913e
--- /dev/null
+++ b/compose-customisable-ui-example/app/src/main/java/io/scanbot/example/compose/doc_code_snippet/barcode/BarcodeScannerSnippet.kt
@@ -0,0 +1,100 @@
+package io.scanbot.example.compose.doc_code_snippet.barcode
+
+import android.util.Log
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.mutableFloatStateOf
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.unit.dp
+import io.scanbot.common.*
+import io.scanbot.example.compose.CustomBarcodesArView
+import io.scanbot.sdk.barcode.textWithExtension
+import io.scanbot.sdk.geometry.AspectRatio
+import io.scanbot.sdk.ui_v2.barcode.BarcodeScannerCustomUI
+import io.scanbot.sdk.ui_v2.common.CameraModule
+import io.scanbot.sdk.ui_v2.common.CameraPermissionScreen
+import io.scanbot.sdk.ui_v2.common.CameraPreviewMode
+import io.scanbot.sdk.ui_v2.common.components.FinderConfiguration
+import io.scanbot.sdk.ui_v2.common.components.ScanbotCameraPermissionView
+
+// @Tag("Detailed Barcode Scanner Composable")
+@Composable
+fun BarcodeScannerSnippet() {
+ // @Tag("Mutable states for camera control")
+ // Use these states to control camera, torch and zoom
+ val zoom = remember { mutableFloatStateOf(1.0f) }
+ val torchEnabled = remember { mutableStateOf(false) }
+ val cameraEnabled = remember { mutableStateOf(true) }
+ // Unused in this example, but you may use it to
+ // enable/disable barcode scanning dynamically
+ val scanningEnabled = remember { mutableStateOf(true) }
+ // @EndTag("Mutable states for camera control")
+ BarcodeScannerCustomUI(
+ // Modify Size here:
+ modifier = Modifier
+ .fillMaxSize(),
+ // See more details about FinderConfiguration in the FinderConfigurationSnippet.kt
+ finderConfiguration = FinderConfiguration(
+ verticalAlignment = Alignment.Top,
+ // Modify aspect ratio of the viewfinder here:
+ aspectRatio = AspectRatio(1.0, 1.0),
+ ),
+ // Enable or disable camera view here:
+ cameraEnabled = cameraEnabled.value,
+ // Select front or back camera here:
+ cameraModule = CameraModule.BACK,
+ // Set camera preview mode here. Possible values: FIT_IN, FILL_IN
+ cameraPreviewMode = CameraPreviewMode.FILL_IN,
+ // Enable or disable Barcode scanning here:
+ barcodeScanningEnabled = scanningEnabled.value,
+ // Enable or disable torch here:
+ torchEnabled = torchEnabled.value,
+ // Set zoom level. Range from 1.0 to 5.0:
+ zoomLevel = zoom.floatValue,
+ // Permission view that will be shown if camera permission is not granted
+ arPolygonView = { dataFlow->
+ CustomBarcodesArView(dataFlow,{
+ //handle click on barcode in AR
+ })
+ },
+ permissionView = {
+ // View that will be shown while camera permission is not granted
+ // Use custom layout of camera permission handling view here:
+ ScanbotCameraPermissionView(
+ modifier = Modifier.fillMaxSize(),
+ bottomContentPadding = 0.dp,
+ permissionConfig = CameraPermissionScreen(),
+ onClose = {
+ // Handle permission screen close if needed
+ })
+ },
+ onBarcodeScanningResult = { result ->
+ // See https://docs.scanbot.io/android/data-capture-modules/detailed-setup-guide/result-api/ for details of result handling
+ result.onSuccess { data ->
+ // Handle scanned barcodes here (for example, show a dialog or navigate to another screen)
+ Log.d(
+ "BarcodeScannerScreen",
+ "Scanned Barcodes: ${data.barcodes.joinToString { it.textWithExtension }}"
+ )
+ }.onFailure { error ->
+ when (error) {
+ is Result.InvalidLicenseError -> {
+ Log.e(
+ "BarcodeScannerScreen",
+ "Barcodes scanning license error: ${error.message}"
+ )
+ }
+
+ else -> {
+ // Handle error here
+ Log.e("BarcodeScannerScreen", "Barcodes scanning error: ${error.message}")
+ }
+ }
+ }
+ }
+ )
+}
+// @EndTag("Detailed Barcode Scanner Composable")
diff --git a/compose-customisable-ui-example/app/src/main/java/io/scanbot/example/compose/doc_code_snippet/document/DocumentScannerSnippet.kt b/compose-customisable-ui-example/app/src/main/java/io/scanbot/example/compose/doc_code_snippet/document/DocumentScannerSnippet.kt
new file mode 100644
index 00000000..74694862
--- /dev/null
+++ b/compose-customisable-ui-example/app/src/main/java/io/scanbot/example/compose/doc_code_snippet/document/DocumentScannerSnippet.kt
@@ -0,0 +1,333 @@
+package io.scanbot.example.compose.doc_code_snippet.document
+
+import android.graphics.Bitmap
+import android.util.Log
+import androidx.annotation.OptIn
+import androidx.camera.camera2.interop.ExperimentalCamera2Interop
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.mutableFloatStateOf
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.rememberCoroutineScope
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.unit.dp
+import coil.compose.AsyncImage
+import io.scanbot.common.mapSuccess
+import io.scanbot.common.onFailure
+import io.scanbot.common.onSuccess
+import io.scanbot.sdk.ScanbotSDK
+import io.scanbot.sdk.camera.FrameHandler
+import io.scanbot.sdk.documentscanner.DocumentDetectionResult
+import io.scanbot.sdk.documentscanner.DocumentDetectionStatus
+import io.scanbot.sdk.documentscanner.DocumentScanner
+import io.scanbot.sdk.documentscanner.DocumentScannerConfiguration
+import io.scanbot.sdk.documentscanner.DocumentScannerParameters
+import io.scanbot.sdk.image.ImageRef
+import io.scanbot.sdk.imageprocessing.ScanbotSdkImageProcessor
+import io.scanbot.sdk.ui_v2.common.CameraModule
+import io.scanbot.sdk.ui_v2.common.CameraPermissionScreen
+import io.scanbot.sdk.ui_v2.common.CameraPreviewMode
+import io.scanbot.sdk.ui_v2.common.camera.TakePictureActionController
+import io.scanbot.sdk.ui_v2.common.components.ScanbotCameraPermissionView
+import io.scanbot.sdk.ui_v2.common.components.ScanbotSnapButton
+import io.scanbot.sdk.ui_v2.document.DocumentScannerCustomUI
+import io.scanbot.sdk.ui_v2.document.components.camera.ScanbotDocumentArOverlay
+import io.scanbot.sdk.ui_v2.document.screen.AutoSnappingConfiguration
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.flow.SharedFlow
+import kotlinx.coroutines.launch
+
+// @Tag("Detailed Document Scanner Composable")
+@Composable
+@OptIn(ExperimentalCamera2Interop::class)
+fun DocumentScannerSnippet() {
+ val context = LocalContext.current
+ val sdk = remember { ScanbotSDK(context) }
+ val imageProcessor = remember { ScanbotSdkImageProcessor.create() }
+ val documentScanner = remember { sdk.createDocumentScanner().getOrNull() }
+ // @Tag("Mutable states for camera control")
+ // Use these states to control camera, torch and zoom
+ val zoom = remember { mutableFloatStateOf(1.0f) }
+ val torchEnabled = remember { mutableStateOf(false) }
+ val cameraEnabled = remember { mutableStateOf(true) }
+ val scope = rememberCoroutineScope()
+ // Unused in this example, but you may use it to
+ // enable/disable barcode scanning dynamically
+ val scanningEnabled = remember { mutableStateOf(true) }
+ val autosnappingEnabled = remember { mutableStateOf(false) }
+ val cameraInProcessingState = remember { mutableStateOf(false) }
+ val scannedImage = remember { mutableStateOf(null) }
+ val takePictureActionController =
+ remember { mutableStateOf(null) }
+ // @EndTag("Mutable states for camera control")
+ Box(
+ modifier = Modifier
+ .fillMaxWidth(),
+ ) {
+ DocumentScannerCustomUI(
+ // Modify Size here:
+ modifier = Modifier.fillMaxSize(),
+ // Enable or disable camera view.
+ cameraEnabled = cameraEnabled.value,
+ // Select front or back camera here:
+ cameraModule = CameraModule.BACK,
+ // Set camera preview mode here. Possible values: FIT_IN, FILL_IN
+ cameraPreviewMode = CameraPreviewMode.FILL_IN,
+ // Enable or disable document scanning process here:
+ documentScanningEnabled = scanningEnabled.value,
+ // Auto-snapping configuration
+ autoSnappingConfiguration = AutoSnappingConfiguration(
+ enabled = autosnappingEnabled.value,
+ //Changes sensitivity of auto-snapping. That is: the more sensitive it is the faster it shoots.
+ // Sensitivity must be within `[0..1]` range. A value of 1.0 triggers automatic capturing immediately,
+ // a value of 0.0 delays the automatic by 3 seconds.
+ sensitivity = 0.66f,
+ // Enable or disable shake detection to stop auto-snapping when device is shaken
+ shakeDetectionEnabled = true,
+ ),
+ // Enable or disable torch here:
+ torchEnabled = torchEnabled.value,
+ // Set zoom level. Range from 1.0 to 5.0:
+ zoomLevel = zoom.floatValue,
+ // Document scanner configuration to customize scanning parameters . Eg document apect ratios, size ratios, etc.
+ documentScannerConfiguration = DocumentScannerConfiguration(
+ parameters = DocumentScannerParameters(
+ ignoreOrientationMismatch = true
+ )
+ ),
+ //uncomment to turn on the finder view with custom aspect ratio
+ // See more details about FinderConfiguration in the FinderConfigurationSnippet.kt
+ /*finderConfiguration = FinderConfiguration(
+ verticalAlignment = Alignment.CenterVertically,
+ // Modify aspect ratio of the viewfinder here:
+ aspectRatio = AspectRatio(21.0, 29.0),
+ ),*/
+ // Permission view that will be shown if camera permission is not granted
+ permissionView = {
+ // View that will be shown while camera permission is not granted
+ ScanbotCameraPermissionView(
+ modifier = Modifier.fillMaxSize(),
+ bottomContentPadding = 0.dp,
+ permissionConfig = CameraPermissionScreen(),
+ onClose = {
+ // Handle permission screen close if needed
+ })
+ },
+ // AR Polygon overlay configuration
+ arPolygonView = { dataFlow ->
+ // AR Overlay composable that will be drawn over the camera preview
+ // remove this block to disable AR overlay
+ ScanbotDocumentArOverlay(
+ dataFlow = dataFlow,
+ // Enable or disable all polygons drawing here
+ drawPolygon = true,
+ isPolygonOk = { status ->
+ // Define which detection statuses are considered for AR polygon as OK
+ status == DocumentDetectionStatus.OK
+ },
+ // lambda to customize polygon style when document is shown as OK
+ getOkPolygonStyle = { defaultStyle ->
+ // Customize polygon style if needed
+ defaultStyle.copy(
+ strokeWidth = 8f,
+ strokeColor = Color.Blue,
+ fillColor = Color.Blue.copy(alpha = 0.15f)
+ )
+ },
+ // lambda to customize polygon style when document is shown as Not OK
+ getNotOkPolygonStyle = { defaultStyle ->
+ // Customize polygon style if needed
+ defaultStyle.copy(
+ strokeWidth = 8f,
+ strokeColor = Color.Red,
+ fillColor = Color.Red.copy(alpha = 0.15f)
+ )
+ },
+ // lambda to customize polygon that shown on top of the OK polygon during auto-snapping countdown
+ getProgressPolygonStyle = { defaultStyle ->
+ // Customize polygon style if needed
+ defaultStyle.copy(
+ strokeWidth = 8f,
+ strokeColor = Color.Green,
+ fillColor = Color.Transparent
+ )
+ },
+ )
+ },
+ // Triggered when take picture is called (auto or manual)
+ onTakePictureCalled = {
+ Log.d("DocumentScannerScreen1", "Take picture called")
+ cameraInProcessingState.value = true
+ },
+ // Triggered when take picture is canceled (auto or manual)
+ onTakePictureCanceled = {
+ Log.d("DocumentScannerScreen1", "Take picture canceled")
+ cameraInProcessingState.value = false
+ },
+ // Triggered when picture is taken successfully and provides image for further processing
+ onPictureSnapped = { imageRef, captureInfo ->
+ // WARNING: move all processing operation to view model with proper coroutine scope in real apps to avoid data loss during recompositions
+ scope.launch(Dispatchers.Default) {
+ scannedImage.value = createPreview(documentScanner, imageRef, imageProcessor)
+ // Picture is received, allow auto-snapping again or proceed further and allow image snap after some additional processing
+ cameraInProcessingState.value = false
+ }
+ },
+ // Provides TakePictureActionController to use for triggering picture taking manually
+ onTakePictureControllerCreated = {
+ takePictureActionController.value = it
+ },
+ // Callback that is called right before auto-snapping should be triggered.
+ // Return false to allow auto-snapping, true to prevent it.
+ onAutoSnapping = {
+ // return true if auto-snapping should be consumed and not proceed to take picture
+ cameraInProcessingState.value // Disable auto-snapping while awaiting picture result after snap is triggered
+ },
+ // Callback invoked after each frame with document scanning result.
+ onDocumentScanningResult = { result ->
+ result.onSuccess { data ->
+ // Handle scanned barcodes here (for example, show a dialog)
+ val points = data.pointsNormalized
+ val status = data.status
+ }
+ },
+ )
+ ScanbotSnapButton(
+ modifier = Modifier
+ .height(100.dp)
+ .align(Alignment.BottomCenter),
+ // Disable button when scanning or auto-snapping is disabled
+ clickable = scanningEnabled.value && !cameraInProcessingState.value,
+ // Show indicator that camera in auto-snapping mode
+ autoCapture = autosnappingEnabled.value,
+ // Animate progress when camera is processing the last taken picture
+ animateProgress = cameraInProcessingState.value,
+ ) {
+ takePictureActionController.value?.invoke()
+ }
+
+ // Simple view that shows scanned image preview
+ if (scannedImage.value != null) {
+ AsyncImage(
+ model = scannedImage.value,
+ contentDescription = "ScannedImage",
+ modifier = Modifier
+ .height(100.dp)
+ .align(Alignment.BottomEnd)
+ )
+ }
+ }
+}
+
+private fun createPreview(
+ documentScanner: DocumentScanner?,
+ imageRef: ImageRef,
+ imageProcessor: ScanbotSdkImageProcessor,
+): Bitmap? {
+ // See https://docs.scanbot.io/android/data-capture-modules/detailed-setup-guide/result-api/ for details of result handling
+ // run detection and cropping on the captured image
+ return documentScanner?.run(imageRef)?.mapSuccess { documentData ->
+ val croppedImage =
+ imageProcessor.crop(imageRef, documentData.pointsNormalized)
+ .getOrReturn() // get the result of cropping operation or leave onSuccess if cropping failed
+ imageRef.close() // clear image ref resources
+
+ val downscaledCrop = imageProcessor.resize(croppedImage, 300).getOrReturn().toBitmap()
+ .getOrReturn() // get the result of cropping operation or leave onSuccess if cropping failed
+ croppedImage.close() // clear image ref resources
+ downscaledCrop
+ }?.onFailure { error ->
+ Log.e(
+ "DocumentScannerScreen",
+ "Document scanning error: ${error.message}"
+ )
+ }?.getOrNull()
+}
+// @EndTag("Detailed Document Scanner Composable")
+
+// @Tag("Detailed Snap Button Composable")
+@Composable
+fun SnapButtonSnippet() {
+ ScanbotSnapButton(
+ modifier = Modifier
+ .height(100.dp),
+ // Disable button when scanning or auto-snapping is disabled
+ clickable = true,
+ // Show indicator that camera in auto-snapping mode
+ autoCapture = false,
+ // Animate progress when camera is processing the last taken picture
+ // Need to be true to show progress animation. Use mutable state to control it.
+ animateProgress = false,
+ // Rotation speed of the outer big arc in auto-capture mode
+ bigArcSpeed = 3000,
+ // Speed of the progress arc during processing
+ progressSpeed = 400,
+ // Color of buttons inner component
+ innerColor = Color.White,
+ // Outer color of buttons outer component
+ outerColor = Color.Red,
+ // Color of the outer Arc
+ lineWidth = 5.dp,
+ // Size of the empty space between inner and outer components
+ emptyLineWidth = 10.dp,
+ // Sweep angle of the outer arc
+ arcSweepAngle = 270f,
+ // Initial angle of the 360 degrees rotating big arc
+ bigArcInitialAngle = 200f,
+ // Scale factor applied to the button when pressed
+ snapButtonPressedSize = 1.2f
+ ) {
+ // Handle button click here
+ }
+}
+// @EndTag("Detailed Snap Button Composable")
+
+// @Tag(" Document AR overlay Snippet")
+@Composable
+fun DocumentArOverlaySnippet(dataFlow: SharedFlow>) {
+ // AR Overlay composable that will be drawn over the camera preview
+ // remove this block to disable AR overlay
+ ScanbotDocumentArOverlay(
+ dataFlow = dataFlow,
+ // Enable or disable all polygons drawing here
+ drawPolygon = true,
+ isPolygonOk = { status ->
+ // Define which detection statuses are considered for AR polygon as OK
+ status == DocumentDetectionStatus.OK
+ },
+ // lambda to customize polygon style when document is shown as OK
+ getOkPolygonStyle = { defaultStyle ->
+ // Customize polygon style if needed
+ defaultStyle.copy(
+ strokeWidth = 8f,
+ strokeColor = Color.Blue,
+ fillColor = Color.Blue.copy(alpha = 0.15f)
+ )
+ },
+ // lambda to customize polygon style when document is shown as Not OK
+ getNotOkPolygonStyle = { defaultStyle ->
+ // Customize polygon style if needed
+ defaultStyle.copy(
+ strokeWidth = 8f,
+ strokeColor = Color.Red,
+ fillColor = Color.Red.copy(alpha = 0.15f)
+ )
+ },
+ // lambda to customize polygon that shown on top of the OK polygon during auto-snapping countdown
+ getProgressPolygonStyle = { defaultStyle ->
+ // Customize polygon style if needed
+ defaultStyle.copy(
+ strokeWidth = 8f,
+ strokeColor = Color.Green,
+ fillColor = Color.Transparent
+ )
+ },
+ )
+}
\ No newline at end of file
diff --git a/compose-customisable-ui-example/app/src/main/java/io/scanbot/example/compose/doc_code_snippet/finder/FinderConfigurationSnippet.kt b/compose-customisable-ui-example/app/src/main/java/io/scanbot/example/compose/doc_code_snippet/finder/FinderConfigurationSnippet.kt
new file mode 100644
index 00000000..6f8d89d0
--- /dev/null
+++ b/compose-customisable-ui-example/app/src/main/java/io/scanbot/example/compose/doc_code_snippet/finder/FinderConfigurationSnippet.kt
@@ -0,0 +1,98 @@
+package io.scanbot.example.compose.doc_code_snippet.finder
+
+import androidx.compose.foundation.background
+import androidx.compose.foundation.border
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.PaddingValues
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.text.style.TextAlign
+import androidx.compose.ui.unit.dp
+import io.scanbot.sdk.geometry.AspectRatio
+import io.scanbot.sdk.ui_v2.common.components.FinderConfiguration
+
+
+// @Tag("Detailed Finder Configuration")
+@Composable
+fun FinderConfigurationSnippet() {
+ FinderConfiguration(
+ // align the viewfinder to the top free space of the Camera Preview. Means that it will be close to the preview edge minus the previewInsets
+ verticalAlignment = Alignment.Top,
+ // align the viewfinder to the free horizontal space of the Camera Preview
+ horizontalAlignment = Alignment.CenterHorizontally,
+ // Insets from the edges of the camera preview to the viewfinder.
+ previewInsets = PaddingValues(
+ top = 32.dp,
+ bottom = 32.dp,
+ start = 16.dp,
+ end = 16.dp
+ ),
+ // Aspect ratio of the viewfinder window:
+ aspectRatio = AspectRatio(1.0, 1.0),
+ // Change viewfinder overlay color here:
+ overlayColor = Color.Black.copy(alpha = 0.3f),
+ // Viewfinder stroke color here, default is Transparent:
+ strokeColor = Color.White,
+ // Viewfinder stroke width here:
+ strokeWidth = 2.dp,
+ // radius for rounded corners of the viewfinder window:
+ cornerRadius = 8.dp,
+ // Limit the maximum width of the viewfinder window on the preview. This parameter work with aspect ratio to define the final size of the viewfinder.
+ //preferredMaxWidth = 300.dp,
+ // Limit the maximum height of the viewfinder window on the preview. This parameter work with aspect ratio to define the final size of the viewfinder.
+ //preferredMaxHeight = 52.dp,
+ // Composable area that is inserted inside the viewfinder window:
+ finderContent = {
+ // Box with border stroke color as an example of custom viewfinder content
+ Box(
+ modifier = Modifier
+ .fillMaxSize()
+ .background(Color.Transparent)
+ // Same but with rounded corners
+ .border(
+ 4.dp,
+ Color.Cyan,
+ shape = RoundedCornerShape(
+ 16.dp
+ )
+ )
+ ) {
+
+ }
+ },
+ // Composable area that are inserted between the viewfinder window and the top edge of the camera view:
+ topContent = {
+ Text(
+ "Custom Top Content",
+ color = Color.White,
+ textAlign = TextAlign.Center,
+ modifier = Modifier
+ .fillMaxWidth()
+ )
+ },
+ // Composable area that are inserted between the viewfinder window and the bottom edge of the camera view:
+ bottomContent = {
+ // You may add custom components and other elements here:
+ Text(
+ "Custom Bottom Content",
+ color = Color.White,
+ textAlign = TextAlign.Center,
+ modifier = Modifier
+ .fillMaxWidth()
+ )
+ },
+ bottomLayer = {
+ // Draw something between the viewfinder and camera preview layers
+ },
+ topLayer = {
+ // Draw something above the viewfinder layer if needed
+ }
+ )
+}
+// @EndTag("Detailed Finder Configuration")
\ No newline at end of file
diff --git a/compose-customisable-ui-example/app/src/main/java/io/scanbot/example/compose/doc_code_snippet/mrz/MrzScannerSnippet.kt b/compose-customisable-ui-example/app/src/main/java/io/scanbot/example/compose/doc_code_snippet/mrz/MrzScannerSnippet.kt
new file mode 100644
index 00000000..ec9bdc5a
--- /dev/null
+++ b/compose-customisable-ui-example/app/src/main/java/io/scanbot/example/compose/doc_code_snippet/mrz/MrzScannerSnippet.kt
@@ -0,0 +1,85 @@
+package io.scanbot.example.compose.doc_code_snippet.mrz
+
+import android.util.Log
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.mutableFloatStateOf
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.unit.dp
+import io.scanbot.common.*
+import io.scanbot.sdk.geometry.*
+import io.scanbot.sdk.ui_v2.common.*
+import io.scanbot.sdk.ui_v2.common.components.*
+import io.scanbot.sdk.ui_v2.mrz.*
+
+// @Tag("Detailed MRZ Scanner Composable")
+@Composable
+fun MrzScannerSnippet() {
+ //@Tag("Mutable states for camera control")
+ // Use these states to control camera, torch and zoom
+ val zoom = remember { mutableFloatStateOf(1.0f) }
+ val torchEnabled = remember { mutableStateOf(false) }
+ val cameraEnabled = remember { mutableStateOf(true) }
+ // Unused in this example, but you may use it to
+ // enable/disable barcode scanning dynamically
+ val scanningEnabled = remember { mutableStateOf(true) }
+ // @EndTag("Mutable states for camera control")
+ MrzScannerCustomUI(
+ // Modify Size here:
+ modifier = Modifier
+ .fillMaxSize(),
+ // See more details about FinderConfiguration in the FinderConfigurationSnippet.kt
+ finderConfiguration = FinderConfiguration(
+ verticalAlignment = Alignment.CenterVertically,
+ // Modify aspect ratio of the viewfinder here:
+ aspectRatio = AspectRatio(adjustedMrzThreeLinedFinderAspectRatio, 1.0),
+ ),
+ cameraEnabled = cameraEnabled.value,
+ // Select front or back camera here:
+ cameraModule = CameraModule.BACK,
+ // Set camera preview mode here. Possible values: FIT_IN, FILL_IN
+ cameraPreviewMode = CameraPreviewMode.FILL_IN,
+ // Enable or disable MRZ scanning here:
+ mrzScanningEnabled = scanningEnabled.value,
+ // Enable or disable torch here:
+ torchEnabled = torchEnabled.value,
+ // Set zoom level here:
+ zoomLevel = zoom.floatValue,
+ // Permission view that will be shown if camera permission is not granted
+ permissionView = {
+ // View that will be shown while camera permission is not granted
+ // Use custom layout of camera permission handling view here:
+ ScanbotCameraPermissionView(
+ modifier = Modifier.fillMaxSize(),
+ bottomContentPadding = 0.dp,
+ permissionConfig = CameraPermissionScreen(),
+ onClose = {
+ // Handle permission screen close if needed
+ })
+ },
+ onMrzScanningResult = { result ->
+ // See https://docs.scanbot.io/android/data-capture-modules/detailed-setup-guide/result-api/ for details of result handling
+ result.onSuccess { data ->
+ // Handle scanned barcodes here (for example, show a dialog or navigate to another screen)
+ Log.d(
+ "MrzScannerScreen", "Scanned mrz: ${data.rawMRZ}"
+ )
+ }.onFailure { error ->
+ when (error) {
+ is Result.InvalidLicenseError -> {
+ Log.e("MrzScannerScreen", "MRZ scanning license error: ${error.message}")
+ }
+
+ else -> {
+ // Handle error here
+ Log.e("MrzScannerScreen", "MRZ scanning error: ${error.message}")
+ }
+ }
+ }
+ }
+ )
+}
+// @EndTag("Detailed MRZ Scanner Composable")
diff --git a/compose-customisable-ui-example/app/src/main/java/io/scanbot/example/compose/ui/theme/Color.kt b/compose-customisable-ui-example/app/src/main/java/io/scanbot/example/compose/ui/theme/Color.kt
new file mode 100644
index 00000000..00e53801
--- /dev/null
+++ b/compose-customisable-ui-example/app/src/main/java/io/scanbot/example/compose/ui/theme/Color.kt
@@ -0,0 +1,11 @@
+package io.scanbot.demo.composeui.ui.theme
+
+import androidx.compose.ui.graphics.Color
+
+val sbPrimary = Color(0xFF4b4f52)
+val colorPrimaryDark = Color(0xFF000000)
+val colorAccent = Color(0xFF00b92e)
+val backgroundColor = Color(0xFF4b4f52)
+val sbBrandColor = Color(0xFFc8193c)
+val sheetColor = Color(0xFF7f8487)
+
diff --git a/compose-customisable-ui-example/app/src/main/java/io/scanbot/example/compose/ui/theme/Theme.kt b/compose-customisable-ui-example/app/src/main/java/io/scanbot/example/compose/ui/theme/Theme.kt
new file mode 100644
index 00000000..037aa728
--- /dev/null
+++ b/compose-customisable-ui-example/app/src/main/java/io/scanbot/example/compose/ui/theme/Theme.kt
@@ -0,0 +1,55 @@
+package io.scanbot.demo.composeui.ui.theme
+
+import android.os.Build
+import androidx.compose.foundation.isSystemInDarkTheme
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.darkColorScheme
+import androidx.compose.material3.dynamicDarkColorScheme
+import androidx.compose.material3.dynamicLightColorScheme
+import androidx.compose.material3.lightColorScheme
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.platform.LocalContext
+
+private val DarkColorScheme = darkColorScheme(
+ primary = sbPrimary,
+ secondary = sbPrimary,
+ tertiary = sbPrimary
+)
+
+private val LightColorScheme = lightColorScheme(
+ primary = sbPrimary,
+ secondary = sbPrimary,
+ tertiary = sbPrimary,
+ background = backgroundColor,
+ surface = sheetColor,
+ onPrimary = Color.White,
+ onSecondary = Color.White,
+ onTertiary = Color.White,
+ onBackground = Color(0xFF1C1B1F),
+ onSurface = Color(0xFF1C1B1F),
+ )
+
+@Composable
+fun ScanbotsdkandroidTheme(
+ darkTheme: Boolean = isSystemInDarkTheme(),
+ // Dynamic color is available on Android 12+
+ dynamicColor: Boolean = true,
+ content: @Composable () -> Unit,
+) {
+ val colorScheme = when {
+ dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
+ val context = LocalContext.current
+ if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context)
+ }
+
+ darkTheme -> DarkColorScheme
+ else -> LightColorScheme
+ }
+
+ MaterialTheme(
+ colorScheme = colorScheme,
+ typography = Typography,
+ content = content
+ )
+}
diff --git a/compose-customisable-ui-example/app/src/main/java/io/scanbot/example/compose/ui/theme/Type.kt b/compose-customisable-ui-example/app/src/main/java/io/scanbot/example/compose/ui/theme/Type.kt
new file mode 100644
index 00000000..a99a2fe4
--- /dev/null
+++ b/compose-customisable-ui-example/app/src/main/java/io/scanbot/example/compose/ui/theme/Type.kt
@@ -0,0 +1,34 @@
+package io.scanbot.demo.composeui.ui.theme
+
+import androidx.compose.material3.Typography
+import androidx.compose.ui.text.TextStyle
+import androidx.compose.ui.text.font.FontFamily
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.unit.sp
+
+// Set of Material typography styles to start with
+val Typography = Typography(
+ bodyLarge = TextStyle(
+ fontFamily = FontFamily.Default,
+ fontWeight = FontWeight.Normal,
+ fontSize = 16.sp,
+ lineHeight = 24.sp,
+ letterSpacing = 0.5.sp
+ )
+ /* Other default text styles to override
+ titleLarge = TextStyle(
+ fontFamily = FontFamily.Default,
+ fontWeight = FontWeight.Normal,
+ fontSize = 22.sp,
+ lineHeight = 28.sp,
+ letterSpacing = 0.sp
+ ),
+ labelSmall = TextStyle(
+ fontFamily = FontFamily.Default,
+ fontWeight = FontWeight.Medium,
+ fontSize = 11.sp,
+ lineHeight = 16.sp,
+ letterSpacing = 0.5.sp
+ )
+ */
+)
diff --git a/compose-customisable-ui-example/app/src/main/res/drawable-v24/ic_launcher_foreground.xml b/compose-customisable-ui-example/app/src/main/res/drawable-v24/ic_launcher_foreground.xml
new file mode 100644
index 00000000..1f6bb290
--- /dev/null
+++ b/compose-customisable-ui-example/app/src/main/res/drawable-v24/ic_launcher_foreground.xml
@@ -0,0 +1,34 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/compose-customisable-ui-example/app/src/main/res/drawable/divider_menu_items.xml b/compose-customisable-ui-example/app/src/main/res/drawable/divider_menu_items.xml
new file mode 100644
index 00000000..32ee0d2f
--- /dev/null
+++ b/compose-customisable-ui-example/app/src/main/res/drawable/divider_menu_items.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
diff --git a/compose-customisable-ui-example/app/src/main/res/drawable/ic_broken_image_white_24dp.xml b/compose-customisable-ui-example/app/src/main/res/drawable/ic_broken_image_white_24dp.xml
new file mode 100644
index 00000000..32316e1e
--- /dev/null
+++ b/compose-customisable-ui-example/app/src/main/res/drawable/ic_broken_image_white_24dp.xml
@@ -0,0 +1,5 @@
+
+
+
diff --git a/compose-customisable-ui-example/app/src/main/res/drawable/ic_launcher_background.xml b/compose-customisable-ui-example/app/src/main/res/drawable/ic_launcher_background.xml
new file mode 100644
index 00000000..0d025f9b
--- /dev/null
+++ b/compose-customisable-ui-example/app/src/main/res/drawable/ic_launcher_background.xml
@@ -0,0 +1,170 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/compose-customisable-ui-example/app/src/main/res/drawable/warning_shape.xml b/compose-customisable-ui-example/app/src/main/res/drawable/warning_shape.xml
new file mode 100644
index 00000000..e01bd4fa
--- /dev/null
+++ b/compose-customisable-ui-example/app/src/main/res/drawable/warning_shape.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/compose-customisable-ui-example/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/compose-customisable-ui-example/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
new file mode 100644
index 00000000..4ae7d123
--- /dev/null
+++ b/compose-customisable-ui-example/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/compose-customisable-ui-example/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/compose-customisable-ui-example/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
new file mode 100644
index 00000000..4ae7d123
--- /dev/null
+++ b/compose-customisable-ui-example/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/compose-customisable-ui-example/app/src/main/res/mipmap-hdpi/ic_launcher.png b/compose-customisable-ui-example/app/src/main/res/mipmap-hdpi/ic_launcher.png
new file mode 100644
index 00000000..2cd5ef66
Binary files /dev/null and b/compose-customisable-ui-example/app/src/main/res/mipmap-hdpi/ic_launcher.png differ
diff --git a/compose-customisable-ui-example/app/src/main/res/mipmap-hdpi/ic_launcher_background.png b/compose-customisable-ui-example/app/src/main/res/mipmap-hdpi/ic_launcher_background.png
new file mode 100644
index 00000000..b883b839
Binary files /dev/null and b/compose-customisable-ui-example/app/src/main/res/mipmap-hdpi/ic_launcher_background.png differ
diff --git a/compose-customisable-ui-example/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png b/compose-customisable-ui-example/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png
new file mode 100644
index 00000000..529f470d
Binary files /dev/null and b/compose-customisable-ui-example/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png differ
diff --git a/compose-customisable-ui-example/app/src/main/res/mipmap-hdpi/ic_launcher_round.png b/compose-customisable-ui-example/app/src/main/res/mipmap-hdpi/ic_launcher_round.png
new file mode 100644
index 00000000..d46c178c
Binary files /dev/null and b/compose-customisable-ui-example/app/src/main/res/mipmap-hdpi/ic_launcher_round.png differ
diff --git a/compose-customisable-ui-example/app/src/main/res/mipmap-mdpi/ic_launcher.png b/compose-customisable-ui-example/app/src/main/res/mipmap-mdpi/ic_launcher.png
new file mode 100644
index 00000000..6db5e0f2
Binary files /dev/null and b/compose-customisable-ui-example/app/src/main/res/mipmap-mdpi/ic_launcher.png differ
diff --git a/compose-customisable-ui-example/app/src/main/res/mipmap-mdpi/ic_launcher_background.png b/compose-customisable-ui-example/app/src/main/res/mipmap-mdpi/ic_launcher_background.png
new file mode 100644
index 00000000..a65a22b7
Binary files /dev/null and b/compose-customisable-ui-example/app/src/main/res/mipmap-mdpi/ic_launcher_background.png differ
diff --git a/compose-customisable-ui-example/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png b/compose-customisable-ui-example/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png
new file mode 100644
index 00000000..4ab3e5e7
Binary files /dev/null and b/compose-customisable-ui-example/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png differ
diff --git a/compose-customisable-ui-example/app/src/main/res/mipmap-mdpi/ic_launcher_round.png b/compose-customisable-ui-example/app/src/main/res/mipmap-mdpi/ic_launcher_round.png
new file mode 100644
index 00000000..4409f93e
Binary files /dev/null and b/compose-customisable-ui-example/app/src/main/res/mipmap-mdpi/ic_launcher_round.png differ
diff --git a/compose-customisable-ui-example/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/compose-customisable-ui-example/app/src/main/res/mipmap-xhdpi/ic_launcher.png
new file mode 100644
index 00000000..7ea2827c
Binary files /dev/null and b/compose-customisable-ui-example/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ
diff --git a/compose-customisable-ui-example/app/src/main/res/mipmap-xhdpi/ic_launcher_background.png b/compose-customisable-ui-example/app/src/main/res/mipmap-xhdpi/ic_launcher_background.png
new file mode 100644
index 00000000..4f1aeb6e
Binary files /dev/null and b/compose-customisable-ui-example/app/src/main/res/mipmap-xhdpi/ic_launcher_background.png differ
diff --git a/compose-customisable-ui-example/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png b/compose-customisable-ui-example/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png
new file mode 100644
index 00000000..c66d6b35
Binary files /dev/null and b/compose-customisable-ui-example/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png differ
diff --git a/compose-customisable-ui-example/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png b/compose-customisable-ui-example/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png
new file mode 100644
index 00000000..644134cb
Binary files /dev/null and b/compose-customisable-ui-example/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png differ
diff --git a/compose-customisable-ui-example/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/compose-customisable-ui-example/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
new file mode 100644
index 00000000..7322aa33
Binary files /dev/null and b/compose-customisable-ui-example/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ
diff --git a/compose-customisable-ui-example/app/src/main/res/mipmap-xxhdpi/ic_launcher_background.png b/compose-customisable-ui-example/app/src/main/res/mipmap-xxhdpi/ic_launcher_background.png
new file mode 100644
index 00000000..9295e62e
Binary files /dev/null and b/compose-customisable-ui-example/app/src/main/res/mipmap-xxhdpi/ic_launcher_background.png differ
diff --git a/compose-customisable-ui-example/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png b/compose-customisable-ui-example/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png
new file mode 100644
index 00000000..eda5f168
Binary files /dev/null and b/compose-customisable-ui-example/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png differ
diff --git a/compose-customisable-ui-example/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png b/compose-customisable-ui-example/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png
new file mode 100644
index 00000000..01f709ec
Binary files /dev/null and b/compose-customisable-ui-example/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png differ
diff --git a/compose-customisable-ui-example/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/compose-customisable-ui-example/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
new file mode 100644
index 00000000..566a5866
Binary files /dev/null and b/compose-customisable-ui-example/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ
diff --git a/compose-customisable-ui-example/app/src/main/res/mipmap-xxxhdpi/ic_launcher_background.png b/compose-customisable-ui-example/app/src/main/res/mipmap-xxxhdpi/ic_launcher_background.png
new file mode 100644
index 00000000..8c3de908
Binary files /dev/null and b/compose-customisable-ui-example/app/src/main/res/mipmap-xxxhdpi/ic_launcher_background.png differ
diff --git a/compose-customisable-ui-example/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png b/compose-customisable-ui-example/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png
new file mode 100644
index 00000000..1ef923fc
Binary files /dev/null and b/compose-customisable-ui-example/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png differ
diff --git a/compose-customisable-ui-example/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png b/compose-customisable-ui-example/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png
new file mode 100644
index 00000000..b7c965dd
Binary files /dev/null and b/compose-customisable-ui-example/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png differ
diff --git a/compose-customisable-ui-example/app/src/main/res/values-v21/styles.xml b/compose-customisable-ui-example/app/src/main/res/values-v21/styles.xml
new file mode 100644
index 00000000..a15bb1a6
--- /dev/null
+++ b/compose-customisable-ui-example/app/src/main/res/values-v21/styles.xml
@@ -0,0 +1,46 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/compose-customisable-ui-example/app/src/main/res/values/colors.xml b/compose-customisable-ui-example/app/src/main/res/values/colors.xml
new file mode 100644
index 00000000..ff8d61bd
--- /dev/null
+++ b/compose-customisable-ui-example/app/src/main/res/values/colors.xml
@@ -0,0 +1,11 @@
+
+
+ #4b4f52
+ #000000
+ #00b92e
+ #4b4f52
+ #c4c4c4
+ #7f8487
+ #c8193c
+ #1effffff
+
diff --git a/compose-customisable-ui-example/app/src/main/res/values/dimens.xml b/compose-customisable-ui-example/app/src/main/res/values/dimens.xml
new file mode 100644
index 00000000..ae5c3551
--- /dev/null
+++ b/compose-customisable-ui-example/app/src/main/res/values/dimens.xml
@@ -0,0 +1,4 @@
+
+
+ 400dp
+
diff --git a/compose-customisable-ui-example/app/src/main/res/values/snippets_res.xml b/compose-customisable-ui-example/app/src/main/res/values/snippets_res.xml
new file mode 100644
index 00000000..279e51ea
--- /dev/null
+++ b/compose-customisable-ui-example/app/src/main/res/values/snippets_res.xml
@@ -0,0 +1,11 @@
+
+
+
+
+
+
+
+
+ Clear
+ Submit
+
\ No newline at end of file
diff --git a/compose-customisable-ui-example/app/src/main/res/values/strings.xml b/compose-customisable-ui-example/app/src/main/res/values/strings.xml
new file mode 100644
index 00000000..234f70fb
--- /dev/null
+++ b/compose-customisable-ui-example/app/src/main/res/values/strings.xml
@@ -0,0 +1,4 @@
+
+ Scanbot Compose UI Example
+ Scanbot Compose UI Example
+
diff --git a/compose-customisable-ui-example/app/src/main/res/values/styles.xml b/compose-customisable-ui-example/app/src/main/res/values/styles.xml
new file mode 100644
index 00000000..a15bb1a6
--- /dev/null
+++ b/compose-customisable-ui-example/app/src/main/res/values/styles.xml
@@ -0,0 +1,46 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/compose-customisable-ui-example/app/src/main/res/xml/provider_paths.xml b/compose-customisable-ui-example/app/src/main/res/xml/provider_paths.xml
new file mode 100644
index 00000000..ffa74ab5
--- /dev/null
+++ b/compose-customisable-ui-example/app/src/main/res/xml/provider_paths.xml
@@ -0,0 +1,4 @@
+
+
+
+
\ No newline at end of file
diff --git a/compose-customisable-ui-example/applyMinifyEnabledPatch.sh b/compose-customisable-ui-example/applyMinifyEnabledPatch.sh
new file mode 100755
index 00000000..8ff6bee4
--- /dev/null
+++ b/compose-customisable-ui-example/applyMinifyEnabledPatch.sh
@@ -0,0 +1,4 @@
+#!/bin/sh
+
+cd ..
+sh applyMinifyEnabledPatch.sh
\ No newline at end of file
diff --git a/compose-customisable-ui-example/build.gradle b/compose-customisable-ui-example/build.gradle
new file mode 100644
index 00000000..16f7a52d
--- /dev/null
+++ b/compose-customisable-ui-example/build.gradle
@@ -0,0 +1,13 @@
+
+plugins {
+ id "com.android.application" version '8.9.0' apply false
+ id "org.jetbrains.kotlin.android" version "2.2.20" apply false
+ id "org.jetbrains.kotlin.plugin.compose" version "2.2.20" apply false
+}
+
+allprojects {
+ configurations.all {
+ // Hack to let Gradle pickup latest SNAPSHOTS
+ resolutionStrategy.cacheChangingModulesFor 0, 'seconds'
+ }
+}
diff --git a/compose-customisable-ui-example/gradle.properties b/compose-customisable-ui-example/gradle.properties
new file mode 100644
index 00000000..2d817b8f
--- /dev/null
+++ b/compose-customisable-ui-example/gradle.properties
@@ -0,0 +1,21 @@
+# Project-wide Gradle settings.
+
+# IDE (e.g. Android Studio) users:
+# Gradle settings configured through the IDE *will override*
+# any settings specified in this file.
+
+# For more details on how to configure your build environment visit
+# http://www.gradle.org/docs/current/userguide/build_environment.html
+
+# Specifies the JVM arguments used for the daemon process.
+# The setting is particularly useful for tweaking memory settings.
+# Default value: -Xmx10248m -XX:MaxPermSize=256m
+# org.gradle.jvmargs=-Xmx2048m -XX:MaxPermSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8
+
+# When configured, Gradle will run in incubating parallel mode.
+# This option should only be used with decoupled projects. More details, visit
+# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
+# org.gradle.parallel=true
+android.enableJetifier=true
+android.useAndroidX=true
+org.gradle.jvmargs=-Xmx3072M -Dkotlin.daemon.jvm.options="-Xmx3072M"
diff --git a/compose-customisable-ui-example/gradle/wrapper/gradle-wrapper.jar b/compose-customisable-ui-example/gradle/wrapper/gradle-wrapper.jar
new file mode 100644
index 00000000..8c0fb64a
Binary files /dev/null and b/compose-customisable-ui-example/gradle/wrapper/gradle-wrapper.jar differ
diff --git a/compose-customisable-ui-example/gradle/wrapper/gradle-wrapper.properties b/compose-customisable-ui-example/gradle/wrapper/gradle-wrapper.properties
new file mode 100644
index 00000000..c190f9a7
--- /dev/null
+++ b/compose-customisable-ui-example/gradle/wrapper/gradle-wrapper.properties
@@ -0,0 +1,6 @@
+#Wed Apr 24 08:45:12 CEST 2019
+distributionBase=GRADLE_USER_HOME
+distributionPath=wrapper/dists
+zipStoreBase=GRADLE_USER_HOME
+zipStorePath=wrapper/dists
+distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-bin.zip
diff --git a/compose-customisable-ui-example/gradlew b/compose-customisable-ui-example/gradlew
new file mode 100755
index 00000000..91a7e269
--- /dev/null
+++ b/compose-customisable-ui-example/gradlew
@@ -0,0 +1,164 @@
+#!/usr/bin/env bash
+
+##############################################################################
+##
+## Gradle start up script for UN*X
+##
+##############################################################################
+
+# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+DEFAULT_JVM_OPTS=""
+
+APP_NAME="Gradle"
+APP_BASE_NAME=`basename "$0"`
+
+# Use the maximum available, or set MAX_FD != -1 to use that value.
+MAX_FD="maximum"
+
+warn ( ) {
+ echo "$*"
+}
+
+die ( ) {
+ echo
+ echo "$*"
+ echo
+ exit 1
+}
+
+# OS specific support (must be 'true' or 'false').
+cygwin=false
+msys=false
+darwin=false
+case "`uname`" in
+ CYGWIN* )
+ cygwin=true
+ ;;
+ Darwin* )
+ darwin=true
+ ;;
+ MINGW* )
+ msys=true
+ ;;
+esac
+
+# For Cygwin, ensure paths are in UNIX format before anything is touched.
+if $cygwin ; then
+ [ -n "$JAVA_HOME" ] && JAVA_HOME=`cygpath --unix "$JAVA_HOME"`
+fi
+
+# Attempt to set APP_HOME
+# Resolve links: $0 may be a link
+PRG="$0"
+# Need this for relative symlinks.
+while [ -h "$PRG" ] ; do
+ ls=`ls -ld "$PRG"`
+ link=`expr "$ls" : '.*-> \(.*\)$'`
+ if expr "$link" : '/.*' > /dev/null; then
+ PRG="$link"
+ else
+ PRG=`dirname "$PRG"`"/$link"
+ fi
+done
+SAVED="`pwd`"
+cd "`dirname \"$PRG\"`/" >&-
+APP_HOME="`pwd -P`"
+cd "$SAVED" >&-
+
+CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
+
+# Determine the Java command to use to start the JVM.
+if [ -n "$JAVA_HOME" ] ; then
+ if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
+ # IBM's JDK on AIX uses strange locations for the executables
+ JAVACMD="$JAVA_HOME/jre/sh/java"
+ else
+ JAVACMD="$JAVA_HOME/bin/java"
+ fi
+ if [ ! -x "$JAVACMD" ] ; then
+ die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+ fi
+else
+ JAVACMD="java"
+ which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+fi
+
+# Increase the maximum file descriptors if we can.
+if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then
+ MAX_FD_LIMIT=`ulimit -H -n`
+ if [ $? -eq 0 ] ; then
+ if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
+ MAX_FD="$MAX_FD_LIMIT"
+ fi
+ ulimit -n $MAX_FD
+ if [ $? -ne 0 ] ; then
+ warn "Could not set maximum file descriptor limit: $MAX_FD"
+ fi
+ else
+ warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
+ fi
+fi
+
+# For Darwin, add options to specify how the application appears in the dock
+if $darwin; then
+ GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
+fi
+
+# For Cygwin, switch paths to Windows format before running java
+if $cygwin ; then
+ APP_HOME=`cygpath --path --mixed "$APP_HOME"`
+ CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
+
+ # We build the pattern for arguments to be converted via cygpath
+ ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
+ SEP=""
+ for dir in $ROOTDIRSRAW ; do
+ ROOTDIRS="$ROOTDIRS$SEP$dir"
+ SEP="|"
+ done
+ OURCYGPATTERN="(^($ROOTDIRS))"
+ # Add a user-defined pattern to the cygpath arguments
+ if [ "$GRADLE_CYGPATTERN" != "" ] ; then
+ OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
+ fi
+ # Now convert the arguments - kludge to limit ourselves to /bin/sh
+ i=0
+ for arg in "$@" ; do
+ CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
+ CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
+
+ if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
+ eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
+ else
+ eval `echo args$i`="\"$arg\""
+ fi
+ i=$((i+1))
+ done
+ case $i in
+ (0) set -- ;;
+ (1) set -- "$args0" ;;
+ (2) set -- "$args0" "$args1" ;;
+ (3) set -- "$args0" "$args1" "$args2" ;;
+ (4) set -- "$args0" "$args1" "$args2" "$args3" ;;
+ (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
+ (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
+ (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
+ (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
+ (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
+ esac
+fi
+
+# Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules
+function splitJvmOpts() {
+ JVM_OPTS=("$@")
+}
+eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS
+JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME"
+
+exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@"
diff --git a/compose-customisable-ui-example/gradlew.bat b/compose-customisable-ui-example/gradlew.bat
new file mode 100644
index 00000000..8a0b282a
--- /dev/null
+++ b/compose-customisable-ui-example/gradlew.bat
@@ -0,0 +1,90 @@
+@if "%DEBUG%" == "" @echo off
+@rem ##########################################################################
+@rem
+@rem Gradle startup script for Windows
+@rem
+@rem ##########################################################################
+
+@rem Set local scope for the variables with windows NT shell
+if "%OS%"=="Windows_NT" setlocal
+
+@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+set DEFAULT_JVM_OPTS=
+
+set DIRNAME=%~dp0
+if "%DIRNAME%" == "" set DIRNAME=.
+set APP_BASE_NAME=%~n0
+set APP_HOME=%DIRNAME%
+
+@rem Find java.exe
+if defined JAVA_HOME goto findJavaFromJavaHome
+
+set JAVA_EXE=java.exe
+%JAVA_EXE% -version >NUL 2>&1
+if "%ERRORLEVEL%" == "0" goto init
+
+echo.
+echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:findJavaFromJavaHome
+set JAVA_HOME=%JAVA_HOME:"=%
+set JAVA_EXE=%JAVA_HOME%/bin/java.exe
+
+if exist "%JAVA_EXE%" goto init
+
+echo.
+echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:init
+@rem Get command-line arguments, handling Windowz variants
+
+if not "%OS%" == "Windows_NT" goto win9xME_args
+if "%@eval[2+2]" == "4" goto 4NT_args
+
+:win9xME_args
+@rem Slurp the command line arguments.
+set CMD_LINE_ARGS=
+set _SKIP=2
+
+:win9xME_args_slurp
+if "x%~1" == "x" goto execute
+
+set CMD_LINE_ARGS=%*
+goto execute
+
+:4NT_args
+@rem Get arguments from the 4NT Shell from JP Software
+set CMD_LINE_ARGS=%$
+
+:execute
+@rem Setup the command line
+
+set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
+
+@rem Execute Gradle
+"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
+
+:end
+@rem End local scope for the variables with windows NT shell
+if "%ERRORLEVEL%"=="0" goto mainEnd
+
+:fail
+rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
+rem the _cmd.exe /c_ return code!
+if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
+exit /b 1
+
+:mainEnd
+if "%OS%"=="Windows_NT" endlocal
+
+:omega
diff --git a/compose-customisable-ui-example/settings.gradle.kts b/compose-customisable-ui-example/settings.gradle.kts
new file mode 100644
index 00000000..e67bcf5f
--- /dev/null
+++ b/compose-customisable-ui-example/settings.gradle.kts
@@ -0,0 +1,20 @@
+pluginManagement {
+ repositories {
+ gradlePluginPortal()
+ google()
+ mavenCentral()
+ }
+}
+
+dependencyResolutionManagement {
+ repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
+ repositories {
+ mavenCentral()
+ google()
+ maven(url = "https://nexus.scanbot.io/nexus/content/repositories/releases/")
+ maven(url = "https://nexus.scanbot.io/nexus/content/repositories/snapshots/")
+ }
+}
+
+include(":app")
+rootProject.name = "Scanbot Compose Customisable UI example"