QR Scanner with ML Kit , Jetpack Compose

Eang Tithsophorn
8 min readApr 7, 2023

--

I believe there is no blog and source code out there that include all this even with the view for you.

I’m working hard on create this for helping you guys. In case you want to know me more here is my site.

If you are looking for QR Generator, Please check here.

Note: I kinda into Kotlin even my Gradle (kts)

Theese are my usage:

  • Compose
  • Material 3
  • CameraX
  • MLKit
  • Custom canvas path for qr frame view.

Let’s start with dependencies

dependencies {
val camerax_version = "1.3.0-alpha04"
implementation("androidx.camera:camera-view:${camerax_version}")
implementation("androidx.camera:camera-lifecycle:${camerax_version}")
implementation("androidx.camera:camera-camera2:${camerax_version}")
implementation("com.google.mlkit:barcode-scanning:17.1.0")

// camera permission or any onther permissions
implementation("com.google.accompanist:accompanist-permissions:0.30.1")
}

Please add the permission for use case in android manifest

Camera permission

<uses-feature android:name="android.hardware.camera" />
<uses-permission android:name="android.permission.CAMERA" />

Gallery or storage permission

<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.ACCESS_MEDIA_LOCATION" />

Now Start with permission code

@OptIn(ExperimentalPermissionsApi::class)
@Composable
fun KotlinPermission(
permissionStatus: PermissionStatus,
permissionNotAvailableContent: @Composable () -> Unit = {},
content: @Composable () -> Unit = {}
) {
when (permissionStatus) {
is PermissionStatus.Denied -> {
permissionNotAvailableContent.invoke()
}
PermissionStatus.Granted -> content.invoke()
}
}

And also image picker function

@OptIn(ExperimentalPermissionsApi::class)
@Composable
fun OpenGallery(
onImageUri: (Uri?) -> Unit = { }
) {
val launcher = rememberLauncherForActivityResult(
contract = ActivityResultContracts.GetContent(),
onResult = { uri: Uri? ->
onImageUri(uri)
}
)

@Composable
fun LaunchGallery() {
SideEffect {
launcher.launch("image/*")
}
}

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
val permissionState = rememberPermissionState(
permission = Manifest.permission.ACCESS_MEDIA_LOCATION
)

KotlinPermission(
permissionStatus = permissionState.status,
permissionNotAvailableContent = {
LaunchGallery()
},
) {
LaunchGallery()
}
} else {
LaunchGallery()
}
}

I want to sperate the preview from cameraX to another class by name it CameraSetup.kt

object CameraSetup {
fun init(context: Context): PreviewView {
val cameraExecutor = Executors.newSingleThreadExecutor()
val previewView = PreviewView(context).also {
it.scaleType = PreviewView.ScaleType.FILL_CENTER
}
val cameraProviderFuture = ProcessCameraProvider.getInstance(context)
cameraProviderFuture.addListener({
val cameraProvider: ProcessCameraProvider =
cameraProviderFuture.get()

val preview = Preview.Builder()
.setTargetAspectRatio(AspectRatio.RATIO_4_3)
.build()
.also {
it.setSurfaceProvider(previewView.surfaceProvider)
}

val imageCapture = ImageCapture.Builder().build()

val imageAnalyzer = ImageAnalysis.Builder()
.setTargetAspectRatio(AspectRatio.RATIO_4_3)
.setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST)
.build()
.also {
it.setAnalyzer(
cameraExecutor,
BarcodeAnalyser { uiSate ->
Toast.makeText(
context,
"Barcode found! ${uiSate}",
Toast.LENGTH_LONG
).show()
},
)
}
try {
// Unbind use cases before rebinding
cameraProvider.unbindAll()
// Bind use cases to camera
cameraProvider.bindToLifecycle(
context as ComponentActivity,
CameraSelector.DEFAULT_BACK_CAMERA,
preview,
imageCapture,
imageAnalyzer
)

} catch (exc: Exception) {
Log.e("DEBUG", "Use case binding failed", exc)
}
}, ContextCompat.getMainExecutor(context))
return previewView
}
}

Now we combine all code into one

@OptIn(ExperimentalPermissionsApi::class)
@Composable
fun ScanScreen() {
val permissionState = rememberPermissionState(
permission = Manifest.permission.CAMERA
)
val context = LocalContext.current
KotlinPermission(
permissionStatus = permissionState.status,
permissionNotAvailableContent = {
LaunchedEffect(
key1 = permissionState.status.isGranted,
block = {
permissionState.launchPermissionRequest()
},
)
Box(
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center
) {
Text(
text = "App is need for permission. Allowed permission in setting , By Clicked here",
modifier = Modifier
.padding(16.dp)
.clickable {
context.navigateToAppSettings()
},
textAlign = TextAlign.Center,
)
}
},
) {
Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
var isShowGallery by remember { mutableStateOf(false) }
var imageUri by remember { mutableStateOf<Uri?>(null) }
if (imageUri != null) {
Image(
modifier = Modifier.fillMaxSize(),
painter = rememberAsyncImagePainter(model = imageUri),
contentDescription = "Captured image"
)
Button(
modifier = Modifier.align(Alignment.BottomCenter),
onClick = {
imageUri = null
}
) {
Text("clear image")
}
} else {
if (isShowGallery) {
OpenGallery(
onImageUri = {
imageUri = it
MLKitHelper.analyseFor(imageUri, context, callback = {
Toast.makeText(context, "Barcode found ${it}", Toast.LENGTH_LONG)
.show()
})
isShowGallery = false
},
)
} else {
AndroidView(
{ context ->
CameraSetup.init(context)
},
modifier = Modifier
.fillMaxSize()
)
Text(
"Upload QR",
modifier = Modifier
.drawWithCache {
onDrawBehind {
drawRoundRect(
Brush.linearGradient(
listOf(
Color.White,
Color.Cyan
)
),
cornerRadius = CornerRadius(16.dp.toPx())
)
}
}
.padding(horizontal = 16.dp, vertical = 8.dp)
.align(Alignment.BottomCenter)
.clickable {
isShowGallery = true
},
)
}
}
}
}
}

Let’s run all of this!

does it work?

Want give me a clap? just await for next with canvas

Now We start with custom canvas view

With all of this by call drawWithContent is come in handy

val overlayWidth = size.width
val overlayHeight = size.height
val boxWidth = overlayWidth * 0.65
val boxHeight = overlayHeight * 0.35
val cx = overlayWidth / 2
val cy = overlayHeight / 2
val rectF = RectF(
(cx - boxWidth / 2).toFloat(),
(cy - boxHeight / 2).toFloat(),
(cx + boxWidth / 2).toFloat(),
(cy + boxHeight / 2).toFloat()
)
val scrimPaint: Paint = Paint().apply {
color = Color(0xFF99000000)
}
val boxPaint: Paint = Paint().apply {
color = Color.White
style = PaintingStyle.Stroke
strokeWidth = 4f
}
val eraserPaint: Paint = Paint().apply {
strokeWidth = boxPaint.strokeWidth
blendMode = BlendMode.Clear
}

drawIntoCanvas { canvas ->
drawContent()
canvas.drawPath(
Path(),
Paint().apply {
strokeWidth = 4f
color = Color.White
},
)
canvas.drawRect(
left = 0f,
top = 0f,
right = overlayWidth,
bottom = overlayHeight,
paint = scrimPaint
)
eraserPaint.style = PaintingStyle.Fill
canvas.drawRoundRect(
left = rectF.left,
top = rectF.top,
right = rectF.right,
bottom = rectF.bottom,
8f,
8f,
eraserPaint
)
eraserPaint.style = PaintingStyle.Stroke
canvas.drawRoundRect(
left = rectF.left,
top = rectF.top,
right = rectF.right,
bottom = rectF.bottom,
8f,
8f,
eraserPaint
)
canvas.drawRoundRect(
left = rectF.left,
top = rectF.top,
right = rectF.right,
bottom = rectF.bottom,
8f,
8f,
eraserPaint
)
// full frame view with stroke view
canvas.drawRoundRect(
left = rectF.left,
top = rectF.top,
right = rectF.right,
bottom = rectF.bottom,
8f,
8f,
boxPaint
)
}

let’s bring it to where it belong!

@OptIn(ExperimentalPermissionsApi::class)
@Composable
fun ScanScreen() {
val permissionState = rememberPermissionState(
permission = Manifest.permission.CAMERA
)
val context = LocalContext.current
KotlinPermission(
permissionStatus = permissionState.status,
permissionNotAvailableContent = {
LaunchedEffect(
key1 = permissionState.status.isGranted,
block = {
permissionState.launchPermissionRequest()
},
)
Box(
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center
) {
Text(
text = "App is need for permission. Allowed permission in setting , By Clicked here",
modifier = Modifier
.padding(16.dp)
.clickable {
context.navigateToAppSettings()
},
textAlign = TextAlign.Center,
)
}
},
) {
Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
var isShowGallery by remember { mutableStateOf(false) }
var imageUri by remember { mutableStateOf<Uri?>(null) }
if (imageUri != null) {
Image(
modifier = Modifier.fillMaxSize(),
painter = rememberAsyncImagePainter(model = imageUri),
contentDescription = "Captured image"
)
Button(
modifier = Modifier.align(Alignment.BottomCenter),
onClick = {
imageUri = null
}
) {
Text("clear image")
}
} else {
if (isShowGallery) {
OpenGallery(
onImageUri = {
imageUri = it
MLKitHelper.analyseFor(imageUri, context, callback = {
Toast.makeText(context, "Barcode found ${it}", Toast.LENGTH_LONG)
.show()
})
isShowGallery = false
},
)
} else {
AndroidView(
{ context ->
CameraSetup.init(context)
},
modifier = Modifier
.fillMaxSize()
.drawWithContent {
val overlayWidth = size.width
val overlayHeight = size.height
val boxWidth = overlayWidth * 0.65
val boxHeight = overlayHeight * 0.35
val cx = overlayWidth / 2
val cy = overlayHeight / 2
val rectF = RectF(
(cx - boxWidth / 2).toFloat(),
(cy - boxHeight / 2).toFloat(),
(cx + boxWidth / 2).toFloat(),
(cy + boxHeight / 2).toFloat()
)
val scrimPaint: Paint = Paint().apply {
color = Color(0xFF99000000)
}
val boxPaint: Paint = Paint().apply {
color = Color.White
style = PaintingStyle.Stroke
strokeWidth = 4f
}
val eraserPaint: Paint = Paint().apply {
strokeWidth = boxPaint.strokeWidth
blendMode = BlendMode.Clear
}

drawIntoCanvas { canvas ->
drawContent()
canvas.drawPath(
Path(),
Paint().apply {
strokeWidth = 4f
color = Color.White
},
)
canvas.drawRect(
left = 0f,
top = 0f,
right = overlayWidth,
bottom = overlayHeight,
paint = scrimPaint
)
eraserPaint.style = PaintingStyle.Fill
canvas.drawRoundRect(
left = rectF.left,
top = rectF.top,
right = rectF.right,
bottom = rectF.bottom,
8f,
8f,
eraserPaint
)
eraserPaint.style = PaintingStyle.Stroke
canvas.drawRoundRect(
left = rectF.left,
top = rectF.top,
right = rectF.right,
bottom = rectF.bottom,
8f,
8f,
eraserPaint
)
canvas.drawRoundRect(
left = rectF.left,
top = rectF.top,
right = rectF.right,
bottom = rectF.bottom,
8f,
8f,
eraserPaint
)
// full frame view with stroke view
canvas.drawRoundRect(
left = rectF.left,
top = rectF.top,
right = rectF.right,
bottom = rectF.bottom,
8f,
8f,
boxPaint
)
}
}
)
Text(
"Upload QR",
modifier = Modifier
.drawWithCache {
onDrawBehind {
drawRoundRect(
Brush.linearGradient(
listOf(
Color.White,
Color.Cyan
)
),
cornerRadius = CornerRadius(16.dp.toPx())
)
}
}
.padding(horizontal = 16.dp, vertical = 8.dp)
.align(Alignment.BottomCenter)
.clickable {
isShowGallery = true
},
)
}
}
}
}
}

With all of that code above , here is the result of the run

Is that all you want?

Now i take it to another level for you guys.

or this what you want?

Just to let’s you know that i am using path for drawing each corner!

Here the code

val mWidth = cx - boxWidth / 2
val mHeight = cy - boxHeight / 2
val lineHeight = 100f

val path = Path()
path.moveTo((overlayWidth - mWidth).toFloat(), (mHeight).toFloat())
path.lineTo(
(overlayWidth - mWidth).toFloat(),
(mHeight + lineHeight).toFloat()
)

path.moveTo((overlayWidth - mWidth).toFloat(), (mHeight).toFloat())
path.lineTo(
(overlayWidth - mWidth).toFloat() - lineHeight,
(mHeight).toFloat()
)

path.moveTo(
(overlayWidth - mWidth).toFloat(),
(overlayHeight - mHeight).toFloat()
)
path.lineTo(
(overlayWidth - mWidth).toFloat() - lineHeight,
(overlayHeight - mHeight).toFloat()
)

path.moveTo(
(overlayWidth - mWidth).toFloat(),
(overlayHeight - mHeight).toFloat()
)
path.lineTo(
(overlayWidth - mWidth).toFloat(),
((overlayHeight - mHeight) - lineHeight).toFloat()
)

path.moveTo((mWidth).toFloat(), (mHeight).toFloat())
path.lineTo(
(mWidth).toFloat(),
(mHeight + lineHeight).toFloat()
)

path.moveTo((mWidth).toFloat(), (mHeight).toFloat())
path.lineTo(
(mWidth).toFloat() + lineHeight,
(mHeight).toFloat()
)

