2024. 5. 14. 17:28, ๐ฑAndroid Study
๋ฐ์ํ
์๋๋ก์ด๋ JetPack ์ค CameraX ๋ผ์ด๋ธ๋ฌ๋ฆฌ์, ๊ตฌ๊ธ ML ํคํธ๋ฅผ ์ฌ์ฉํด๋ณด๊ธฐ ์ํด QR์ฝ๋ ๋ฆฌ๋๊ธฐ๋ฅผ ๋ง๋ค์ด ๋ณด์๋ค.
์ญ์ Joyce๋์ ์์ ์ ์ฐธ๊ณ ํ์๋ค.
๐ก Android Jetpack
- ๊ตฌ๊ธ ์๋๋ก์ด๋ํ์์ ๊ณต์ ๋ฐํํ ๋ผ์ด๋ธ๋ฌ๋ฆฌ ๋ชจ์
- ์์ ์ฑ: ๋ชจ๋ ์๋๋ก์ด๋ ๋ฒ์ ๋ฐ ๋ค์ํ ๊ธฐ๊ธฐ์์ ์ผ๊ด๋๊ฒ ์๋ํ๋ ๋ผ์ด๋ธ๋ฌ๋ฆฌ์ด๋ฏ๋ก, ๋ณต์ก์ฑ ๋ฎ์ถ๊ณ ์์ ์ฑ ๋์
- ๊ฐํธ์ฑ: ๋ฐฑ๊ทธ๋ผ์ด๋ ์์ , ์๋ช ์ฃผ๊ธฐ ๊ด๋ฆฌ ๋ฑ์ ๋์ ํด์ฃผ์ด ๋ณด์ผ๋ฌํ๋ ์ดํธ ์ฝ๋๋ฅผ ์ค์ด๊ณ ๋ก์ง์๋ง ์ง์ค ๊ฐ๋ฅ
- ์ ฏํฉ ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ ๋ชจ๋ [androidx.์ด๋ฆ]๊ณผ ๊ฐ์ ํจํค์ง๋ช ์ผ๋ก ๊ตฌ์ฑ
- ์: AppSearch, CameraX, Compose, Data Binding, LiveData, WorkManager, Navigation, Test, ViewBinding ...
๐ก CameraX
- ๋ฏธ๋ฆฌ๋ณด๊ธฐ (์ฌ์ง ์ฐ๊ธฐ ์ ๋ฏธ๋ฆฌ๋ณด๊ธฐ ํ๋ฉด)
- ์ด๋ฏธ์ง ๋ถ์ (๋จธ์ ๋ฌ๋๊ณผ ๊ฐ์ ์ด๋ฏธ์ง ๋ถ์)
- ์ด๋ฏธ์ง ์บก์ฒ (์ด๋ฏธ์ง ์ ์ฅ)
๐ก ๊ตฌ๊ธ MLํคํธ
- ๊ตฌ๊ธ ๋จธ์ ๋ฌ๋ ๊ธฐ์ ์ ์๋๋ก์ด๋, 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