QR Scanner with ML Kit , Jetpack Compose
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!
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
Now i take it to another level for you guys.
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
Thank you for your time , How about a clap?