Skip to content

Instantly share code, notes, and snippets.

@Tonnie-Dev
Created September 25, 2025 06:08
Show Gist options
  • Select an option

  • Save Tonnie-Dev/e1566a4eacd929850eb84f310afa0eb6 to your computer and use it in GitHub Desktop.

Select an option

Save Tonnie-Dev/e1566a4eacd929850eb84f310afa0eb6 to your computer and use it in GitHub Desktop.
QR Analysis Flow
package com.tonyxlab.qrcraft.presentation.screens.scan.components
import android.view.ViewGroup
import androidx.annotation.OptIn
import androidx.camera.camera2.interop.ExperimentalCamera2Interop
import androidx.camera.core.Camera
import androidx.camera.core.CameraSelector
import androidx.camera.core.FocusMeteringAction
import androidx.camera.core.ImageAnalysis
import androidx.camera.core.Preview
import androidx.camera.lifecycle.ProcessCameraProvider
import androidx.camera.view.PreviewView
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.viewinterop.AndroidView
import androidx.core.content.ContextCompat
import androidx.lifecycle.compose.LocalLifecycleOwner
import com.tonyxlab.qrcraft.R
import com.tonyxlab.qrcraft.data.QRCodeAnalyzer
import com.tonyxlab.qrcraft.domain.QrData
import com.tonyxlab.qrcraft.presentation.core.utils.spacing
import com.tonyxlab.qrcraft.presentation.screens.scan.handling.ScanUiState
import com.tonyxlab.qrcraft.presentation.theme.ui.OnOverlay
@OptIn(ExperimentalCamera2Interop::class)
@Composable
fun CameraPreview(
uiState: ScanUiState,
onScanSuccess: (QrData) -> Unit,
onAnalyzing: (Boolean) -> Unit,
modifier: Modifier = Modifier
) {
val lifecycleOwner = LocalLifecycleOwner.current
val context = LocalContext.current
val executor = ContextCompat.getMainExecutor(context)
AndroidView(
modifier = modifier,
factory = { ctx ->
PreviewView(ctx).apply {
layoutParams = ViewGroup.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.MATCH_PARENT
)
scaleType = PreviewView.ScaleType.FILL_CENTER
}
}
) { previewView ->
val cameraProviderFuture =
ProcessCameraProvider.getInstance(context)
cameraProviderFuture.addListener(
{
val analyzer = ImageAnalysis.Builder()
.setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST)
.build()
.also {
it.setAnalyzer(
executor,
QRCodeAnalyzer(
onCodeScanned = { data ->
onScanSuccess(data)
},
onAnalyzing = { active ->
onAnalyzing(active)
},
consumeOnce = true
)
)
}
val cameraProvider = cameraProviderFuture.get()
val preview = Preview.Builder()
.build()
.apply {
surfaceProvider = previewView.surfaceProvider
}
val selector = CameraSelector.DEFAULT_BACK_CAMERA
cameraProvider.unbindAll()
cameraProvider.bindToLifecycle(
lifecycleOwner, selector, preview, analyzer
)
val camera = cameraProvider.bindToLifecycle(
lifecycleOwner, selector, preview, analyzer
)
//lockCenterFocus(previewView, camera)
},
executor
)
}
if (uiState.isLoading) {
Column(
horizontalAlignment = Alignment.CenterHorizontally
) {
CircularProgressIndicator(
modifier = Modifier
.size(MaterialTheme.spacing.spaceLarge)
.padding(bottom = MaterialTheme.spacing.spaceMedium),
color = OnOverlay
)
Text(
text = stringResource(id = R.string.cap_text_loading),
style = MaterialTheme.typography.bodyLarge.copy(
color = OnOverlay
)
)
}
}
}
fun lockCenterFocus(previewView: PreviewView, camera: Camera) {
val factory = previewView.meteringPointFactory
val center = factory.createPoint(previewView.width / 2f, previewView.height / 2f)
val action = FocusMeteringAction.Builder(
center,
FocusMeteringAction.FLAG_AF or FocusMeteringAction.FLAG_AE
)
.setAutoCancelDuration(2, java.util.concurrent.TimeUnit.SECONDS)
.build()
camera.cameraControl.startFocusAndMetering(action)
}
@file:kotlin.OptIn(ExperimentalAtomicApi::class)
@file:OptIn(ExperimentalGetImage::class)
package com.tonyxlab.qrcraft.data
import androidx.annotation.OptIn
import androidx.camera.core.ExperimentalGetImage
import androidx.camera.core.ImageAnalysis
import androidx.camera.core.ImageProxy
import com.google.mlkit.vision.barcode.BarcodeScannerOptions
import com.google.mlkit.vision.barcode.BarcodeScanning
import com.google.mlkit.vision.barcode.common.Barcode
import com.google.mlkit.vision.common.InputImage
import com.tonyxlab.qrcraft.domain.QrData
import com.tonyxlab.qrcraft.domain.QrDataType
import kotlin.concurrent.atomics.AtomicBoolean
import kotlin.concurrent.atomics.ExperimentalAtomicApi
class QRCodeAnalyzer(
private val onCodeScanned: (QrData) -> Unit,
private val onAnalyzing: (Boolean) -> Unit = {},
private val consumeOnce: Boolean = true
) : ImageAnalysis.Analyzer {
private val onShotGuard = AtomicBoolean(false)
private val options = BarcodeScannerOptions.Builder()
.setBarcodeFormats(Barcode.FORMAT_QR_CODE)
.build()
private val scanner = BarcodeScanning.getClient(options)
override fun analyze(imageProxy: ImageProxy) {
val mediaImage = imageProxy.image ?: run {
imageProxy.close()
return
}
val image = InputImage.fromMediaImage(
mediaImage, imageProxy.imageInfo.rotationDegrees
)
scanner.process(image)
.addOnSuccessListener { barcodes ->
barcodes.ifEmpty { return@addOnSuccessListener }
onAnalyzing(true)
val barcode =
barcodes.maxByOrNull { it.boundingBox?.width() ?: 0 } ?: barcodes.first()
val data = barcode.toQrData()
if (!consumeOnce || onShotGuard.compareAndSet(
expectedValue = false,
newValue = true
)
) {
onCodeScanned(data)
}
}
.addOnCompleteListener {
imageProxy.close()
}
}
private fun Barcode.toQrData(): QrData {
this.url?.let { url ->
return QrData(
displayName = "Link",
prettifiedData = url.url ?: displayValue.orEmpty(),
qrDataType = QrDataType.LINK,
rawDataValue = rawValue.orEmpty()
)
}
this.contactInfo?.let { contactInfo ->
val name = listOfNotNull(
contactInfo.name?.formattedName,
contactInfo.name?.first,
// contactInfo.name?.last
).joinToString(" ")
.ifBlank { "Contact" }
val pieces = buildList {
add(name)
contactInfo.emails.firstOrNull()?.address?.let { add(it) }
contactInfo.phones.firstOrNull()?.number?.let { add(it) }
contactInfo.organization?.firstOrNull()
?.let { add(it) }
contactInfo.addresses.firstOrNull()?.addressLines?.joinToString()
?.let { add(it) }
}
return QrData(
displayName = "Contact",
prettifiedData = pieces.joinToString("\n")
.ifBlank { displayValue.orEmpty() },
qrDataType = QrDataType.CONTACT,
rawDataValue = rawValue.orEmpty()
)
}
this.phone?.let { phone ->
return QrData(
displayName = "Phone Number",
prettifiedData = phone.number ?: displayValue.orEmpty(),
qrDataType = QrDataType.PHONE_NUMBER,
rawDataValue = rawValue.orEmpty()
)
}
this.geoPoint?.let { geoPoint ->
val latLng = "${geoPoint.lat}, ${geoPoint.lng}"
return QrData(
displayName = "Geolocation",
prettifiedData = latLng,
qrDataType = QrDataType.GEOLOCATION,
rawDataValue = rawValue.orEmpty()
)
}
this.wifi?.let { wiFi ->
val security = when (wiFi.encryptionType) {
Barcode.WiFi.TYPE_OPEN -> "Open"
Barcode.WiFi.TYPE_WEP -> "WEP"
Barcode.WiFi.TYPE_WPA -> "WPA/WPA2"
else -> "Unknown"
}
val block = buildString {
appendLine("SSID: ${wiFi.ssid ?: "--"}")
appendLine("Password: ${wiFi.password ?: "--"}")
append("Encryption: $security")
}
return QrData(
displayName = "Wi-Fi",
prettifiedData = block,
qrDataType = QrDataType.WIFI,
rawDataValue = rawValue.orEmpty()
)
}
return QrData(
displayName = "Text",
prettifiedData = displayValue ?: rawValue.orEmpty(),
qrDataType = QrDataType.TEXT,
rawDataValue = rawValue.orEmpty()
)
}
}
package com.tonyxlab.qrcraft.presentation.screens.scan.components
import androidx.compose.foundation.Canvas
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.CornerRadius
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Size
import androidx.compose.ui.graphics.BlendMode
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.CompositingStrategy
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import com.tonyxlab.qrcraft.R
import com.tonyxlab.qrcraft.presentation.core.utils.spacing
import com.tonyxlab.qrcraft.presentation.theme.ui.OnOverlay
import com.tonyxlab.qrcraft.presentation.theme.ui.Overlay
import kotlin.math.min
@Composable
fun ScanOverlay(
modifier: Modifier = Modifier,
isLoading: Boolean
) {
Box(modifier = modifier) {
Canvas(
modifier = Modifier
.matchParentSize()
.graphicsLayer {
compositingStrategy = CompositingStrategy.Offscreen
}) {
val w = size.width
val h = size.height
val punchHoleFraction = .9f
val frameSquareDimen = min(w, h) * punchHoleFraction
val left = (w - frameSquareDimen) / 2f
val top = (h - frameSquareDimen) / 2f
val radius = 20.dp.toPx()
// Draw scrim
drawRect(Overlay)
// Punch a transparent hole for the frame
drawRoundRect(
color = Color.Red,
topLeft = Offset(left, top),
size = Size(width = frameSquareDimen, height = frameSquareDimen),
cornerRadius = CornerRadius(radius, radius),
blendMode = BlendMode.Clear
)
}
// Hint Text
Column(
Modifier
.fillMaxSize()
.padding(horizontal = 24.dp),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Top
) {
Spacer(Modifier.fillMaxHeight(0.25f))
Text(
text = stringResource(id = R.string.cap_text_point_camera),
style = MaterialTheme.typography.titleSmall.copy(color = OnOverlay),
textAlign = TextAlign.Center
)
}
/* if (isLoading) {
Column(
modifier = Modifier.align(Alignment.Center),
horizontalAlignment = Alignment.CenterHorizontally
) {
CircularProgressIndicator(
modifier = Modifier
.size(MaterialTheme.spacing.spaceLarge)
.padding(bottom = MaterialTheme.spacing.spaceMedium),
color = OnOverlay
)
Text(
text = stringResource(id = R.string.cap_text_loading),
style = MaterialTheme.typography.bodyLarge.copy(
color = OnOverlay
)
)
}
}*/
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment