ทำ feature scan QR code บน Android app อย่างง่าย ด้วย ML Kit
ถ้าแอพนึงอยากมี feature scan QR Code เมื่อก่อนเราต้องหาจาก 3rd-party library อื่นใด
แต่ตอนนี้มี ML Kit ใน Vision API ให้เราเลือกใช้แบบฟรี ๆ แถมวิธีใช้ก็ง่ายกว่าที่คิดอีกด้วย แล้วทำยังไง? ไปดูกัน!

ทำความรู้จัก ML Kit กันก่อน
ML Kit เป็นชุด Mobile SDK จาก Google ที่ช่วยให้เรานำ Machine Learning มาใช้ในแอป Android และ iOS ได้ง่ายๆ โดยใช้ Vision API และ Natural Language API บนแอพของเราได้เลย (ซึ่งเมื่อก่อนเขาอยู่บน Firebase แหละ ถ้าใครคุ้น ๆ)
และทั้งหมดใน ML Kit API แอพ Android ของเราต้องมี Android API Level ตั้งแต่ 21 ขึ้นไป
ตัวที่เป็น Vision API มี feature ต่าง ๆ ที่พ้น beta แล้ว (เพราะมันมีเยอะมาก) ดังนี้
- Text Recognition v2: ถอด text ออกจากรูป ใช้ทำ OCR
- Face Detection: แยกใบหน้าคนได้
- Barcode Scanning: ใช้สำหรับ Scan QR Code และ Barcode ต่าง ๆ ในที่นี้เราจะใช้อันนี้กัน
- Image Labeling: บอกว่าในภาพนี้มีอะไรบ้าง
- Object Detection and Tracking: แยกวัตถุได้
- Digital Ink Recognition: แกะลายมือได้ว่าเขียนอะไรมา ใช้เทคโนโลยีเดียวกันกับ Gboard, Google Translate, และเกมส์ Quick, Draw!
- Custom models with ML Kit: สำหรับคนที่อยากทำ model เองในแอพของเรา
และในส่วนของ Barcode Scanning นั้น เป็นแบบ on-device ไม่ต้องใช้การเชื่อมต่ออินเทอร์เน็ต รองรับ format มาตรฐานเป็น 2 ประเภท
- Linear formats: Codabar, Code 39, Code 93, Code 128, EAN-8, EAN-13, ITF, UPC-A, UPC-E
- 2D formats: Aztec, Data Matrix, PDF417, QR Code
และบน Android พิเศษ สามารถ implement ได้ 2 แบบด้วยกัน
Scan barcodes with ML Kit on Android
แบบมาตรฐานสามัญ แบ่งเป็น 2 องค์ประกอบใหญ่ ๆ คือ ตัว ML Kit ที่ทำหน้าที่อ่าน barcode กับตัวกล้อง ในที่นี้เราจะใช้ Camera X กัน
เซ็ทอัพเซ็ทใจกันก่อน
ก่อนอื่นมา setup อะไรกันก่อน เอาแบบง่าย ๆ คือ เพิ่ม dependency ที่ build.gradle
ของ module ที่เราทำการเพิ่มไปนั่นเอง มันจะมีให้เลือก 2 อัน คือ
dependencies {
// ...
// Use this dependency to bundle the model with your app
implementation 'com.google.mlkit:barcode-scanning:17.3.0'
// Use this dependency to use the dynamically downloaded model in Google Play Services
implementation 'com.google.android.gms:play-services-mlkit-barcode-scanning:18.3.1'
}
- สำหรับการรวมตัว model ไปกับแอพ: เราใช้อันนี้
- ใช้ model จาก Google Play: อันนี้โหลดมาเป็น dynamic แล้วต้องเพิ่ม config ในการ download แบบอัตโนมัติที่
AndroidManifest.xml
ถ้าเลื่อนลงไปดู Google code scanner จะต่างอยู่นิดหน่อย
<application ...>
...
<meta-data
android:name="com.google.mlkit.vision.DEPENDENCIES"
android:value="barcode"/>
<!-- To use multiple models: android:value="barcode,model2,model3" -->
</application>
เริ่มเขียนโค้ดกัน
ของเราทำแบบง่าย ๆ คือกดปุ่มนี้ แล้วไปหน้า scanner โดยเราสร้าง activity ใหม่ที่ชื่อว่า BarcodeScannerActivity
และแน่นอน มี activity แล้วต้องมี layout ให้ชื่อว่า activity_barcode_scanner
ก็แล้วกัน
ข้างใน activity_barcode_scanner
ก็แสนจะเรียบง่าย มี parent สักตัว และข้างในเป็น PreviewView
เพื่อแสดงภาพจากกล้องให้เราดู
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".BarcodeScannerActivity">
<androidx.camera.view.PreviewView
android:id="@+id/preview_view"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
กลับมาที่ BarcodeScannerActivity
มาจัดการตัวแปรต่าง ๆ ที่ใช้กัน
binding
: เข้าถึง layout xmlactivity_barcode_scanner
ที่สร้างมาเมื่อกี้มาใช้ต่อcameraExecutor
: ใช้ในการประมวลผลของกล้องที่ background thread เราสร้าง execution ตัวนี้เพื่อเอามาวิเคราะห์หา QR CodebarcodeBoxView
: custom view ตัวหนึ่ง ที่เอามาวาดสี่เหลี่ยมตรง QR Code ของเรา แล้วเอามาเพิ่มโดยโค้ด ให้มันเต็มจอ
private lateinit var binding: ActivityBarcodeScannerBinding
private lateinit var cameraExecutor: ExecutorService
private lateinit var barcodeBoxView: BarcodeBoxView
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityBarcodeScannerBinding.inflate(layoutInflater)
setContentView(binding.root)
cameraExecutor = Executors.newSingleThreadExecutor()
barcodeBoxView = BarcodeBoxView(this)
addContentView(
barcodeBoxView,
ViewGroup.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.MATCH_PARENT
)
)
}
แล้ว BarcodeBoxView
มันคืออะไร และหน้าตาเป็นยังไง? มันคือ View ที่แสดงกรอบสี่เหลี่ยมเพื่อบอกว่าตัวแอพตรวจจับเจอ QR Code แล้วว่าอยู่ตรงนี้นะ
เราสร้าง RectF
เพื่อวาดกรอบสี่เหลี่ยมนี้ โดยใช้ Paint
บอกว่ากรอบนี้เป็นสีแดง มีความหนา 5px และขอบมน 10px
การใช้งาน เรียกใช้ setRect()
เพื่ออัพเดตตำแหน่ง QR Code ที่เจอ ว่า ได้ว่าอยู่ตรงไหน แล้วให้วาดสี่เหลี่ยมนี้ขึ้นมา ถ้าไม่เจอให้ลบของเก่าออกโดยการใส่ RectF
เปล่า ๆ ลงไป
class BarcodeBoxView(context: Context) : View(context) {
private val paint = Paint()
private var mRect = RectF()
override fun onDraw(canvas: Canvas?) {
super.onDraw(canvas)
val cornerRadius = 10f
paint.style = Paint.Style.STROKE
paint.color = Color.RED
paint.strokeWidth = 5f
canvas?.drawRoundRect(mRect, cornerRadius, cornerRadius, paint)
}
fun setRect(rect: RectF) {
mRect = rect
invalidate()
requestLayout()
}
}
เพิ่ม permission ของกล้องกันก่อนเลย เมื่อ install app มาใหม่ จะเจอหน้าให้ allow camera permission กันก่อน แต่ถ้าเคย aollow permission แล้ว ก็ไปเปิดกล้อง ถ้าไม่จะมี popup dialog บอกว่าเราต้องการ permission ในการใช้กล้องเพื่อ process barcode น้า
override fun onCreate(savedInstanceState: Bundle?) {
// ...
checkCameraPermission()
}
/**
* This function is executed once the user has granted or denied the missing permission
*/
override fun onRequestPermissionsResult(
requestCode: Int,
permissions: Array<out String>,
grantResults: IntArray
) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
checkIfCameraPermissionIsGranted()
}
/**
* This function is responsible to request the required CAMERA permission
*/
private fun checkCameraPermission() {
try {
val requiredPermissions = arrayOf(Manifest.permission.CAMERA)
ActivityCompat.requestPermissions(this, requiredPermissions, 0)
} catch (e: IllegalArgumentException) {
checkIfCameraPermissionIsGranted()
}
}
/**
* This function will check if the CAMERA permission has been granted.
* If so, it will call the function responsible to initialize the camera preview.
* Otherwise, it will raise an alert.
*/
private fun checkIfCameraPermissionIsGranted() {
if (ContextCompat.checkSelfPermission(this, Manifest.permission.CAMERA) == PackageManager.PERMISSION_GRANTED) {
// Permission granted: start the preview
startCamera()
} else {
// Permission denied
MaterialAlertDialogBuilder(this)
.setTitle("Permission required")
.setMessage("This application needs to access the camera to process barcodes")
.setPositiveButton("Ok") { _, _ ->
// Keep asking for permission until granted
checkCameraPermission()
}
.setCancelable(false)
.create()
.apply {
setCanceledOnTouchOutside(false)
show()
}
}
}

