안드로이드 코틀린 : CameraX 라이브러리로 사진 찍기
CameraX 라이브러리를 사용하여 사진찍는 법
을 소개합니다. 안드로이드 개발자 가이드와 Codelab의 CameraX 설명이 있지만, CameraX는 베타버전으로 계속 업데이트를 진행중이라서 그런지, 안되는 부분이 있어 동작이 되도록 일부 수정한 코드입니다. 이 코드를 뼈대로 여러가지 기능을 추가할 예정입니다. Intent의 암시적 호출 방법으로 사진 찍는 방법은 아래의 포스트를 참고해주세요.
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
}
}
※ ViewBinding은 Layout에 있는 View의 Id를 코틀린 코드에서 직접 사용 할 수 있도록 해주는 도구입니다. View Binding과 관련된 설명은 아래의 링크를 참조해주세요.
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>
코틀린 코드 작성
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() 메서드로 사진 찍느 코드를 작성 할 수 있습니다. 사진 촬영 실패 성공에 따라 콜백 함수 작성이 가능합니다.
※ 권한 코드를 작성하면 코드만 너무 길어져 생략하였습니다. 권한 코드 작성은 아래의 이전 포스트를 참고해주세요.
안드로이드 위험 권한(런타임 권한) 요청 코드 작성 방법
실행 결과
위 코드를 실행하면 아래와 같이 사진 촬영이 정상적으로 처리되고, 저장 경로가 토스트 메세지로 출력됩니다.
끝까지 읽어 주셔서 감사합니다.^^
'Programming > Android App(Kotlin)' 카테고리의 다른 글
안드로이드 코틀린 : imageView에 올바르게 회전 된 Bitmap 삽입하기(with CameraX) (0) | 2021.04.10 |
---|---|
안드로이드 코틀린 : 사진의 Exif 데이터 읽기, 쓰기 - ExifInterface (0) | 2021.04.08 |
안드로이드 코틀린 : 일반 클래스 또는 Fragment에서 MainActivity의 메서드 사용하기 (0) | 2021.04.05 |
안드로이드 코틀린 : 카메라 사진 찍기, 내부저장소, 외부저장소, 공용저장소 저장 방법 with FileProvider (5) | 2021.04.05 |
안드로이드 코틀린 : RecyclerView로 목록 만들기(with ViewBinding) (2) | 2021.03.31 |