https://developer.android.com/codelabs/camerax-getting-started#0
Getting Started with CameraX | Android Developers
This codelab introduces how to create a camera app that uses CameraX to show a viewfinder, take photos and analyze an image stream from the camera.
developer.android.com
https://itstudy-mary.tistory.com/361
Android Studio, Kotlin] 12. CameraX로 사진을 찍고 미리보기를 띄우기
앞서 이야기 한 적 있는데, 일반 Camera2를 사용하면 기기마다 다 다른 옵션을 설정해야한다. (이거랑 이어서 보면 죠습니다) itstudy-mary.tistory.com/359 Android Studio, Kotiln] 11. 카메라 셔터st한 깜빡 애..
itstudy-mary.tistory.com
CameraX
카메라 앱 개발을 더 쉽게 할 수 있도록 만들어진 Jetpack 지원 라이브러리. 대부분의 Android 기기에서 작동하는 일관되고 사용하기 쉬운 API 영역을 제공하고 Android 5.0 (API 수준 21) 까지 하위 호환된다.
CameraX는 *camera2의 기능을 활용하면서 수명 주기를 인식하고 Use Case에 기반하여 더 간단한 접근 방식을 따른다. 또한 기기 호환성 문제를 해결하므로 개발자가 코드베이스에 일일히 기기별 코드를 포함할 필요가 없다. 따라서 카메라 기능을 앱에 추가해야할 때 필요한 코드량이 확줄어든다.
또한 CameraX를 통해 적은 양의 코드로 기본 카메라말고 확장된 카메라를 사용할 수 있다 . ExtensionAPI를 활용하여 빛망울 효과, HDR, 야간모드, 얼굴 보정 등의 효과를 추가할 수 있다.
CameraX Use Case
- Preview : 화면에 사용자가 찍을 화면을 미리 가져와서 보여준다.
- ImageCaputre : 사용자가 화면을 찍으면 해당 이미지를 MediaStore등에 저장한다.
- ImageAnalysis : 사용자가 캡쳐한 이미지에 대해 분석이 가능하게끔 만든다. (아직 잘모름)
- VideoCapture : 동영상 및 오디오를 캡쳐할 수 있다.
해당 Use Case는 출시된 대부분의 기기에서 동일한 코드로 사용할 수 있다.
CameraX 와 camera2 의 차이
CameraX 같은 경우, 앱에서 카메라를 사용하긴 해야하는데 곁다리로 쓰일때 (신분증 인증, 여권사진 인증 등) 적은 양의 코드로 빠른 개발을 가능하게 만든다. 또한 앱의 수명주기를 따르기 때문에 카메라를 열고, 닫는 불필요한 코드도 줄어든게 된다.
camera2 의 경우는 앱의 주된 기능이 카메라를 사용해야 되는 경우 (스노우, 인스타그램 등 카메라 어플)에 카메라의 세세한 부분까지 커스텀해가면서 사용할 수 있다. 단 그만큼 코드양이 늘어나게되고, 각 기기별로 코드를 만들어줘야 하기 때문에 개발시간이 길어진다.
Gradle Dependency
def camerax_version = "1.1.0-beta01"
implementation "androidx.camera:camera-core:${camerax_version}"
implementation "androidx.camera:camera-camera2:${camerax_version}"
implementation "androidx.camera:camera-lifecycle:${camerax_version}"
implementation "androidx.camera:camera-video:${camerax_version}"
implementation "androidx.camera:camera-view:${camerax_version}"
implementation "androidx.camera:camera-extensions:${camerax_version}"
CarmeraX의 기능을 사용하기 위한 Permission 요청 - 자세한 설명 패스
App이 Camera를 Open하기 위해서는 사용자의 승인이 필요하다. 이는 오디오를 녹음할 때나 Mediastore에 미디어 저장 또는 열람할 때도 승인이 필요하다.
- AndroidMenifest.xml, before application Tag
<?xml version="1.0" encoding="utf-8"?> <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.example.cameraxexample"> <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"/> <application ... </application> </manifest>
다음과 같은 Permission을 요청한다.- <uses-feature android:name="android.hardware.camera.any"/> : 해당 기기가 카메라를 가지고 있는지 확인하는 태그이다. name 마지막 ".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"/> : 외부 저장소 (Mediastore) 사용에 대한 접근 허용 요청. SDK 버전은 28이하일 경우에만 허용 요청을 한다.
- <uses-feature android:name="android.hardware.camera.any"/> : 해당 기기가 카메라를 가지고 있는지 확인하는 태그이다. name 마지막 ".any"를 통해 해당 기기가 후면 카메라 또는 전면 카메라를 가지고 있다면 정상 작동된다.
- MainActivity.kt
메인 액티비티에서는 유저들이 Permission 요청에 대해 승인 또는 거절하였을 때 App이 어떤 action을 취해야 하는지 구현할 수 있다.
companion object - 싱글톤 객체를 이용하여 Permission 수행과 관련된 변수들을 설정해준다. 권한 요청에 대한 부분은 나중에 따로 상세하게 정리해보자companion object { 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() }
https://developer.android.com/training/permissions/requesting.html?hl=ko#perm-check 이거 보면서!
override fun onRequestPermissionsResult( requestCode: Int, permissions: Array<String>, grantResults: IntArray ) { super.onRequestPermissionsResult(requestCode, permissions, grantResults) if (requestCode == REQUEST_CODE_PERMISSIONS) { if (allPermissionsGranted()) { startCamera() } else { Toast.makeText( this, "Permissions not granted by the user.", Toast.LENGTH_SHORT ).show() finish() } } }
requestCode가 10이랑 동일한지 확인하고 (왜 확인하는진 아직 모름) 이 후
allPermissionGranted() 메서드를 통해 모든 요청이 승인 받았는지 확인.
승인 됐다면 -> Camera를 사용할 준비가 완료 됐음을 의미. 카메라 시작. startCamera()
그렇지 않다면 -> 승인 요청이 허용되지 않았음을 유저에게 알림 (by Toast msg)
-private fun allPermissionsGranted()
private fun allPermissionsGranted() = REQUIRED_PERMISSIONS.all { ContextCompat.checkSelfPermission( baseContext, it ) == PackageManager.PERMISSION_GRANTED }
CameraX Preview 구현
- 레이아웃 파일에 PreviewView 추가.
PreviewView는 유저들이 찍을 사진을, 미리 볼 수 있는 환경을 제공한다.
이 후, 코틀린 코드를 통해 PreviewView에 cameraX Preview Class를 연동해준다.//In main_activity.xml <androidx.camera.view.PreviewView android:id="@+id/viewFinder" android:layout_width="match_parent" android:layout_height="match_parent" /> - UI Component에서 CameraProvider 요청
CameraProvider요청은 꼭 startCamera 메서드 내에서 이루어지지 않아도 된다.private fun startCamera() { val cameraProviderFuture = ProcessCameraProvider.getInstance(this) }
- val cameraProviderFuture = ProcessCameraProvider.getInstance(this)class MainActivity : AppCompatActivity() { private lateinit var cameraProviderFuture : ListenableFuture<ProcessCameraProvider> override fun onCreate(savedInstanceState: Bundle?) { cameraProviderFuture = ProcessCameraProvider.getInstance(this) } }
ProcessCameraProvider의 인스턴스는 카메라의 수명주기와 수명주기Owner를 연동하기 위한 인스턴스이다. - CameraProvider 사용여부 확인.
- cameraProcessProvider.addListener (Runnable{}, Executor)
CameraProvider를 요청한 후에는 뷰를 만들 때 초기화에 성공했는지 확인한다. .addListener() 를 호출하여 cameraProvider가 초기화가 잘되었는지 확인가능하며, 지정한 executor 에서 실행하게 만든다.cameraProviderFuture.addListener({ val cameraProvider: ProcessCameraProvider = cameraProviderFuture.get() ... }, ContextCompat.getMainExecutor(this) )
다음 코드 부터는 모두 Runnable{} 내에 속하는 코드들이다. - PreView 구성 - Use Case
//In Runnalbe val preview = Preview.Builder() .build() .also { it.setSurfaceProvider(viewBinding.viewFinder.surfaceProvider) }
Preview.Builder() 를 사용하여 Preview를 PreviewView와 연결해준다.
그 방법은 해당Preview의 surfaceProvider를 PreviewView의 surfaceProvider로 설정해주는 것. - CameraSelector 선정
val cameraSelector = CameraSelector.DEFAULT_BACK_CAMERA
Device의 어떤 카메라로 촬영을 할 지 선택하는 코드이다. - 카메라의 수명주기를 UI의 수명주기와 연동.
try{} 구문 내에 코드를 작성한다.//Runnable{P} try { cameraProvider.unbindAll() cameraProvider.bindToLifecycle( this, cameraSelector, preview, ) } catch (exc: Exception) { Log.e(TAG, "use case binding failed", exc) }
- cameraProvider.unbindAll() : CameraProcessProvider와 기존에 연동된 수명주기를 모두 해제한다.
- cameraProvider.bindToLifecycle() : 카메라 수명주기를 다음과 같은 옵션으로 UI의 수명주기와 연동시킨다.
- 첫번째 인자 - LifecycleOwner : 해당 activity의 수명주기와 연동하기 위해 this를 넘긴다. (Fragment였다면 viewLifecycleOwner 를?)
- 두번째 인자 - CameraSelector : 카메라 셀렉터를 넘겨준다.
- 세번째 인자 - Preivew : Preview를 넘겨준다.
- 만약 제대로 수명주기가 연동이 되지 않았다면 (UI의 수명주기로 인해 또는, 프리뷰,카메라 셀렉터가 잘못됐을 경우) catch{} 구문을 통해 Log을 날려준다.
CameraX ImageCapture 구현
CameraX를 이용하여 이미지 캡처를 구현하기 위한 과정 또한 위의 Preview를 구현하는 과정과 비슷하다.
- ImageCaputre Use Case 인스턴스화. - 내용 구현.
- Camera의 lifecycle (ImageCaputre) 과 LifecyclerOwner 연동.
ImageCaputre의 Use Case 를 구현해보도록 하자.
ImageCapture 인스턴스 초기화
ImageCaputre 인스턴스는 CameraProcessProvider가 제대로 구현된 뒤에 생성하는 것이 바람직하다.
따라서 CameraProcessProvider의 사용여부가 확인된 다음에 ImageCaputre 인스턴스를 초기화하도록 하자.
//전역변수에 imageCapture을 정의.
private var imageCapture : ImageCapture? = null
전역변수로 ImageCapture? 타입의 변수를 정의한 다음,
//ImageCapture는 cameraProcessProvider의 사용여부가 확인되면 초기화한다.
cameraProviderFuture.addListener({
val cameraProvider: ProcessCameraProvider = cameraProviderFuture.get()
//ImageCapture.Builder()를 통해서 build()
imageCapture = ImageCapture.Builder().build()
}, ContextCompat.getMainExecutor(this)
)
cameraProcessProvider가 사용될 때 imageCapture변수를 ImageCapture 타입의 값으로 초기화한다.
이 후 bindToLifecycle
try {
cameraProvider.unbindAll()
cameraProvider.bindToLifecycle(
this,
cameraSelector,
preview,
imageCapture,
)
} catch (exc: Exception) {
Log.e(TAG, "use case binding failed", exc)
}
ImageCapture Use Case 구현
일반적으로 카메라에서의 ImageCapture 는 보통 카메라 찍기 버튼을 누를 때 시작된다.

