CameraX란?
개발자가 더 쉽게 카메라 기능을 구현할 수 있도록 만든 JetPack 라이브러리
안드로이드 공식 문서에선 카메라 앱 제작 시 CameraX를 사용하도록 권고하고 있습니다.
주요 특징
1. 광범위한 기기 호환성
Android 5.0(API 25)까지 지원됩니다. 이는 기존 안드로이드 기기의 98% 이상입니다.
2. 사용 편의성
광범위한 기기의 호환성을 고려하며 CameraX에선 카메라에서 기본적으로 필요한 기능들을 기본적으로 제공합니다.
1) 미리보기(Preview)
카메라로 보여지는 화면을 앱 상에 띄우는 기능입니다.
2) 이미지 캡처(ImageCapture)
Preview로 가져온 이미지를 캡처하여 MediaStore에 저장하는 기능입니다.
3) 이미지 분석(ImageAnalysis)
이미지처리, 컴퓨터 비전 또는 머신러닝에서 사용할 수 있도록 CPU에서 액세스 가능한 이미지로 변환해 주는 기능입니다.
4) 동영상 캡처(ViedoCapture)
동영상 및 오디오를 저장하는 기능입니다.
3. 기기 간 일관성
다양한 기기에도 카메라 동작을 일관되게 작동하도록 유지시키는 기능을 가지고 있습니다.
4. 카메라 확장 API
CameraX엔 카메라 확장 API가 존재하는데 이는 기본 카메라 앱과 동일한 기능을 가져올 수 있습니다.
HDR, 야간 모드, 인물 촬영 모드등등 기본 카메라에 있는 기능이라면 그 기능을 사용할 수 있습니다.
1. Preview 구현
기본 레이아웃 구성
res/values/string.xml
<resources>
<string name="app_name">My Application</string>
<string name="take_photo">Take Photo</string>
<string name="start_capture">Start Capture</string>
<string name="stop_capture">Stop Capture</string>
</resources>
activity_main.xml
<?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">
<androidx.camera.view.PreviewView
android:id="@+id/viewFinder"
android:layout_width="match_parent"
android:layout_height="0dp"
app:layout_constraintBottom_toTopOf="@id/lowerLayout"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_weight="4"/>
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/lowerLayout"
android:layout_width="match_parent"
android:layout_height="0dp"
app:layout_constraintTop_toBottomOf="@id/viewFinder"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintVertical_weight="1">
<androidx.appcompat.widget.AppCompatButton
android:id="@+id/image_capture_button"
android:layout_width="0dp"
android:layout_height="match_parent"
android:text="@string/take_photo"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"/>
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.constraintlayout.widget.ConstraintLayout>
Preview 기능을 사용하기 위해선 PreviewView 라는 뷰를 사용해야 합니다.
해당 뷰의 id를 viewFinder라고 두었습니다.
카메라 권한 불러오기
AndroidManifest.xml
<uses-feature android:name="android.hardware.camera.any" />
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"
android:maxSdkVersion="28" />
CameraX는 사진 또는 동영상을 저장할 때 MediaStore를 사용합니다.
이에 MeidaStore의 외부 저장소 쓰기 권한이 필요합니다.
MainActivity.kt
private lateinit var viewBinding: ActivityMainBinding
private lateinit var cameraExecutor: ExecutorService
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
viewBinding = ActivityMainBinding.inflate(layoutInflater)
setContentView(viewBinding.root)
if (allPermissionsGranted()) {
//차후 구현 예정
startCamera()
} else {
ActivityCompat.requestPermissions(
this, REQUIRED_PERMISSIONS, REQUEST_CODE_PERMISSIONS)
}
cameraExecutor = Executors.newSingleThreadExecutor()
}
private fun allPermissionsGranted() = REQUIRED_PERMISSIONS.all {
ContextCompat.checkSelfPermission(
baseContext, it) == PackageManager.PERMISSION_GRANTED
}
override fun onRequestPermissionsResult(
requestCode: Int, permissions: Array<String>, grantResults:
IntArray) {
if (requestCode == REQUEST_CODE_PERMISSIONS) {
if (allPermissionsGranted()) {
//차후 구현 예정
startCamera()
} else {
Toast.makeText(this,
"Permissions not granted by the user.",
Toast.LENGTH_SHORT).show()
finish()
}
}
}
override fun onDestroy() {
super.onDestroy()
cameraExecutor.shutdown()
}
//차후 카메라 구현에 필요한 const 및 권한 목록
companion object {
private const val TAG = "CameraXApp"
private const val FILENAME_FORMAT = "yyyy-MM-dd-HH-mm-ss-SSS"
private const val REQUEST_CODE_PERMISSIONS = 10
private val REQUIRED_PERMISSIONS =
mutableListOf (
Manifest.permission.CAMERA,
Manifest.permission.RECORD_AUDIO
).apply {
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.P) {
add(Manifest.permission.WRITE_EXTERNAL_STORAGE)
}
}.toTypedArray()
}
앱이 실행될 때 사용자에게 카메라 사용 권한을 받아야 하기 때문에 사용자에게 권한을 요청합니다.
startCamera() 메소드 구현
MainActivity.kt
private fun startCamera() {
val cameraProviderFuture = ProcessCameraProvider.getInstance(this)
cameraProviderFuture.addListener({
val cameraProvider: ProcessCameraProvider = cameraProviderFuture.get()
val preview = Preview.Builder()
.build()
.also {
it.setSurfaceProvider(viewBinding.viewFinder.surfaceProvider)
}
val cameraSelector = CameraSelector.DEFAULT_BACK_CAMERA
try {
cameraProvider.unbindAll()
cameraProvider.bindToLifecycle(
this, cameraSelector, preview)
} catch(exc: Exception) {
Log.e(TAG, "Use case binding failed", exc)
}
}, ContextCompat.getMainExecutor(this))
}
preview를 PreviewViewPreviewView에 뛰우는 코드입니다.
val cameraProviderFuture = ProcessCameraProvider.getInstance(this)
먼저 ProcessCameraProvider를 통해 CameraProvider의 인스턴스를 가져옵니다.
cameraProviderFuture.addListener({
}, ContextCompat.getMainExecutor(this))
ProcessCameraProvider로 받은 인스턴스에 리스너를 추가하는데 이때 Runnable을 하나의 인수로 추가합니다.
ContextCompat.getMainExecutore()를 두 번째 인수로 추가하여 기븐 스레드에서 실행되는 Executor를 반환할 수 있게 합니다.
val cameraProvider: ProcessCameraProvider = cameraProviderFuture.get()
차후 카메라의 수명 주기를 액티비티에 바인딩시키기 위해 Runnable 내에서 ProcesscameraProvider를 다시 가져옵니다.
val preview = Preview.Builder()
.build()
.also {
it.setSurfaceProvider(viewBinding.viewFinder.surfaceProvider)
}
Preview 객체를 초기화합니다. 이때 사용할 view를 지정합니다.
val cameraSelector = CameraSelector.DEFAULT_BACK_CAMERA
기본 후면 카메라를 사용할 것이기 때문에 CameraSelector에서 DEFAULT_BACK_CAMERA를 가져옵니다.
여기서 전면 카메라를 사용하기 위해선
CameraSelector.DEFAULT_FRONT_CAMERA
를 사용해 주시면 됩니다.
try {
cameraProvider.unbindAll()
cameraProvider.bindToLifecycle(
this, cameraSelector, preview)
} catch(exc: Exception) {
Log.e(TAG, "Use case binding failed", exc)
}
마지막으로 카메라 수명 주기를 액티비티에 바인딩합니다.
카메라의 수명 주기를 액티비티에 바인딩하게 되기 때문에
따로 카메라를 닫거나 여는 작업을 하지 않아도 됩니다.
결과물
2. ImageCapture 구현
MainActivity.kt
private lateinit var viewBinding: ActivityMainBinding
private var imageCapture: ImageCapture? = null
private lateinit var cameraExecutor: ExecutorService
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
viewBinding = ActivityMainBinding.inflate(layoutInflater)
setContentView(viewBinding.root)
if (allPermissionsGranted()) {
startCamera()
} else {
ActivityCompat.requestPermissions(
this, REQUIRED_PERMISSIONS, REQUEST_CODE_PERMISSIONS)
}
viewBinding.imageCaptureButton.setOnClickListener { takePhoto() }
cameraExecutor = Executors.newSingleThreadExecutor()
}
사진 촬영을 위해 버튼에 클릭 리스너를 추가해 줍니다. 또한 차후에 사용할 imageCapture 객체도 생성해 둡니다.
takePhoto() 메소드 구현
private fun takePhoto() {
val imageCapture = imageCapture ?: return
val name = SimpleDateFormat(FILENAME_FORMAT, Locale.US)
.format(System.currentTimeMillis())
val contentValues = ContentValues().apply {
put(MediaStore.MediaColumns.DISPLAY_NAME, name)
put(MediaStore.MediaColumns.MIME_TYPE, "image/jpeg")
if(Build.VERSION.SDK_INT > Build.VERSION_CODES.P) {
put(MediaStore.Images.Media.RELATIVE_PATH, "Pictures/CameraX-Image")
}
}
val outputOptions = ImageCapture.OutputFileOptions
.Builder(contentResolver,
MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
contentValues)
.build()
imageCapture.takePicture(
outputOptions,
ContextCompat.getMainExecutor(this),
object : ImageCapture.OnImageSavedCallback {
override fun onError(exc: ImageCaptureException) {
Log.e(TAG, "Photo capture failed: ${exc.message}", exc)
}
override fun
onImageSaved(output: ImageCapture.OutputFileResults){
val msg = "Photo capture succeeded: ${output.savedUri}"
Toast.makeText(baseContext, msg, Toast.LENGTH_SHORT).show()
Log.d(TAG, msg)
}
}
)
}
사진 저장은 MediaStore를 통하여 진행되기 때문에 MediaStore에 저장될 수 있도록 미리 양식을 지정해둬야합니다.
val name = SimpleDateFormat(FILENAME_FORMAT, Locale.US)
.format(System.currentTimeMillis())
val contentValues = ContentValues().apply {
put(MediaStore.MediaColumns.DISPLAY_NAME, name)
put(MediaStore.MediaColumns.MIME_TYPE, "image/jpeg")
if(Build.VERSION.SDK_INT > Build.VERSION_CODES.P) {
put(MediaStore.Images.Media.RELATIVE_PATH, "Pictures/CameraX-Image")
}
}
name 변수엔 현재 시작을 파일 이름 포맷에 맞게 변환해서 저장해 두었고
contentValues는 MediaStore에 저장하기 위해서 필요한 칼럼의 key에 맞는 value를 저장해 둔 ContentValues입니다.
MediaStore.MediaColumns.DISPLAY_NAME, MediaStore.MediaColumns.MIME_TYPE, MediaStore.Images.Media.RELATIVE_PATH, 가 칼럼의 key라 보시면 됩니다.
val outputOptions = ImageCapture.OutputFileOptions
.Builder(contentResolver,
MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
contentValues)
.build()
ImageCapture의 OutputFileOptions 객체를 사용해 출력 방법을 지정할 수 있으며,
MediaStore에 저장하기 때문에 contentResolver,
저장 위치인 MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
위에서 저장한 contentValues를 지정합니다.
imageCapture.takePicture(
outputOptions,
ContextCompat.getMainExecutor(this),
object : ImageCapture.OnImageSavedCallback {
override fun onError(exc: ImageCaptureException) {
Log.e(TAG, "Photo capture failed: ${exc.message}", exc)
}
override fun onImageSaved(output: ImageCapture.OutputFileResults){
val msg = "Photo capture succeeded: ${output.savedUri}"
Toast.makeText(baseContext, msg, Toast.LENGTH_SHORT).show()
Log.d(TAG, msg)
}
}
)
이미지 캡처를 실행하는 메소드로 캡처를 진행했을 때 결과를 받기 위한 콜백함수를 작성해야 합니다.
onError()는 캡처에 실패했을 경우, onImageSaved는 캡처에 성공했을 경우를 나타냅니다.
startCamera() 메소드 변경
private fun startCamera() {
val cameraProviderFuture = ProcessCameraProvider.getInstance(this)
cameraProviderFuture.addListener({
val cameraProvider: ProcessCameraProvider = cameraProviderFuture.get()
val preview = Preview.Builder()
.build()
.also {
it.setSurfaceProvider(viewBinding.viewFinder.surfaceProvider)
}
imageCapture = ImageCapture.Builder()
.build()
val cameraSelector = CameraSelector.DEFAULT_BACK_CAMERA
try {
cameraProvider.unbindAll()
cameraProvider.bindToLifecycle(
this, cameraSelector, preview, imageCapture)
} catch(exc: Exception) {
Log.e(TAG, "Use case binding failed", exc)
}
}, ContextCompat.getMainExecutor(this))
}
변경된 부분은 다음과 같습니다.
imageCapture = ImageCapture.Builder()
.build()
ImageCapture 객체를 생성하는 부분입니다.
cameraProvider.bindToLifecycle(
this, cameraSelector, preview, imageCapture)
카메라의 생명 주기를 바인딩할 때 생성한 ImageCapture 객체 또한 인수로 넘겨줍니다.
결과
'Android' 카테고리의 다른 글
[안드로이드] LiveData에 관하여 (0) | 2023.08.24 |
---|---|
[안드로이드]안드로이드 앱 모듈화 (0) | 2023.08.13 |
[안드로이드] CustomTextView (0) | 2023.07.30 |
[안드로이드] 서비스 (0) | 2023.07.22 |
[안드로이드] 콘텐츠 제공자(ContentProvider) 와 콘텐츠 리졸버(ContentResolver) (0) | 2023.07.16 |