Skip to content

Commit

Permalink
Merge pull request #323 from Fabi019/257-custom-scanner-overlay-area
Browse files Browse the repository at this point in the history
Custom scan area overlay
  • Loading branch information
Fabi019 authored Dec 1, 2024
2 parents 406a641 + 7ea8dd5 commit e0b22c3
Show file tree
Hide file tree
Showing 6 changed files with 263 additions and 21 deletions.
241 changes: 224 additions & 17 deletions app/src/main/java/dev/fabik/bluetoothhid/ui/CameraPreview.kt
Original file line number Diff line number Diff line change
Expand Up @@ -18,16 +18,23 @@ import androidx.compose.animation.core.spring
import androidx.compose.animation.core.tween
import androidx.compose.animation.core.updateTransition
import androidx.compose.foundation.Canvas
import androidx.compose.foundation.gestures.detectDragGestures
import androidx.compose.foundation.layout.absoluteOffset
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.DragIndicator
import androidx.compose.material.icons.filled.Error
import androidx.compose.material.icons.filled.OpenInFull
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.IconButtonDefaults
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableFloatStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
Expand All @@ -47,13 +54,16 @@ import androidx.compose.ui.graphics.drawscope.Stroke
import androidx.compose.ui.graphics.drawscope.clipPath
import androidx.compose.ui.graphics.nativeCanvas
import androidx.compose.ui.graphics.toArgb
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.viewinterop.AndroidView
import androidx.core.content.ContextCompat
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.compose.LocalLifecycleOwner
import androidx.lifecycle.viewmodel.compose.viewModel
import com.google.common.util.concurrent.FutureCallback
import com.google.common.util.concurrent.Futures
import com.google.mlkit.vision.barcode.BarcodeScannerOptions
import com.google.mlkit.vision.barcode.ZoomSuggestionOptions
import dev.fabik.bluetoothhid.LocalJsEngineService
Expand All @@ -65,9 +75,14 @@ import dev.fabik.bluetoothhid.utils.PreferenceStore
import dev.fabik.bluetoothhid.utils.RequiresModuleInstallation
import dev.fabik.bluetoothhid.utils.getPreference
import dev.fabik.bluetoothhid.utils.rememberPreference
import dev.fabik.bluetoothhid.utils.setPreference
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import java.util.concurrent.Executors
import kotlin.math.absoluteValue
import kotlin.math.roundToInt


@Composable
Expand Down Expand Up @@ -238,6 +253,9 @@ fun CameraViewModel.CameraPreview(

runCatching {
cameraController.bindToLifecycle(lifecycleOwner)

// Enable only the image analysis use case
cameraController.setEnabledUseCases(CameraController.IMAGE_ANALYSIS)
}.onFailure {
Log.e("CameraPreview", "Failed to bind camera", it)
errorDialog.open()
Expand All @@ -253,23 +271,36 @@ fun CameraViewModel.CameraPreview(
Log.d("CameraPreview", "Focusing: $isFocusing ($it)")
}

cameraController.initializationFuture.addListener({
// Enable only the image analysis use case
cameraController.setEnabledUseCases(CameraController.IMAGE_ANALYSIS)

// Attach PreviewView after we know the camera is available.
previewView.controller = cameraController
previewView.setOnTouchListener { _, event ->
if (event.action == MotionEvent.ACTION_DOWN)
focusTouchPoint = Offset(event.x, event.y)
false
}

// Camera is ready
onCameraReady(cameraController)
Futures.addCallback(
cameraController.initializationFuture,
object : FutureCallback<Void> {
override fun onSuccess(result: Void?) {
runCatching {
// Attach PreviewView after we know the camera is available.
previewView.controller = cameraController
previewView.setOnTouchListener { _, event ->
if (event.action == MotionEvent.ACTION_DOWN)
focusTouchPoint = Offset(event.x, event.y)
false
}

// Camera is ready
onCameraReady(cameraController)

initialized = true
}.onFailure {
Log.e("CameraPreview", "Failed to bind preview", it)
errorDialog.open()
}
}

initialized = true
}, ContextCompat.getMainExecutor(context))
override fun onFailure(t: Throwable) {
Log.e("CameraPreview", "Failed to initialize camera", t)
errorDialog.open()
}
},
context.mainExecutor
)
}

Lifecycle.Event.ON_PAUSE -> {
Expand Down Expand Up @@ -368,6 +399,24 @@ fun CameraViewModel.OverlayCanvas() {
)
}

2 -> {
val pos = overlayPosition
val size = overlaySize

if (pos != null && size != null)
Rect(
pos + Offset(
x - size.width.absoluteValue,
y - size.height.absoluteValue
),
pos + Offset(
x + size.width.absoluteValue,
y + size.height.absoluteValue
)
)
else Rect.Zero
}

// Square for scanning qr codes
else -> {
val length = if (landscape) this.size.height * 0.6f else this.size.width * 0.8f
Expand Down Expand Up @@ -418,6 +467,41 @@ fun CameraViewModel.OverlayCanvas() {
}

drawPath(path, color = Color.Blue, style = Stroke(5f))

// Draw horizontal line
points?.let {
val leftMiddle =
Offset((it[0].first + it[3].first) / 2f, (it[0].second + it[3].second) / 2f)
val rightMiddle =
Offset((it[1].first + it[2].first) / 2f, (it[1].second + it[2].second) / 2f)
val hDist = (rightMiddle - leftMiddle).getDistanceSquared()

val topMiddle =
Offset((it[0].first + it[1].first) / 2f, (it[0].second + it[1].second) / 2f)
val bottomMiddle =
Offset((it[3].first + it[2].first) / 2f, (it[3].second + it[2].second) / 2f)
val vDist = (bottomMiddle - topMiddle).getDistanceSquared()

val relDiff = (hDist - vDist) / (hDist + vDist)
if (relDiff > 0.05) {
drawLine(
Color.Red,
leftMiddle,
rightMiddle
)
} else if (relDiff < -0.05) {
drawLine(
Color.Red,
topMiddle,
bottomMiddle
)
}

// drawCircle(Color.Yellow, 10f, leftMiddle)
// drawCircle(Color.Green, 10f, rightMiddle)
// drawCircle(Color.Cyan, 10f, topMiddle)
// drawCircle(Color.Magenta, 10f, bottomMiddle)
}
}

// Draw the focus circle if currently focusing
Expand All @@ -438,6 +522,118 @@ fun CameraViewModel.OverlayCanvas() {
drawDebugOverlay(drawContext.canvas.nativeCanvas, this.size)
}
}

