[Android] ๊ฐ„๋‹จํ•œ QR ์ฝ”๋“œ ๋ฆฌ๋”๊ธฐ ๋งŒ๋“ค๊ธฐ : JetPack, CameraX, Google ML kit
๋ฐ˜์‘ํ˜•

 

 

 

 

์•ˆ๋“œ๋กœ์ด๋“œ JetPack ์ค‘ CameraX ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ์™€, ๊ตฌ๊ธ€ ML ํ‚คํŠธ๋ฅผ ์‚ฌ์šฉํ•ด๋ณด๊ธฐ ์œ„ํ•ด QR์ฝ”๋“œ ๋ฆฌ๋”๊ธฐ๋ฅผ ๋งŒ๋“ค์–ด ๋ณด์•˜๋‹ค.
์—ญ์‹œ Joyce๋‹˜์˜ ์„œ์ ์„ ์ฐธ๊ณ ํ•˜์˜€๋‹ค.

 


 

๐Ÿ’ก Android Jetpack

 

Android Jetpack ๊ฐœ๋ฐœ์ž ๋ฆฌ์†Œ์Šค - Android ๊ฐœ๋ฐœ์ž  |  Android Developers

Follow best practices, eliminate boilerplate code, and reduce fragmentation

developer.android.com

  • ๊ตฌ๊ธ€ ์•ˆ๋“œ๋กœ์ด๋“œํŒ€์—์„œ ๊ณต์‹ ๋ฐœํ‘œํ•œ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ ๋ชจ์Œ
  • ์•ˆ์ •์„ฑ: ๋ชจ๋“  ์•ˆ๋“œ๋กœ์ด๋“œ ๋ฒ„์ „ ๋ฐ ๋‹ค์–‘ํ•œ ๊ธฐ๊ธฐ์—์„œ ์ผ๊ด€๋˜๊ฒŒ ์ž‘๋™ํ•˜๋Š” ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ์ด๋ฏ€๋กœ, ๋ณต์žก์„ฑ ๋‚ฎ์ถ”๊ณ  ์•ˆ์ •์„ฑ ๋†’์ž„
  • ๊ฐ„ํŽธ์„ฑ: ๋ฐฑ๊ทธ๋ผ์šด๋“œ ์ž‘์—…, ์ƒ๋ช… ์ฃผ๊ธฐ ๊ด€๋ฆฌ ๋“ฑ์„ ๋Œ€์‹  ํ•ด์ฃผ์–ด ๋ณด์ผ๋Ÿฌํ”Œ๋ ˆ์ดํŠธ ์ฝ”๋“œ๋ฅผ ์ค„์ด๊ณ  ๋กœ์ง์—๋งŒ ์ง‘์ค‘ ๊ฐ€๋Šฅ
  • ์ ฏํŒฉ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋Š” ๋ชจ๋‘ [androidx.์ด๋ฆ„]๊ณผ ๊ฐ™์€ ํŒจํ‚ค์ง€๋ช…์œผ๋กœ ๊ตฌ์„ฑ
  • ์˜ˆ: AppSearch, CameraX, Compose, Data Binding, LiveData, WorkManager, Navigation, Test, ViewBinding ...

 

 


 

๐Ÿ’ก CameraX

 

CameraX ์‹œ์ž‘ํ•˜๊ธฐ  |  Android Developers

์ด Codelab์—์„œ๋Š” CameraX๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ๋ทฐํŒŒ์ธ๋”๋ฅผ ํ‘œ์‹œํ•˜๊ณ , ์‚ฌ์ง„์„ ์ฐ๊ณ , ์นด๋ฉ”๋ผ์—์„œ ์ด๋ฏธ์ง€ ์ŠคํŠธ๋ฆผ์„ ๋ถ„์„ํ•˜๋Š” ์นด๋ฉ”๋ผ ์•ฑ์„ ๋งŒ๋“œ๋Š” ๋ฐฉ๋ฒ•์„ ์†Œ๊ฐœํ•ฉ๋‹ˆ๋‹ค.

developer.android.com

 

 

CameraX ์•„ํ‚คํ…์ฒ˜  |  Android media  |  Android Developers

์ด ํŽ˜์ด์ง€๋Š” Cloud Translation API๋ฅผ ํ†ตํ•ด ๋ฒˆ์—ญ๋˜์—ˆ์Šต๋‹ˆ๋‹ค. CameraX ์•„ํ‚คํ…์ฒ˜ ์ปฌ๋ ‰์…˜์„ ์‚ฌ์šฉํ•ด ์ •๋ฆฌํ•˜๊ธฐ ๋‚ด ํ™˜๊ฒฝ์„ค์ •์„ ๊ธฐ์ค€์œผ๋กœ ์ฝ˜ํ…์ธ ๋ฅผ ์ €์žฅํ•˜๊ณ  ๋ถ„๋ฅ˜ํ•˜์„ธ์š”. ์ด ํŽ˜์ด์ง€์—์„œ๋Š” CameraX์˜ ๊ตฌ์กฐ, API์˜

developer.android.com

  • ๋ฏธ๋ฆฌ๋ณด๊ธฐ (์‚ฌ์ง„ ์ฐ๊ธฐ ์ „ ๋ฏธ๋ฆฌ๋ณด๊ธฐ ํ™”๋ฉด)
  • ์ด๋ฏธ์ง€ ๋ถ„์„ (๋จธ์‹ ๋Ÿฌ๋‹๊ณผ ๊ฐ™์€ ์ด๋ฏธ์ง€ ๋ถ„์„)
  • ์ด๋ฏธ์ง€ ์บก์ฒ˜ (์ด๋ฏธ์ง€ ์ €์žฅ)

 

 

 


 

๐Ÿ’ก ๊ตฌ๊ธ€ MLํ‚คํŠธ

 

ML Kit  |  Google for Developers

๋ชจ๋ฐ”์ผ ๊ฐœ๋ฐœ์ž๋ฅผ ์œ„ํ•œ Google์˜ ๊ธฐ๊ธฐ๋ณ„ ๋จธ์‹ ๋Ÿฌ๋‹ ํ‚คํŠธ์ž…๋‹ˆ๋‹ค.

developers.google.com

  • ๊ตฌ๊ธ€ ๋จธ์‹ ๋Ÿฌ๋‹ ๊ธฐ์ˆ ์„ ์•ˆ๋“œ๋กœ์ด๋“œ, iOS์™€ ๊ฐ™์€ ๋ชจ๋ฐ”์ผ ๊ธฐ๊ธฐ์—์„œ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๊ฒŒ ํ•ด์ฃผ๋Š” ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ
  • ๋ฐ”์ฝ”๋“œ ์Šค์บ๋‹, ์–ผ๊ตด ์ธ์‹, ํ…์ŠคํŠธ ์ธ์‹, ํฌ์ฆˆ ์ธ์‹, ์–ธ์–ด ๊ฐ์ง€ ๋“ฑ ์ˆ˜๋งŽ์€ API๋ฅผ ์ œ๊ณตํ•จ

 

 

 


 

 

๐Ÿ’ก build.gradle ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ ์„ค์ •

dependencies {
..

    // CameraX core library using the camera2 implementation
    val camerax_version = "1.4.0-alpha05"
    // The following line is optional, as the core library is included indirectly by camera-camera2
    implementation("androidx.camera:camera-core:${camerax_version}")
    implementation("androidx.camera:camera-camera2:${camerax_version}")
    // If you want to additionally use the CameraX Lifecycle library
    implementation("androidx.camera:camera-lifecycle:${camerax_version}")
    // If you want to additionally use the CameraX View class
    implementation("androidx.camera:camera-view:${camerax_version}")
    // If you want to additionally add CameraX ML Kit Vision Integration
    implementation("androidx.camera:camera-mlkit-vision:${camerax_version}")
    // ML Kit ๋ฐ”์ฝ”๋“œ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ
    implementation("com.google.mlkit:barcode-scanning:17.2.0")
}

 

 

๐Ÿ’ก ๋งค๋‹ˆํŽ˜์ŠคํŠธ ๊ถŒํ•œ ์ถ”๊ฐ€

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

 

 

๐Ÿ’ก ๋ฏธ๋ฆฌ๋ณด๊ธฐ ๊ธฐ๋Šฅ ๊ตฌํ˜„ : MainAcitivty

  • ๋ฏธ๋ฆฌ๋ณด๊ธฐ๋ฅผ ๊ตฌํ˜„
  • ์นด๋ฉ”๋ผ ๊ถŒํ•œ ์Šน์ธํ•˜๊ธฐ

 

 

๐Ÿ’กQR ์ฝ”๋“œ ์ธ์‹ ๊ธฐ๋Šฅ ๊ตฌํ˜„

  • CameraX์˜ Analyzer ํด๋ž˜์Šค ๋งŒ๋“ค๊ธฐ (QRCodeAnalyzer.kt)
  • onDetect() ๋ฉ”์„œ๋“œ๊ฐ€ ์žˆ๋Š” ์ธํ„ฐํŽ˜์ด์Šค ๋งŒ๋“ค๊ธฐ
  • MainActivity.kt์—์„œ Analyzer์™€ ์นด๋ฉ”๋ผ ์—ฐ๋™ํ•˜๊ธฐ

 

 


 

๐Ÿ”ฅ ์™„์„ฑ ์ฝ”๋“œ

 

 

MainActivity.kt

package com.limheejin.qrcodereader

import android.content.Context
import android.Manifest
import android.content.Intent
import android.content.pm.PackageManager
import android.os.Bundle
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
import androidx.camera.core.CameraSelector
import androidx.camera.core.ImageAnalysis
import androidx.camera.core.Preview
import androidx.camera.lifecycle.ProcessCameraProvider
import androidx.core.content.ContextCompat
import com.google.common.util.concurrent.ListenableFuture
import com.limheejin.qrcodereader.databinding.ActivityMainBinding
import java.util.concurrent.ExecutorService
import java.util.concurrent.Executors

class MainActivity : AppCompatActivity() {

    private lateinit var binding: ActivityMainBinding
    private lateinit var camerProviderFuture: ListenableFuture<ProcessCameraProvider> // ํƒœ์Šคํฌ๊ฐ€ ์ œ๋Œ€๋กœ ๋๋‚ฌ์„ ๋•Œ ๋™์ž‘ ์ง€์ •
    private val PERMISSIONS_REQUEST_CODE = 1 // ํƒœ๊ทธ ๊ธฐ๋Šฅ์„ ํ•˜๋Š” ์ฝ”๋“œ๋กœ, 0๋ณด๋‹ค ํฐ ์–‘์ˆ˜๋ฉด ๋ญ๋“  ์ƒ๊ด€ ์—†์Œ
    private val PERMISSONS_REQUIRED = arrayOf(Manifest.permission.CAMERA) // ์นด๋ฉ”๋ผ ๊ถŒํ•œ ์ง€์ •
    private var isDetected = false // ์ด๋ฏธ์ง€ ๋ถ„์„์ด ์‹ค์‹œ๊ฐ„์œผ๋กœ ์ง„ํ–‰๋˜๋ฏ€๋กœ, onDetect()๊ฐ€ ์—ฌ๋Ÿฌ๋ฒˆ ํ˜ธ์ถœ๋˜๋Š” ๊ฑธ ๋ง‰๊ธฐ ์œ„ํ•ด ๋ณ€์ˆ˜ ์ƒ์„ฑ

    // ๋‹ค์‹œ ์‚ฌ์šฉ์ž์˜ ํฌ์ปค์Šค๊ฐ€ MainActivity๋กœ ์˜จ๋‹ค๋ฉด ๋‹ค์‹œ QR์„ ์ธ์‹ํ•  ์ˆ˜ ์žˆ๋„๋ก
    override fun onResume() {
        super.onResume()
        isDetected = false
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = ActivityMainBinding.inflate(layoutInflater)
        setContentView(binding.root)

        if (!hasPermissions(this)) {
            requestPermissions(PERMISSONS_REQUIRED, PERMISSIONS_REQUEST_CODE) // ๊ถŒํ•œ ์š”์ฒญ
        } else {
            startCamera() // ์ด๋ฏธ ๊ถŒํ•œ์ด ์žˆ๋‹ค๋ฉด ์นด๋ฉ”๋ผ ์‹œ์ž‘
        }
    }

    // ๊ถŒํ•œ ์œ ๋ฌด ํ™•์ธ
    private fun hasPermissions(context: Context) =
        PERMISSONS_REQUIRED.all { // .all์€ ๋ฐฐ์—ด์˜ ์›์†Œ๊ฐ€ ๋ชจ๋‘ ์กฐ๊ฑด๋ฌธ์„ ๋งŒ์กฑํ•˜๋ฉด true, ์•„๋‹ˆ๋ฉด false
            ContextCompat.checkSelfPermission(context, it) == PackageManager.PERMISSION_GRANTED
        }