เมื่อเรา allow camera กันเรียบร้อยแล้ว มาต่อกันที่ startCamera()
ตรงนี้จะเปิด Camera X และเรียกใช้ ML Kit กันล่ะ
เริ่มจากสร้าง ProcessCameraProvider
ที่มีชื่อว่า cameraProviderFuture
จากนั้นมาเพิ่ม listener กัน โดยดึงตัว provider ที่ชื่อว่า cameraProvider
ขึ้นมา
เอ้อออ ในที่นี้เราจะ execute บน main thread เนอะ เพราะเราใช้กล้อง
private fun startCamera() {
val cameraProviderFuture = ProcessCameraProvider.getInstance(this)
cameraProviderFuture.addListener({
val cameraProvider = cameraProviderFuture.get()
}, ContextCompat.getMainExecutor(this))
}
สร้าง Preview ขึ้นมา ให้ตัว surface provider เป็นตัว PreviewView
ที่เราสร้างไว้ใน layout xml ตอนแรกเลย
private fun startCamera() {
val cameraProviderFuture = ProcessCameraProvider.getInstance(this)
cameraProviderFuture.addListener({
val cameraProvider = cameraProviderFuture.get()
// Preview
val preview = Preview.Builder()
.build()
.also {
it.setSurfaceProvider(binding.previewView.surfaceProvider)
}
}, ContextCompat.getMainExecutor(this))
}
มาถึงส่วนสำคัญ ส่วน image analyzer สร้างตัว ImageAnalysis ขึ้นมา เนื่องจากเราใช้ Camera X เขาให้ใส่ ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST
ที่ setBackpressureStrategy()
เพื่อการันตีว่าจะมีเพียงรูปเดียวที่จะส่งไป analyze ในแต่ละครั้ง และ setAnalyzer()
ด้วย cameraExecutor
ที่สร้างไว้เมื่อกี้ กับตัว Analyzer ที่เราสร้างใหม่ ชื่อว่า QrCodeAnalyzer
private fun startCamera() {
val cameraProviderFuture = ProcessCameraProvider.getInstance(this)
cameraProviderFuture.addListener({
val cameraProvider = cameraProviderFuture.get()
// Preview
val preview = Preview.Builder()
.build()
.also {
it.setSurfaceProvider(binding.previewView.surfaceProvider)
}
}, ContextCompat.getMainExecutor(this))
}
แล้ว QrCodeAnalyzer
ข้างในมีอะไรบ้าง? เป็นส่วนในการทำ image analysis ด้วย ML Kit นี่แหละ สืบทอดมาจาก ImageAnalysis.Analyzer
มี parameter ที่ต้องโยนเข้ามา 4 อันด้วยกัน คือ
context
: ใช้ show toast ว่าได้ value ของ QR Code ออกมาเป็นอะไร ในการ implement จริงอาจจะไปเปิดหน้าเว็บ หรืออื่นใดแล้วแต่เลย ถ้าเปิดเว็บอาจจะใช้เป็น parameter ชนิดอื่นแทนเนอะbarcodeBoxView
: View กรอบสี่เหลี่ยม เอามาวาดในนี้previewViewWidth
: ความกว้างของPreviewView
previewViewHeight
: ความสูงของPreviewView
ก่อนอื่น สร้างตัวแปร เพื่อใช้ในการ scale factor กับตัว PreviewView และภาพจากกล้อง ทั้งแกน x และ y
private var scaleX = 1f
private var scaleY = 1f
หลัก ๆ เรา override ตัว analyze()
เพื่อทำการรับภาพมาวิเคราะห์ทีละภาพ
ก่อนอื่นดูว่ากล้องมีภาพไหม ถ้ามีภาพ ไปคำนวณ scale factors ต่อ และแปลงภาพที่ได้เอาไปใช้ต่อใน ML Kit
override fun analyze(image: ImageProxy) {
val img = image.image
if (img != null) {
// Update scale factors
scaleX = previewViewWidth / img.height.toFloat()
scaleY = previewViewHeight / img.width.toFloat()
val inputImage = InputImage.fromMediaImage(img, image.imageInfo.rotationDegrees)
}
}
จุดสำคัญ เอา ML Kit ไปใช้ล่ะ โดยการ config barcode scanner โดยเรา detect QR Code อย่างเดียว ใส่เป็น Barcode.FORMAT_QR_CODE
หรือเอา barcode ทั้งหมดก็ใส่ Barcode.FORMAT_ALL_FORMATS
ใส่ enableAllPotentialBarcodes()
เป็น optional ในการ return barcode ทั้งหมดที่เป็นไปได้ แม้จะ decode ออกมาไม่ได้ก็ตาม
override fun analyze(image: ImageProxy) {
// ...
if (img != null) {
// ...
// Process image searching for barcodes
val options = BarcodeScannerOptions.Builder()
.setBarcodeFormats(
Barcode.FORMAT_QR_CODE
)
.enableAllPotentialBarcodes() // Optional
.build()
}
}
สร้างตัวแปร scanner เพื่อเอา instance ของ BarcodeScanner แล้วใส่ option เมื่อกี้ลงไป ถ้าไม่มีก็ไม่ต้องใส่
override fun analyze(image: ImageProxy) {
// ...
if (img != null) {
// ...
// Get an instance of BarcodeScanner
val scanner = BarcodeScanning.getClient(options)
// or not option
//val scanner = BarcodeScanning.getClient()
}
}
จากนั้นประมวลผลภาพ ผ่าน listener 2 อัน
addOnSuccessListener
: ได้ค่า barcode แล้วเอาไปทำอะไรต่อ ในที่นี้ success แล้ว barcode ไม่ empty จะ show toast ว่าได้ value อะไรออกมา และวาดสี่เหลี่ยมขึ้นมาตรง QR Code นั้นที่เจอ และถ้าได้ empty ก็ลบทิ้งaddOnFailureListener
: ถ้าเกิด failure จะทำยังไงต่อ
override fun analyze(image: ImageProxy) {
// Process the image
scanner.process(inputImage)
.addOnSuccessListener { barcodes ->
if (barcodes.isNotEmpty()) {
for (barcode in barcodes) {
// Handle received barcodes...
Toast.makeText(
context,
"Value: " + barcode.rawValue,
Toast.LENGTH_SHORT
).show()
// Update bounding rect
barcode.boundingBox?.let { rect ->
barcodeBoxView.setRect(
adjustBoundingRect(
rect
)
)
}
}
} else {
// Remove bounding rect
barcodeBoxView.setRect(RectF())
}
}
.addOnFailureListener { }
}
เพิ่มเติมในส่วน barcode information มันจะคืนออกมาเป็น object ที่ชื่อว่า Barcode
ข้างในมีอะไรที่เราเอาไปใช้ต่อได้บ้าง
barcode.rawValue
: ค่าที่อ่านได้จาก barcode นั้น ๆbarcode.valueType
: type ของ barcode นั้น ๆ มีหลายอันเลย ที่คิดว่าใช้หลัก ๆ จะมีTYPE_URL
,TYPE_TEXT
,TYPE_WIFI
โดยแต่ละ type จะมี object แตกต่างกันออกไป
เช่น TYPE_URL
เป็น object UrlBookmark
การใช้งานดึง barcode.url?.title
และ barcode.url?.url
ออกมาได้ เผื่อใช้ในการเปิดหน้าเว็บงี้
ส่วน TYPE_WIFI
เป็น object WiFi
สามารถดึง barcode.wifi?.ssid
, barcode.wifi?.password
และ barcode.wifi?.encryptionType
barcode.cornerPoints
: มุมของตัว barcode นั้นbarcode.boundingBox
: กรอบที่เจอ barcode นั้น ๆ
ในส่วนการคำนวณกรอบสี่เหลี่ยมที่แสดงล้อมรอบ QR Code มี adjustBoundingRect()
โดยตัว rect
เอามาจาก barcode.boundingBox
แล้วมาวาดตามตำแหน่งที่เจอ
private fun translateX(x: Float) = x * scaleX
private fun translateY(y: Float) = y * scaleY
private fun adjustBoundingRect(rect: Rect) = RectF(
translateX(rect.left.toFloat()),
translateY(rect.top.toFloat()),
translateX(rect.right.toFloat()),
translateY(rect.bottom.toFloat())
)
ปิดจบโดยการปิดตัว image เพื่อให้ตัว Camera X ส่งภาพใหม่ในการประมวลผล
override fun analyze(image: ImageProxy) {
// ...
if (img != null) {
// ...
}
image.close()
}
กลับมาที่ BarcodeScannerActivity
เลือก default เป็นกล้องหลัง
private fun startCamera() {
val cameraProviderFuture = ProcessCameraProvider.getInstance(this)
cameraProviderFuture.addListener({
// ...
// Select back camera as a default
val cameraSelector = CameraSelector.DEFAULT_BACK_CAMERA
}, ContextCompat.getMainExecutor(this))
}
และเอา cameraProvider
ไป unbind ของเดิมออก แล้ว bind ใหม่กับ lifecycle อย่าลืมครอบ try-catch และจัดการ exception ด้วยล่ะ
private fun startCamera() {
val cameraProviderFuture = ProcessCameraProvider.getInstance(this)
cameraProviderFuture.addListener({
// ...
try {
// Unbind use cases before rebinding
cameraProvider.unbindAll()
// Bind use cases to camera
cameraProvider.bindToLifecycle(
this, cameraSelector, preview, imageAnalyzer
)
} catch (exc: Exception) {
exc.printStackTrace()
}
}, ContextCompat.getMainExecutor(this))
}
เมื่อ user ใช้หน้านี้เสร็จแล้ว ปิดหน้านี้ทิ้ง เราจะปิด executor เพื่อป้องกัน memory leak เมื่อเราไม่ใช้งานหน้านี้แล้ว
override fun onDestroy() {
super.onDestroy()
cameraExecutor.shutdown()
}
Reference