위 사진에서 처럼 하단 중간에 하얀색 버튼을 누르면 이미지캡처가 이루어진다. 이를 코드로 구현하면
viewBinding.imageCaptureButton.setOnClickListener { takePhoto() }
이러한 코드가 나온다.
이처럼 takePhoto() 메서드를 통해 ImageCapture Use case를 구현하고자 한다.
private fun takePhoto() {}
- imageCapture의 초기화 상태 확인 후, Use Case 구현
지역 변수 imageCapture는 전역 변수인 imageCapture의 값으로 초기화 된다.private fun takePhoto() { val imageCapture = imageCapture ?: return }
※만약 전역 변수의 값이 null이라면 카메라 사용 여부가 확인되지 않았기에 return 구문을 통해 함수로 부터 빠져나온다. - ImageCapture.takePicture() 함수 구현하기.
ImageCapture Use Case에는 이미지를 캡처할 수 있는 두가지 메서드를 제공한다.
- takePicture( Executor, OnImageCapturedCallback) : 캡처된 이미지의 메모리 내 버퍼를 제공.
- takePicture( OutPutFileOptions, Executor, OnImageSavedCallback) : 캡처된 이미지를 제공된파일 위치에 저장.
이번 예제에서는 두번째 메서드를 사용한다.
1. OutPutFileOptions의 ContentValue 설정
캡처된 이미지를 이미지 파일로 변경하기 위해, "이미지 파일명", "이미지의 확장자", "이미지 저장 경로 External Storage (외부 저장소)" 를 정의할 수 있다.
val name = SimpleDateFormat(FILENAME_FORMAT, Locale.KOREA).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 name = SimpleDataFormat(FILENAME_FORMAT, Locale.KOREA).format(System.currentTimeMillis())
compaion object { private const val FILENAME_FORMAT = "yyyy-MM-dd-HH-mm-ss-SSS" }
이미지 파일명을, FILENAME_FORAMT 형식으로, 포맷한다. System의 currentTimeMillis 메서드를 이용하여 이미지가 캡처될 당시의 현재 시간을 값으로 가져온다.
- val contentValues = ContentValues().apply{}
ContentValues 타입은 OutputFileOptions을 구성하는데 필요한 값이다.
MediaStore.MediaColumns.DISPLAY_NAME : 안드로이드 기본 갤러리앱에 저장될 미디어의 이름을 결정.
MediaStore.MediaColumns.MIME_TYPE : 안드로이드 기본 갤러리앱에 저장될 미디어의 확장자를 결정.
MediaStore.Images.Meida.RELATIVE_PATH : 안드로이드 기본 갤러리앱에 저장될 이미지의 상대 경로를 결정.
(단, 상대 경로 설정은 API P 이상에서만 가능)
위 3가지 값을 정의하여 ContentValues를 build 할 수 있다.
2.OutPutFileOptions Build
OutPutFileOptions은 ImageCapture.OutPutFileOptions.Builder()를 이용하여 초기화할 수 있다.
val outputOptions = ImageCapture.OutputFileOptions.Builder( contentResolver, MediaStore.Images.Media.EXTERNAL_CONTENT_URI, contentValues ).build()
ContentValues를 이용하여 OutputFileOptions을 초기화할 때는 3가지 인자를 넘긴다.
contentResolver : ContentValue를 decode하는 역할?
MediaStore.Images.Media.EXTERNAL_CONTENT_URI : 잘모르겠다. table에 삽입될 URL을 의미?
contentValues : 위에서 생성한 ContentValues값을 넘긴다.
3.ImageCapture.takePicture(OutputFileOptions, Executor, OnImageSavedCallback) 구현
imageCapture.takePicture( outputOptions, ContextCompat.getMainExecutor(this), object : ImageCapture.OnImageSavedCallback { override fun onError(exc: ImageCaptureException) { Log.d(TAG, "Photo capture failed : ${exc.message}", exc) } override fun onImageSaved(outputFileResults: ImageCapture.OutputFileResults) { val msg = "Photo capture succeeded : ${outputFileResults.savedUri}" Toast.makeText(baseContext, msg, Toast.LENGTH_SHORT).show() outputFileResults.savedUri?.let { setImageView(it) } Log.d(TAG, msg) } } )
첫번째 인자에는 위에서 생성한 outputFileOptions을 넘겨주고,
두번째 인자에는 ConextCompat.getMainExecutor(this)을 넘겨준다.
세번째 인자에는 OnImageSavedCallback 인터페이스를 구현하여 넘겨주어야 한다. -> 익명 객체로 정의하여 넘겨주자.
-object : ImageCapture.OnImageSavedCallback {} : onError() 메서드와 onImageSaved() 메서드를 override.
onError() : 이미지 캡처에 실패했을 때 또는 이미지 저장에 실패했을 때 실행된다.
onImageSaved() : 이미지 캡처에 성공하여 지정된 장소에 이미지를 저장한다.
4.override fun onImageSaved(outputFileOptions : ImageCapture.OutputFileResults)
imageCapture에 성공했을 경우 해당 함수가 호출되기 때문에 해당 함수 내에서 ImageCapture를 가지고 많은 코드를 구현할 수 있다.
- outputFileOptions.saveUri : 캡처된 이미지의 Uri
- setImageView(saveUri) : saveUri를 가지고 곧바로 imageView에 set.
private fun setImageView(saveUri: Uri) { viewBinding.imageCaptureButton.visibility = View.GONE viewBinding.videoCaptureButton.visibility = View.GONE viewBinding.frameLayoutPreview.visibility = View.VISIBLE // Glide.with(viewBinding.imageViewPreview.context) // .load(saveUri) // .into(viewBinding.imageViewPreview) viewBinding.imageViewPreview.setImageURI(saveUri) }
In xml.
<FrameLayout android:id="@+id/frameLayoutPreview" android:layout_width="match_parent" android:layout_height="match_parent" app:layout_constraintTop_toTopOf="parent" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" android:visibility="gone" > <ImageView android:id="@+id/imageViewPreview" android:layout_width="match_parent" android:layout_height="match_parent" android:background="@color/color_1E1E1E"/> </FrameLayout>