    // ๊ถŒํ•œ ์š”์ฒญ ์ฝœ๋ฐฑ ํ•จ์ˆ˜
    override fun onRequestPermissionsResult(
        requestCode: Int,
        permissions: Array<String>,
        grantResults: IntArray
    ) {
        super.onRequestPermissionsResult(requestCode, permissions, grantResults)

        // requestPermissions์˜ ์ธ์ˆ˜๋กœ ๋„ฃ์€ PERMISSION_REQUEST_CODE์™€ ๋งž๋Š”์ง€ ํ™•์ธ
        if (requestCode == PERMISSIONS_REQUEST_CODE) {
            if (PackageManager.PERMISSION_GRANTED == grantResults.firstOrNull()) {
                Toast.makeText(this@MainActivity, "๊ถŒํ•œ ์š”์ฒญ์ด ์Šน์ธ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.", Toast.LENGTH_LONG).show()
                startCamera()
            } else {
                Toast.makeText(this@MainActivity, "๊ถŒํ•œ ์š”์ฒญ์ด ๊ฑฐ๋ถ€๋˜์—ˆ์Šต๋‹ˆ๋‹ค.", Toast.LENGTH_LONG).show()
                finish()
            }
        }
    }

    // ๋ฏธ๋ฆฌ๋ณด๊ธฐ์™€ ์ด๋ฏธ์ง€ ๋ถ„์„ ์‹œ์ž‘
    fun startCamera() {
        camerProviderFuture = ProcessCameraProvider.getInstance(this) // ๊ฐ์ฒด์˜ ์ฐธ์กฐ๊ฐ’ ํ• ๋‹น
        camerProviderFuture.addListener(Runnable { // cameraProviderFuter ํƒœ์Šคํฌ๊ฐ€ ๋๋‚˜๋ฉด ์‹คํ–‰
            val cameraProvider =
                camerProviderFuture.get() // ์นด๋ฉ”๋ผ์˜ ์ƒ๋ช…์ฃผ๊ธฐ๋ฅผ ์•กํ‹ฐ๋น„ํ‹ฐ๋‚˜ ํ”„๋ž˜๊ทธ๋จผํŠธ์˜ ์ƒ๋ช…์ฃผ๊ธฐ์— ๋ฐ”์ธ๋“œํ•ด์ฃผ๋Š” ProcessCameraProvider ๊ฐ์ฒด ๊ฐ€์ ธ์˜ด
            val preview = getPreview() // ๋ฏธ๋ฆฌ๋ณด๊ธฐ ๊ฐ์ฒด ๊ฐ€์ ธ์˜ด
            val imageAnalysis = getImageAnalysis() // ImageAnalysis์˜ ๊ฐ์ฒด ์ƒ์„ฑ
            val cameraSelector = CameraSelector.DEFAULT_BACK_CAMERA // ํ›„๋ฉด ์นด๋ฉ”๋ผ ์„ ํƒ
            cameraProvider.bindToLifecycle(this, cameraSelector, preview, imageAnalysis) // ๋ฏธ๋ฆฌ๋ณด๊ธฐ ๊ธฐ๋Šฅ ์„ ํƒ, imageAnalysis ๊ฐ์ฒด ๋„ฃ์Œ -> ์ด๋ฏธ์ง€ ๋ถ„์„ ๊ธฐ๋Šฅ ์‚ฌ์šฉ ๊ฐ€๋Šฅ
        }, ContextCompat.getMainExecutor(this))
    }

    // ๋ฏธ๋ฆฌ๋ณด๊ธฐ ๊ธฐ๋Šฅ์„ ์„ค์ •ํ•˜๊ณ  ์„ค์ •์ด ์™„๋ฃŒ๋œ ๊ฐ์ฒด ๋ฐ™ํ™˜
    fun getPreview(): Preview {
        val preview: Preview = Preview.Builder().build() // Preview ๊ฐ์ฒด ์ƒ์„ฑ
        preview.setSurfaceProvider(binding.barcodePreview.getSurfaceProvider())
        return preview
    }

    // ์ด๋ฏธ์ง€ ๋ถ„์„ ๊ธฐ๋Šฅ
    fun getImageAnalysis(): ImageAnalysis {
        val cameraExecutor: ExecutorService = Executors.newSingleThreadExecutor()
        val imageAnalysis = ImageAnalysis.Builder().build()

        // Analyzer ์„ค์ •
        imageAnalysis.setAnalyzer(
            cameraExecutor,
            QRCodeAnalyzer(object : OnDetectListener {
                override fun onDetect(msg: String) {
                    if (!isDetected) {
                        isDetected = true // ๋ฐ์ดํ„ฐ๊ฐ€ ๊ฐ์ง€๋˜์—ˆ์œผ๋ฏ€๋กœ true๋กœ ๋ฐ”๊ฟ”์คŒ
                        val intent = Intent(this@MainActivity, ResultActivity::class.java)
                        intent.putExtra("msg", msg) // ์ธํ…ํŠธ๋กœ ์•กํ‹ฐ๋น„ํ‹ฐ ์ด๋™ ๋ฐ ํ‚ค-์Œ ํ˜•ํƒœ๋กœ ๋ฐ์ดํ„ฐ ์ „๋‹ฌ
                        startActivity(intent)
                    }
                }
            }))
        return imageAnalysis
    }
}

 

 

 

