App/Android Native

카메라 기능 구현 w cameraX in kotlin

Agrafenaaa 2021. 9. 30. 14:54

참조: https://www.youtube.com/watch?v=HjXJh_vHXFs

 

Spring으로 쇼핑몰 구현했을 때도 느꼈지만 파일 업로드/저장은 언어, 프레임 무관하게 어렵다. 

게다가 안드로이드는 보안이나 기타 설정 등으로 그 난이도가 제곱은 되는 듯하다. 

이것저것 다 시도하다가 가장 디버깅 요소가 적었던 cameraX를 여기에 적어보며 솔직히 아직도 알고리즘이 아니라 프로세스에 대한 이해만 있는 만큼 재차 복습할 것 😬

 

 

❓ emulator의 자체 카메라 사용이 아닌 실제 웹캠 사용을 원할 경우 AVD 설정 바꿔줄 것

AVD MANAGER -> 시뮬할 모델 Edit -> Show advanced settings -> Camera (Front: Webcam, Back: Webcam) 

 

1. 환경 설정(+ minSdk 26/ targetSdk 31)

Module
Manifest

2. CONSTANTS

import ANDROID.MANIFEST instead of JAVA.UTIL.JAR.MANIFEST

3. 로직 

 

주안점

- ouputdir 설정

- permissions 여부에 따라 if-else문으로 처리(앱 카메라 비허용일 경우 ActivityCompat의 requestPermissions 호출)

- externalMEdiaDirs.firstOrNull()?.let

- 사진촬영 로직 중 imageCapture nullable 설정

- imageCApture의 takePicture - two functions overriden : onImageSaved, onError 

- preview 화면 - processCameraProvider 객체 생성 후 이벤트 처리

- onDestory ... ?

 

class MainActivity : AppCompatActivity() {

    private lateinit var binding: ActivityMainBinding
    private var imageCapture: ImageCapture? = null
    private lateinit var outputDirectory: File
//    private lateinit var cameraExecutor: ExecutorService

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

        outputDirectory = getOutputDirectory()
//        cameraExecutor = Executors.newSingleThreadExecutor()

        if(allPermissionGranted()){
            startCamera()
        }else{
            ActivityCompat.requestPermissions(
                this, Constants.REQUIRED_PERMISSIONS,
                Constants.REQUEST_CODE_PERMISSIONS
            )
        }

        binding.btnTakePicture.setOnClickListener{
            takePhoto()
        }
    }

    private fun getOutputDirectory(): File {
        val mediaDir = externalMediaDirs.firstOrNull()?.let { mFile ->
            File(mFile, resources.getString(R.string.app_name)).apply {
                mkdirs()
            }
        }
        return if(mediaDir != null && mediaDir.exists())
            mediaDir else filesDir
    }

    private fun takePhoto() {
        val imageCapture = imageCapture ?: return
        val photoFile = File(
            outputDirectory,
            SimpleDateFormat(Constants.FILE_NAME_FORMAT,
                Locale.getDefault()).format(System.currentTimeMillis())+".jpg")
        val outputOption = ImageCapture
            .OutputFileOptions
            .Builder(photoFile)
            .build()

        imageCapture.takePicture(
            outputOption, ContextCompat.getMainExecutor(this),
            object :ImageCapture.OnImageSavedCallback{
                override fun onImageSaved(outputFileResults: ImageCapture.OutputFileResults) {
                    val savedUri = Uri.fromFile(photoFile)
                    val msg = "Photo saved successfully"

                    Toast.makeText(this@MainActivity,
                                        "$msg $savedUri",
                                        Toast.LENGTH_LONG
                    ).show()
                }

                override fun onError(exception: ImageCaptureException) {
                    Log.e(Constants.TAG, "onERROR: ${exception.message}", exception)
                }

            }
        )
    }

    private fun startCamera(){

        val cameraProviderFuture = ProcessCameraProvider.getInstance(this)

        cameraProviderFuture.addListener({
            val cameraProvider: ProcessCameraProvider = cameraProviderFuture.get()
            val preview = Preview.Builder()
                    .build()
                    .also { mPreview ->

                        mPreview.setSurfaceProvider(
                            binding.viewFinder.surfaceProvider
                        )
                    }
            imageCapture = ImageCapture.Builder()
                .build()

            val cameraSelector = CameraSelector.DEFAULT_BACK_CAMERA

            try {
                cameraProvider.unbindAll()
                cameraProvider.bindToLifecycle(
                    this, cameraSelector, preview, imageCapture
                )
            }catch (e: Exception){
                Log.d(Constants.TAG, "startCamera Fail: ", e)
            }
        }, ContextCompat.getMainExecutor(this))
    }

    override fun onRequestPermissionsResult(
            requestCode: Int,
            permissions: Array<String>,
            grantResults: IntArray) {
        super.onRequestPermissionsResult(requestCode, permissions, grantResults)

        if(requestCode == Constants.REQUEST_CODE_PERMISSIONS){

            if(allPermissionGranted()){
                startCamera()
            }else{
                Toast.makeText(this, "Permissions not granted", Toast.LENGTH_SHORT).show()

                finish()
            }

        }
    }

    private fun allPermissionGranted() =
        Constants.REQUIRED_PERMISSIONS.all {
            ContextCompat.checkSelfPermission(
                baseContext, it
            ) == PackageManager.PERMISSION_GRANTED
        }

//    override fun onDestory(){
//        super.onDestroy()
//        cameraExecutor.shutdown()
//    }

    }