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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 14 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,24 +15,23 @@ Supported Compose version:
| 1.6.x | 0.1.0+ |
| 1.7 | 0.2 - 0.3 |
| 1.8 | 0.4.0 |
| 1.9 | 0.5.0 |
| 1.9 | 0.5.0 - 0.6.x |

# Dependency
Add the dependency to your commonMain sourceSet (KMP) / Android dependencies (android only):
```kotlin
implementation("io.github.kalinjul.easyqrscan:scanner:0.5.0")
implementation("io.github.kalinjul.easyqrscan:scanner:0.6.0")
```

Or, for your libs.versions.toml:
```toml
[versions]
easyqrscan = "0.5.0"
easyqrscan = "0.6.0"
[libraries]
easyqrscan = { module = "io.github.kalinjul.easyqrscan:scanner", version.ref = "easyqrscan" }
```

# Usage
## Camera Permissions
# Setup Camera Permissions
Include this at root level in your AndroidManifest.xml:
```xml
<uses-feature android:name="android.hardware.camera"/>
Expand All @@ -43,10 +42,18 @@ Include this at root level in your AndroidManifest.xml:
Add this key to the Info.plist in your xcode project:
```NSCameraUsageDescription``` and provide a description as value

## Compose UI
# Usage

The scanner is included by calling a single composable function ```Scanner()``` or ```ScannerWithPermissions```:

```kotlin
// basic permission handling included:
ScannerWithPermissions(onScanned = { println(it); true }, types = listOf(CodeType.QR))
ScannerWithPermissions(
onScanned = { println(it); true }, // return true to disable the scanner, false to continue scanning
types = listOf(CodeType.QR),
cameraPosition = CameraPosition.BACK,
enableTorch = false // toggle this to enable/disable the flashlight
)

// or, if you handle permissions yourself:
Scanner(onScanned = { println(it); true }, types = listOf(CodeType.QR))
Expand Down
35 changes: 35 additions & 0 deletions camerautils/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import org.publicvalue.convention.config.configureIosTargets

plugins {
id("org.publicvalue.convention.android.library")
id("org.publicvalue.convention.kotlin.multiplatform.mobile")
id("org.publicvalue.convention.centralPublish")
id("org.publicvalue.convention.compose.multiplatform")
}

description = "Compose Multiplatform Camera Utilities for Android/iOS"

kotlin {
configureIosTargets()
jvm()

sourceSets {
commonMain {
dependencies {
implementation(compose.runtime)
implementation(compose.foundation)
}
}

androidMain {
dependencies {
implementation(libs.androidx.camera.camera2)
}
}

jvmMain {
dependencies {
}
}
}
}
29 changes: 29 additions & 0 deletions camerautils/src/androidMain/kotlin/AndroidCameraUtils.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package org.publicvalue.multiplatform.qrcode

import android.content.Context
import android.hardware.camera2.CameraCharacteristics
import android.hardware.camera2.CameraManager
import androidx.core.content.ContextCompat

class AndroidCameraUtils(context: Context) : CameraUtils {
private val cameraManager = ContextCompat.getSystemService(context,CameraManager::class.java)

override fun setTorchMode(
cameraPosition: CameraPosition,
value: Boolean
): Boolean {
val cameraId = cameraManager?.cameraIdList?.find {
cameraManager.getCameraCharacteristics(it).get(CameraCharacteristics.LENS_FACING) == cameraPosition.toCharacteristics()
}
if (cameraId == null) {
return false
}
cameraId.let { cameraManager.setTorchMode(it, value) }
return true
}
}

fun CameraPosition.toCharacteristics() = when(this) {
CameraPosition.FRONT -> CameraCharacteristics.LENS_FACING_FRONT
CameraPosition.BACK -> CameraCharacteristics.LENS_FACING_BACK
}
13 changes: 13 additions & 0 deletions camerautils/src/androidMain/kotlin/CameraUtils.android.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package org.publicvalue.multiplatform.qrcode

import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.platform.LocalContext

@Composable
actual fun rememberCameraUtils(): CameraUtils {
val context = LocalContext.current
return remember(context) {
AndroidCameraUtils(context)
}
}
10 changes: 10 additions & 0 deletions camerautils/src/commonMain/kotlin/CameraUtils.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package org.publicvalue.multiplatform.qrcode

import androidx.compose.runtime.Composable

interface CameraUtils {
fun setTorchMode(cameraPosition: CameraPosition, value: Boolean): Boolean
}

@Composable
expect fun rememberCameraUtils(): CameraUtils
21 changes: 21 additions & 0 deletions camerautils/src/iosMain/kotlin/AVCaptureDevice.getDevice.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package org.publicvalue.multiplatform.qrcode

import platform.AVFoundation.AVCaptureDevice
import platform.AVFoundation.AVCaptureDevicePositionBack
import platform.AVFoundation.AVCaptureDevicePositionFront
import platform.AVFoundation.AVMediaTypeVideo
import platform.AVFoundation.position