์ธํ„ฐํŽ˜์ด์Šค : OnDetectListener.kt

package com.limheejin.qrcodereader

interface OnDetectListener {
    fun onDetect(msg: String) // QRCodeAnalyzer์—์„œ QR์ฝ”๋“œ๊ฐ€ ์ธ์‹๋˜์—ˆ์„ ๋•Œ ํ˜ธ์ถœํ•  ํ•จ์ˆ˜, ๋ฐ์ดํ„ฐ ๋‚ด์šฉ์ด ์ธ์ˆ˜
}

 

์ธํ„ฐํŽ˜์ด์Šค ํ•„์š” ์ด์œ  : ์ธ์‹ ์™„๋ฃŒ ํ›„ ์ „๋‹ฌ์„ ์œ„ํ•œ ์†Œํ†ต ์ฐฝ๊ตฌ

 

 

QRCodeAnalyzer.kt

package com.limheejin.qrcodereader

import android.annotation.SuppressLint
import androidx.camera.core.ImageAnalysis
import androidx.camera.core.ImageProxy
import com.google.mlkit.vision.barcode.BarcodeScanning
import com.google.mlkit.vision.common.InputImage


class QRCodeAnalyzer(val onDetectListener: OnDetectListener) : ImageAnalysis.Analyzer {

    // ๋ฐ”์ฝ”๋“œ ์Šค์บ๋‹ ๊ฐ์ฒด ์ƒ์„ฑ
    private val scanner = BarcodeScanning.getClient()

    @SuppressLint("UnsafeOptInUsageError")
    override fun analyze(imageProxy: ImageProxy) {
        val mediaImage = imageProxy.image
        if (mediaImage != null) {
            val image =
                InputImage.fromMediaImage(mediaImage, imageProxy.imageInfo.rotationDegrees)
                // ์ด๋ฏธ์ง€๊ฐ€ ์ฐํž ๋‹น์‹œ ์นด๋ฉ”๋ผ์˜ ํšŒ์ „ ๊ฐ๋„๋ฅผ ๊ณ ๋ คํ•˜์—ฌ ์ž…๋ ฅ ์ด๋ฏธ์ง€๋ฅผ ์ƒ์„ฑ
            scanner.process(image) // ์ด๋ฏธ์ง€ ๋ถ„์„
                .addOnSuccessListener { qrCodes ->
                    for (qrCode in qrCodes) { // qrCodes์™€ ๊ฐ™์€ ๋ฐฐ์—ด์ธ ์ด์œ  : ํ•œ ํ™”๋ฉด์— ๋‹ค์ˆ˜์˜ QR์ฝ”๋“œ๊ฐ€ ์žˆ๋‹ค๋ฉด ๊ฐ๊ฐ ๋ฐฐ์—ด๋กœ ๋ณด๋‚ด๊ธฐ ์œ„ํ•ด
                        onDetectListener.onDetect(qrCode.rawValue ?: "")
                    }
                }
                .addOnFailureListener {
                    it.printStackTrace() // ์‹คํŒจ์‹œ ์—๋Ÿฌ๋ฅผ ๋กœ๊ทธ์— ํ”„๋ฆฐํŠธ
                }
                .addOnCompleteListener {
                    imageProxy.close() // ์Šค์บ๋„ˆ๊ฐ€ ์ด๋ฏธ์ง€๋ฅผ ๋ถ„์„ ์™„๋ฃŒํ–ˆ์„ ๋•Œ ์ด๋ฏธ์ง€ ํ”„๋ก์‹œ๋ฅผ ๋‹ซ์Œ
                }
        }
    }
}

 

 

ResultActivity.kt

package com.limheejin.qrcodereader

import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import com.limheejin.qrcodereader.databinding.ActivityResultBinding

class ResultActivity : AppCompatActivity() {
    private lateinit var binding: ActivityResultBinding

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = ActivityResultBinding.inflate(layoutInflater)
        setContentView(binding.root)

        val result = intent.getStringExtra("msg") ?: "๋ฐ์ดํ„ฐ๊ฐ€ ์กด์žฌํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค."
        setUI(result)
    }

    private fun setUI(result: String) {
        binding.tvResult.text = result
        binding.btnGoBack.setOnClickListener { finish() }
    }
}

 

 

 

 

 

 

 

 

 

๋ฐ˜์‘ํ˜•
 ๐Ÿ’ฌ C O M M E N T