จริง ๆ มีของอีกคนนึง แต่เขาลบบทความบน Medium ออกแล้ว แง
Google code scanner (Android only)
อันนี้ไม่ต้องใช้ permission camera ใช้งานได้ง่ายกว่าแบบปกติด้วย โดยใช้ ML Kit Barcode Scanning API เหมือนกัน และ return Barcode
ออกมาเหมือนกัน
และอันนี้มีใช้เฉพาะ Android เท่านั้น แล้วมี auto-zoom ให้ด้วยนะ
การใช้งาน implement ได้ง่าย กดปุ่มแล้วจะพาไปหน้า activity ที่ scan code เมื่อ success แล้ว เอาค่าที่ได้ไปใช้งานต่อได้ทันที ง่ายเกินจนงง (แต่ความจริงไม่น่ามีแอพไหนใช้อันนี้มั้ง555)
ก่อนอื่นไปไปที่ settings.gradle
เพื่อใส่ Maven repository ของ Google และ maven central ก่อนดังนี้
dependencyResolutionManagement {
repositories {
google()
mavenCentral()
}
}
จากนั้นเพิ่มของใน dependency ที่ build.gradle
อีกเช่นเคย
dependencies {
// ...
// Google code scanner
implementation("com.google.android.gms:play-services-code-scanner:16.1.0")
}
แล้วก็ไป config ที่ AndroidManifest.xml
ว่าแอพเราใช้ Google Play service นะ เพื่อให้มัน download scanner module เข้ามาลงเครื่องเราอัตโนมัติ เมื่อแอพเราติดตั้งจาก Play Store
<application ...>
...
<meta-data
android:name="com.google.mlkit.vision.DEPENDENCIES"
android:value="barcode_ui"/>
</application>
ต่อมาเรามาเข้าโค้ดกัน ก่อนอื่นเลย set option ของตัว scanner ก่อน ว่าเราจะสแกนกับ QR Code นะ ช่วย auto zoom ให้หนูด้วย
val options = GmsBarcodeScannerOptions.Builder()
.setBarcodeFormats(
Barcode.FORMAT_QR_CODE
)
.enableAutoZoom()
.build()
จากนั้นสร้าง instance ของ GmsBarcodeScanner
val scanner = GmsBarcodeScanning.getClient(this, options)
จบด้วยเริ่ม scan แล้วตามด้วย listener ทั้ง 3 อันว่า
addOnSuccessListener
: ถ้า success จะทำอะไรต่อ โดยค่าจาก QR Code ที่เราได้ จะเป็นbarcode.rawValue
addOnCanceledListener
: ถ้า user cancel จะทำอะไรaddOnFailureListener
: ถ้า failure จาก exception ใด ๆ จะ handle ยังไงต่อ อันนี้ใส่ตามใจเราเลย
scanner.startScan()
.addOnSuccessListener { barcode ->
// Task completed successfully
val rawValue: String? = barcode.rawValue
Log.d("addOnSuccessListener", rawValue.toString())
Toast.makeText(this, rawValue, Toast.LENGTH_SHORT).show()
}
.addOnCanceledListener {
// Task canceled
Log.d("addOnCanceledListener", "")
}
.addOnFailureListener { e ->
// Task failed with an exception
Log.d("addOnFailureListener", e.message.toString())
}
รวมร่าง เช่น เรากดปุ่ม เพื่อไปเปิด Google code scanner
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
binding.buttonGoogleScanner.setOnClickListener {
openGoogleCodeScanner()
}
}
private fun openGoogleCodeScanner() {
val options = GmsBarcodeScannerOptions.Builder()
.setBarcodeFormats(
Barcode.FORMAT_QR_CODE
)
.enableAutoZoom()
.build()
val scanner = GmsBarcodeScanning.getClient(this, options)
scanner.startScan()
.addOnSuccessListener { barcode ->
// Task completed successfully
val rawValue: String? = barcode.rawValue
Log.d("addOnSuccessListener", rawValue.toString())
Toast.makeText(this, rawValue, Toast.LENGTH_SHORT).show()
}
.addOnCanceledListener {
// Task canceled
Log.d("addOnCanceledListener", "")
}
.addOnFailureListener { e ->
// Task failed with an exception
Log.d("addOnFailureListener", e.message.toString())
}
}
ข้อควรระวัง: Google code scanner ใช้ได้เฉพาะเครื่องจริงเท่านั้น ในเครื่อง Android Emulator เปิดไม่ได้จ้า
Reference

one more thing
เนื่องจาก code เยอะมาก เลยรวมทั้งหมดใน Github จะได้ดูตามได้ง่ายขึ้นน้า
หวังว่าอ่านแล้วจะไม่งงกันนะ
ติดตามข่าวสารตามช่องทางต่าง ๆ และทุกช่องทางโดเนทกันไว้ที่นี่เลย แนะนำให้ใช้ tipme เน้อ ผ่าน promptpay ได้เต็มไม่หักจ้า
ติดตามข่าวสารแบบไว ๆ มาที่ Twitter เลย บางอย่างไม่มีในบล็อก และหน้าเพจนะ
สวัสดีจ้า ฝากเนื้อฝากตัวกับชาวทวิตเตอร์ด้วยน้าา
— Minseo | Stocker DAO (@mikkipastel) August 24, 2020