fun AVCaptureDevice.Companion.getDevice(cameraPosition: CameraPosition): AVCaptureDevice? {
val devices = AVCaptureDevice.devicesWithMediaType(AVMediaTypeVideo).map { it as AVCaptureDevice }

return devices.firstOrNull { device ->
when(cameraPosition) {
CameraPosition.FRONT -> device.position == AVCaptureDevicePositionFront
CameraPosition.BACK -> device.position == AVCaptureDevicePositionBack
}
} ?: run {
println("Could not find camera with position: $cameraPosition, using default camera")
AVCaptureDevice.defaultDeviceWithMediaType(AVMediaTypeVideo)
}
}
8 changes: 8 additions & 0 deletions camerautils/src/iosMain/kotlin/CameraUtils.ios.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package org.publicvalue.multiplatform.qrcode

import androidx.compose.runtime.Composable

@Composable
actual fun rememberCameraUtils(): CameraUtils {
return IosCameraUtils()
}
47 changes: 47 additions & 0 deletions camerautils/src/iosMain/kotlin/IosCameraUtils.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
package org.publicvalue.multiplatform.qrcode

import kotlinx.cinterop.BetaInteropApi
import kotlinx.cinterop.ExperimentalForeignApi
import kotlinx.cinterop.ObjCObjectVar
import kotlinx.cinterop.alloc
import kotlinx.cinterop.memScoped
import kotlinx.cinterop.ptr
import kotlinx.cinterop.value
import platform.AVFoundation.AVCaptureDevice
import platform.AVFoundation.AVCaptureTorchModeOff
import platform.AVFoundation.AVCaptureTorchModeOn
import platform.AVFoundation.hasTorch
import platform.AVFoundation.setTorchMode
import platform.Foundation.NSError

class IosCameraUtils: CameraUtils {
@OptIn(ExperimentalForeignApi::class, BetaInteropApi::class)
override fun setTorchMode(
cameraPosition: CameraPosition,
value: Boolean
): Boolean {
val device = AVCaptureDevice.getDevice(cameraPosition)
if (device == null || !device.hasTorch()) {
return false
}

memScoped {
val error = alloc<ObjCObjectVar<NSError?>>()
device.lockForConfiguration(error.ptr)
if (error.value != null) {
throw Exception("Error locking device: ${error.value}")
}
}
device.setTorchMode(
if (value) {
AVCaptureTorchModeOn
} else {
AVCaptureTorchModeOff
}
)

device.unlockForConfiguration()
return true
}

}
16 changes: 16 additions & 0 deletions camerautils/src/jvmMain/kotlin/CameraUtils.jvm.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package org.publicvalue.multiplatform.qrcode

import androidx.compose.runtime.Composable

@Composable
actual fun rememberCameraUtils(): CameraUtils {
return object : CameraUtils {
override fun setTorchMode(
cameraPosition: CameraPosition,
value: Boolean
): Boolean {
println("CameraUtils not implemented on JVM")
return false
}
}
}
45 changes: 38 additions & 7 deletions sample-app/shared/src/commonMain/kotlin/MainView.kt
Original file line number Diff line number Diff line change
@@ -1,19 +1,23 @@
package org.publicvalue.multiplatform.qrcode.sample

import androidx.compose.foundation.layout.Arrangement
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.material3.Button
import androidx.compose.material3.Scaffold
import androidx.compose.material3.SnackbarDuration
import androidx.compose.material3.SnackbarHost
import androidx.compose.material3.SnackbarHostState
import androidx.compose.material3.Switch
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import kotlinx.coroutines.Job
Expand All @@ -25,6 +29,7 @@ import kotlin.time.Duration.Companion.minutes
import kotlin.time.Duration.Companion.seconds
import kotlin.time.TimeMark
import kotlin.time.TimeSource
import org.publicvalue.multiplatform.qrcode.rememberCameraUtils

@Composable
fun MainView() {
Expand All @@ -38,11 +43,36 @@ fun MainView() {
) {
Column(modifier = Modifier.padding(it)) {
Text("Scan QR-Code below")
var scannerVisible by remember {mutableStateOf(false)}
Button(onClick = {
scannerVisible = !scannerVisible
}) {
Text("Toggle scanner (visible: $scannerVisible)")
var scannerVisible by remember { mutableStateOf(false) }
var enableTorch by remember { mutableStateOf(false) }

val cameraUtils = rememberCameraUtils()
Row(
modifier = Modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween
) {
Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(8.dp)) {
Text("Toggle scanner")
Switch(
checked = scannerVisible,
onCheckedChange = {
scannerVisible = it
}
)
}
Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(8.dp)) {
Text("Toggle torch")
Switch(
checked = enableTorch,
onCheckedChange = {
enableTorch = it
if (!scannerVisible) { // android needs separate handling when capturing video
cameraUtils.setTorchMode(CameraPosition.BACK, it)
}
}
)
}
}
var lastCode by remember { mutableStateOf<String?>(null)}
var snackbarJob by remember { mutableStateOf<Job?>(null)}
Expand All @@ -68,7 +98,8 @@ fun MainView() {
false // continue scanning
},
types = listOf(CodeType.QR),
cameraPosition = CameraPosition.BACK
cameraPosition = CameraPosition.BACK,
enableTorch = enableTorch,
)
}
}
Expand Down
1 change: 1 addition & 0 deletions scanner/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ kotlin {
implementation(compose.runtime)
implementation(compose.foundation)
implementation(compose.material3)
api(projects.camerautils)
}
}

Expand Down
Loading