안드로이드 코틀린 : CameraX 로 사진 촬영하기
Lucy Archive
Lucy 2023
2021. 4. 5. 23:21

안드로이드 코틀린 : CameraX 라이브러리로 사진 찍기

CameraX 는 카메라 앱 개발을 쉽게 할 수 있도록 만들어진 Jetpack 지원 라이브러리 입니다. CameraX 라이브러리를 사용하여 Snow 어플과 같은 카메라 앱을 제작할 수 있습니다. 본 포스트는

CameraX 라이브러리를 사용하여 사진찍는 법

을 소개합니다. 안드로이드 개발자 가이드Codelab의 CameraX 설명이 있지만, CameraX는 베타버전으로 계속 업데이트를 진행중이라서 그런지, 안되는 부분이 있어 동작이 되도록 일부 수정한 코드입니다. 이 코드를 뼈대로 여러가지 기능을 추가할 예정입니다. Intent의 암시적 호출 방법으로 사진 찍는 방법은 아래의 포스트를 참고해주세요.

카메라 앱 호출해서 사진 찍기

 

안드로이드 코틀린 : 카메라 사진 찍기, 내부저장소, 외부저장소, 공용저장소 저장 방법 with FilePr

Android Kotlin : 기본 카메라 앱 호출해서 사진 찍기 Intent의 암시적 호출 방법으로 카메라를 호출하여 찍은 사진의 미리보기 및 원본을 내부저장소, 외부저장소, 공용저장소의 특정 폴더에 저장하

juahnpop.tistory.com

 

CameraX 라이브러리

CameraX 라이브러리는 아래와 같은 특징이 있습니다.

  • CameraX는 미리보기, 이미지 분석, 이미지 캡처 등의 use case를 활용해 카메라 앱을 제작 할 수 있습니다.
  • 추가적으로 인물 사진, HDR, 야간, 뷰티 등의 네이티브 카메라 기능을 편리하게 사용 할 수 있습니다. 
  • CameraX는 저수준의 기기별 코드를 포함할 필요가 없습니다. 
  • API 21부터 사용 가능

 

프로젝트 생성 및 환경 설정

프로젝트 생성

아래와 같이 안드로이드 프로젝트를 생성합니다.

  • 템플릿 : Empty Activity
  • 프로젝트명 : CameraX
  • 사용언어 : Kotlin

※ 이 포스트는 Android 4.1.2, Kotlin 1.4.31 버전으로 작성하였습니다.

 

ViewBinding 설정

App > Gradle Scripts > build.gradle 에 ViewBinding 설정을 위해 아래의 코드를 추가합니다. 코드 추가가 완료되면 Sync Now 를 클랫해서 Gradle 변경 사항을 적용합니다.

android { 
	... 
    viewBinding { 
    	enabled = true 
    } 
}

Gragle 수정 사항 적용

※ ViewBinding은 Layout에 있는 View의 Id를 코틀린 코드에서 직접 사용 할 수 있도록 해주는 도구입니다. View Binding과 관련된 설명은 아래의 링크를 참조해주세요.

안드로이드 View Binding 사용하기

 

안드로이드 View Binding 사용하기 - kotlin-android-extensions 지원 중단

안드로이드 View Binding 방법 정리 안드로이드 코드에서 레이아웃 View에 접근하기 위해 사용된 kotlin-android-extensions 의 지원이 중단예정으로, 이를 대체하여 사용 할 수 있는 ViewBinding 사용법에 대해

juahnpop.tistory.com

 

CameraX 라이브러리 종속 항목 선언

build.gradle(Project) 파일의 repositories 에 google()을 추가합니다. 아마 최근에 안드로이드 스튜디오를 설치하셨으면 기본으로 설정되어 있습니다. 

allprojects {
    repositories {
        google()
        jcenter()
    }
}

build.gradle(Module) 파일의 android 속성에 아래의 compileOptions 와 kotlinOptions 코드를 추가합니다. 이 부분도 최근에 안드로이드 스튜디오를 설치하셨으면 기본으로 설정 되어 있습니다. 추가로 dependencies 속성에 아래의 CameraX 라이브러리 종속성을 선언합니다. CameraX 는 현재 베타 버전으로 버전 정보는 확인해야 합니다. 

android{

	...
    
	compileOptions {
	    sourceCompatibility JavaVersion.VERSION_1_8
	    targetCompatibility JavaVersion.VERSION_1_8
	}
	// For Kotlin projects
	kotlinOptions {
	    jvmTarget = "1.8"
	}
}

dependencies {

	...
    
    // CameraX core library using the camera2 implementation
    def camerax_version = "1.0.0-rc03"
    // 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:1.0.0-alpha22"
    // If you want to additionally use the CameraX Extensions library
    implementation "androidx.camera:camera-extensions:1.0.0-alpha22"
}

 

권한 설정

Android.Manifest에 아래의 카메라 관련 권한 코드를 추가합니다. 본 포스트에서 권한 요청 코드는 생략하고, 수동으로 권한 설정 할 예정입니다.

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

 

레이아웃 작성

activity_main.xml 코드 작성

activity_main.xml 레이아웃 파일에 아래 코드와 같이 Button 과 PreviewView를 추가합니다. PreView는 카메라 미리보기로 사용하고, 버튼을 사진을 찍는 용도로 사용할 예정입니다.

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <Button
        android:id="@+id/camera_capture_button"
        android:layout_width="100dp"
        android:layout_height="100dp"
        android:layout_marginBottom="50dp"
        android:scaleType="fitCenter"
        android:text="Take Photo"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintBottom_toBottomOf="parent"
        android:elevation="2dp" />

    <androidx.camera.view.PreviewView
        android:id="@+id/viewFinder"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />

</androidx.constraintlayout.widget.ConstraintLayout>

activity_main.xml 레이아웃

 

코틀린 코드 작성

MainActivity.kt 코드 작성

MainActivity.kt 메인 엑티비티에 아래와 같이 코드를 작성합니다. 

package com.blacklog.camerax

import android.net.Uri
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.util.Log
import android.widget.Toast
import androidx.camera.core.*
import androidx.camera.lifecycle.ProcessCameraProvider
import androidx.core.content.ContextCompat
import com.blacklog.camerax.databinding.ActivityMainBinding
import java.io.File
import java.text.SimpleDateFormat
import java.util.concurrent.ExecutorService
import java.util.concurrent.Executors

class MainActivity : AppCompatActivity() {
    // ViewBinding
    lateinit private var binding : ActivityMainBinding

    private var preview : Preview? = null
    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)
        val view = binding.root
        setContentView(view)

        startCamera()

        binding.cameraCaptureButton.setOnClickListener {
            takePhoto()
        }

        outputDirectory = getOutputDirectory()

        cameraExecutor = Executors.newSingleThreadExecutor()
    }

    private fun takePhoto() {
        // Get a stable reference of the modifiable image capture use case
        val imageCapture = imageCapture ?: return

        // Create time-stamped output file to hold the image
        val photoFile = File(
                outputDirectory,
                newJpgFileName())

        // Create output options object which contains file + metadata
        val outputOptions = ImageCapture.OutputFileOptions.Builder(photoFile).build()

        // Set up image capture listener, which is triggered after photo has
        // been taken
        imageCapture.takePicture(
                outputOptions,
                ContextCompat.getMainExecutor(this),
                object : ImageCapture.OnImageSavedCallback {
                    override fun onError(exc: ImageCaptureException) {
                        Log.d("CameraX-Debug", "Photo capture failed: ${exc.message}", exc)
                    }

                    override fun onImageSaved(output: ImageCapture.OutputFileResults) {
                        val savedUri = Uri.fromFile(photoFile)
                        val msg = "Photo capture succeeded: $savedUri"
                        Toast.makeText(baseContext, msg, Toast.LENGTH_SHORT).show()
                        Log.d("CameraX-Debug", msg)
                    }
                })
    }

    // viewFinder 설정 : Preview
    private fun startCamera() {
        val cameraProviderFuture = ProcessCameraProvider.getInstance(this)

        cameraProviderFuture.addListener({
            // Used to bind the lifecycle of cameras to the lifecycle owner
            val cameraProvider: ProcessCameraProvider = cameraProviderFuture.get()

            // Preview
            preview = Preview.Builder()
                    .build()
                    .also {
                        it.setSurfaceProvider(binding.viewFinder.surfaceProvider)
                    }
            // ImageCapture
            imageCapture = ImageCapture.Builder()
                    .build()

            // Select back camera as a default
            val cameraSelector = CameraSelector.DEFAULT_BACK_CAMERA

            try {
                // Unbind use cases before rebinding
                cameraProvider.unbindAll()

                // Bind use cases to camera
                cameraProvider.bindToLifecycle(
                        this,
                        cameraSelector,
                        preview,
                        imageCapture)

            } catch(exc: Exception) {
                Log.d("CameraX-Debug", "Use case binding failed", exc)
            }

        }, ContextCompat.getMainExecutor(this))
    }

    private fun newJpgFileName() : String {
        val sdf = SimpleDateFormat("yyyyMMdd_HHmmss")
        val filename = sdf.format(System.currentTimeMillis())
        return "${filename}.jpg"
    }

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

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

startCamera() 메서드는 카메라 설정을 담당하는 메서드 입니다. 여기서 preview와 imageCapture 설정을 하고 cameraProvider.bindToLifecycle() 을 사용해 설정된 카메라의 수명 주기를 지정합니다. 위 코드에서 ImageAnalyzer를 사용하지는 않았지만, ImageAnalyzer를 추가하고 싶은 경우 startCamera() 에서 imageAnlyzer 설정 후 cameraProvider.bindToLifecycle() 메서드에 imageAnalyzer 인스턴스를 추가합니다.

  • cameraProvider.bindToLifecycle(this, cameraSelector, preview, imageCapture, imageAnalyzer)

takePicture() 메서드는 저장할 파일 경로와 파일명을 지정 후 ImageCapture 클래스의 takePicture() 메서드로 사진 찍느 코드를 작성 할 수 있습니다. 사진 촬영 실패 성공에 따라 콜백 함수 작성이 가능합니다. 

※ 권한 코드를 작성하면 코드만 너무 길어져 생략하였습니다. 권한 코드 작성은 아래의 이전 포스트를 참고해주세요. 

안드로이드 위험 권한(런타임 권한) 요청 코드 작성 방법

 

안드로이드 코틀린 : 런타임 권한(위험 권한) 요청 코드 작성 방법 - 카메라 및 외부 저장소 권한

안드로이드 권한2 : 위험 권한 허가 확인 및 허가 요청 코드 작성 방법 이전 포스트에서 안드로이드 권한에 대해 간략히 설명하고 일반 권한 사용법을 정리하였습니다. 이번 포스트에서는 카메

juahnpop.tistory.com

실행 결과

위 코드를 실행하면 아래와 같이 사진 촬영이 정상적으로 처리되고, 저장 경로가 토스트 메세지로 출력됩니다.

권한 수동 설정 및 실행 결과

끝까지 읽어 주셔서 감사합니다.^^

관련포스트

🤞 안드로이드(코틀린) 앱 제작하기 관련글 목록 보기

🤞 안드로이드(코틀린) 카메라 관련글 목록 보기

🤞 안드로이드(코틀린) CameraX 라이브러리 관련글 목록 보기