// Show the adjust buttons
if (restrictArea && overlayType == 2) {
CustomOverlayButtons()
}
}

@Composable
private fun CameraViewModel.CustomOverlayButtons() {
val context = LocalContext.current
var posOffsetX by remember {
mutableFloatStateOf(runBlocking {
context.getPreference(
PreferenceStore.OVERLAY_POS_X
).first()
})
}
var posOffsetY by remember {
mutableFloatStateOf(runBlocking {
context.getPreference(
PreferenceStore.OVERLAY_POS_Y
).first()
})
}
var sizeOffsetX by remember {
mutableFloatStateOf(runBlocking {
context.getPreference(
PreferenceStore.OVERLAY_WIDTH
).first()
})
}
var sizeOffsetY by remember {
mutableFloatStateOf(runBlocking {
context.getPreference(
PreferenceStore.OVERLAY_HEIGHT
).first()
})
}

LaunchedEffect(Unit) {
overlayPosition = Offset(posOffsetX, posOffsetY)
overlaySize = Size(sizeOffsetX, sizeOffsetY)
}

fun saveState() {
runBlocking {
context.setPreference(PreferenceStore.OVERLAY_POS_X, posOffsetX)
context.setPreference(PreferenceStore.OVERLAY_POS_Y, posOffsetY)
context.setPreference(PreferenceStore.OVERLAY_WIDTH, sizeOffsetX)
context.setPreference(PreferenceStore.OVERLAY_HEIGHT, sizeOffsetY)
}
}

fun reset() {
posOffsetX = PreferenceStore.OVERLAY_POS_X.defaultValue
posOffsetY = PreferenceStore.OVERLAY_POS_Y.defaultValue
sizeOffsetX = PreferenceStore.OVERLAY_WIDTH.defaultValue
sizeOffsetY = PreferenceStore.OVERLAY_HEIGHT.defaultValue

overlayPosition = Offset(posOffsetX, posOffsetY)
overlaySize = Size(sizeOffsetX, sizeOffsetY)

saveState()
}

IconButton(
onClick = { reset() },
colors = IconButtonDefaults.iconButtonColors(Color.Black.copy(alpha = 0.5f)),
modifier = Modifier
.absoluteOffset {
IntOffset(
(posOffsetX + sizeOffsetX).roundToInt(),
(posOffsetY + sizeOffsetY).roundToInt()
)
}
.pointerInput(Unit) {
detectDragGestures(onDragEnd = {
saveState()
}) { change, dragAmount ->
change.consume()
sizeOffsetX += dragAmount.x
sizeOffsetY += dragAmount.y
overlaySize = Size(sizeOffsetX, sizeOffsetY)
}
}
) {
Icon(Icons.Default.OpenInFull, "Modify size")
}

IconButton(
onClick = { reset() },
colors = IconButtonDefaults.iconButtonColors(Color.Black.copy(alpha = 0.5f)),
modifier = Modifier
.absoluteOffset {
IntOffset(
(posOffsetX - sizeOffsetX).roundToInt(),
(posOffsetY + sizeOffsetY).roundToInt()
)
}
.pointerInput(Unit) {
detectDragGestures(onDragEnd = {
saveState()
}) { change, dragAmount ->
change.consume()
posOffsetX += dragAmount.x
posOffsetY += dragAmount.y
overlayPosition = Offset(posOffsetX, posOffsetY)
}
}
) {
Icon(Icons.Default.DragIndicator, "Modify position")
}
}