path.moveTo((mWidth).toFloat(), (overlayHeight - mHeight).toFloat())
path.lineTo(
(mWidth).toFloat(),
((overlayHeight - mHeight) - lineHeight).toFloat()
)

path.moveTo((mWidth).toFloat(), (overlayHeight - mHeight).toFloat())
path.lineTo(
(mWidth).toFloat() + lineHeight,
((overlayHeight - mHeight)).toFloat()
)
path.close()
canvas.drawPath(path, boxPaint)

For include with drawWithcontent view

.drawWithContent {
val overlayWidth = size.width
val overlayHeight = size.height
val boxWidth = overlayWidth * 0.65
val boxHeight = overlayHeight * 0.35
val cx = overlayWidth / 2
val cy = overlayHeight / 2
val rectF = RectF(
(cx - boxWidth / 2).toFloat(),
(cy - boxHeight / 2).toFloat(),
(cx + boxWidth / 2).toFloat(),
(cy + boxHeight / 2).toFloat()
)
val scrimPaint: Paint = Paint().apply {
color = Color(0xFF99000000)
}
val boxPaint: Paint = Paint().apply {
color = Color.White
style = PaintingStyle.Stroke
strokeWidth = 4f
}
val eraserPaint: Paint = Paint().apply {
strokeWidth = boxPaint.strokeWidth
blendMode = BlendMode.Clear
}

drawIntoCanvas { canvas ->
drawContent()
canvas.drawPath(
Path(),
Paint().apply {
strokeWidth = 4f
color = Color.White
},
)
canvas.drawRect(
left = 0f,
top = 0f,
right = overlayWidth,
bottom = overlayHeight,
paint = scrimPaint
)
eraserPaint.style = PaintingStyle.Fill
canvas.drawRoundRect(
left = rectF.left,
top = rectF.top,
right = rectF.right,
bottom = rectF.bottom,
8f,
8f,
eraserPaint
)
eraserPaint.style = PaintingStyle.Stroke
canvas.drawRoundRect(
left = rectF.left,
top = rectF.top,
right = rectF.right,
bottom = rectF.bottom,
8f,
8f,
eraserPaint
)
canvas.drawRoundRect(
left = rectF.left,
top = rectF.top,
right = rectF.right,
bottom = rectF.bottom,
8f,
8f,
eraserPaint
)
// draw border

val mWidth = cx - boxWidth / 2
val mHeight = cy - boxHeight / 2
val lineHeight = 100f

val path = Path()
path.moveTo((overlayWidth - mWidth).toFloat(), (mHeight).toFloat())
path.lineTo(
(overlayWidth - mWidth).toFloat(),
(mHeight + lineHeight).toFloat()
)

path.moveTo((overlayWidth - mWidth).toFloat(), (mHeight).toFloat())
path.lineTo(
(overlayWidth - mWidth).toFloat() - lineHeight,
(mHeight).toFloat()
)

path.moveTo(
(overlayWidth - mWidth).toFloat(),
(overlayHeight - mHeight).toFloat()
)
path.lineTo(
(overlayWidth - mWidth).toFloat() - lineHeight,
(overlayHeight - mHeight).toFloat()
)

path.moveTo(
(overlayWidth - mWidth).toFloat(),
(overlayHeight - mHeight).toFloat()
)
path.lineTo(
(overlayWidth - mWidth).toFloat(),
((overlayHeight - mHeight) - lineHeight).toFloat()
)

path.moveTo((mWidth).toFloat(), (mHeight).toFloat())
path.lineTo(
(mWidth).toFloat(),
(mHeight + lineHeight).toFloat()
)

path.moveTo((mWidth).toFloat(), (mHeight).toFloat())
path.lineTo(
(mWidth).toFloat() + lineHeight,
(mHeight).toFloat()
)

path.moveTo((mWidth).toFloat(), (overlayHeight - mHeight).toFloat())
path.lineTo(
(mWidth).toFloat(),
((overlayHeight - mHeight) - lineHeight).toFloat()
)

path.moveTo((mWidth).toFloat(), (overlayHeight - mHeight).toFloat())
path.lineTo(
(mWidth).toFloat() + lineHeight,
((overlayHeight - mHeight)).toFloat()
)
path.close()
canvas.drawPath(path, boxPaint)

}
}

That’s it . I hope that you need with full customize if you want to please around or remove the board that on you.

Here the git repos for this project

Here the my git and my site

Thank you for your time , How about a clap?

--

--

Eang Tithsophorn
Eang Tithsophorn

Written by Eang Tithsophorn

I am Mobile Development Learner. My mobile list box has Android , iOS and Flutter. More information http://128.199.87.161/

No responses yet