fun CameraViewModel.drawDebugOverlay(canvas: NativeCanvas, size: Size) {
Expand Down Expand Up @@ -488,6 +684,17 @@ fun CameraViewModel.drawDebugOverlay(canvas: NativeCanvas, size: Size) {
}
)

// Draw the custom selection
canvas.drawText(
"Selector size: ${overlaySize?.width?.roundToInt()}x${overlaySize?.height?.roundToInt()} at (${overlayPosition?.x?.roundToInt()}, ${overlayPosition?.y?.roundToInt()})",
10f,
y + 200f,
Paint().apply {
color = Color.White.toArgb()
textSize = 50f
}
)

// Draw the histogram
fun drawHistogram(values: Iterable<Float>, increment: Float, paint: Paint) {
val path = android.graphics.Path()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,9 @@ class CameraViewModel : ViewModel() {
var lastSourceRes: Size? = null
var lastPreviewRes: Size? = null

var overlayPosition by mutableStateOf<Offset?>(null)
var overlaySize by mutableStateOf<androidx.compose.ui.geometry.Size?>(null)

// TODO: remove - no longer used for transformation
var scale = 1f
var transX = 0f
Expand Down Expand Up @@ -104,16 +107,39 @@ class CameraViewModel : ViewModel() {
// Filter out codes without value
it.rawBytes != null && !it.rawValue.isNullOrEmpty() && !it.displayValue.isNullOrEmpty()
}.filter {
// Filter if they are within the scan area
it.cornerPoints?.map { p ->
val points = it.cornerPoints?.map { p ->
Offset(p.x * scale - transX, p.y * scale - transY)
}?.forEach { o ->
}

// Filter if the edge points are within the scan area
points?.forEach { o ->
if (fullyInside && !scanRect.contains(o)) {
return@filter false
} else if (scanRect.contains(o)) {
}
if (!fullyInside && scanRect.contains(o)) {
return@filter true
}
}

// Check if the center lines are included
points?.let {
if (!fullyInside) {
val leftMiddle = (it[0] + it[3]) / 2f
val rightMiddle = (it[1] + it[2]) / 2f

if (scanRect.contains(leftMiddle) && scanRect.contains(rightMiddle)) {
return@filter true
}

val topMiddle = (it[0] + it[1]) / 2f
val bottomMiddle = (it[3] + it[2]) / 2f

if (scanRect.contains(topMiddle) && scanRect.contains(bottomMiddle)) {
return@filter true
}
}
}

fullyInside
}.filter {
// Filter by regex
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,12 @@ open class PreferenceStore {
val PRIVATE_MODE = booleanPreferencesKey("private_mode") defaultsTo false

val DEVELOPER_MODE = booleanPreferencesKey("developer_mode") defaultsTo BuildConfig.DEBUG

// Utility preferences
val OVERLAY_POS_X = floatPreferencesKey("overlay_pos_x") defaultsTo 0.0f
val OVERLAY_POS_Y = floatPreferencesKey("overlay_pos_y") defaultsTo 0.0f
val OVERLAY_WIDTH = floatPreferencesKey("overlay_width") defaultsTo 100.0f
val OVERLAY_HEIGHT = floatPreferencesKey("overlay_height") defaultsTo 100.0f
}
}

Expand Down
1 change: 1 addition & 0 deletions app/src/main/res/values-de-rDE/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,7 @@
<string-array name="overlay_values">
<item>Quadratisch (QR-Code)</item>
<item>Rechteckig (Barcode)</item>
<item>Benutzerdefiniert</item>
</string-array>
<string-array name="scan_freq_values">
<item>Am Schnellsten</item>
Expand Down
Loading

0 comments on commit e0b22c3

Please sign in to comment.