카메라는 소셜 미디어와 공유할 동영상 및 이미지를 캡처하고 문서 및 QR 코드 스캔과 같은 유틸리티를 만드는 등 다양한 애플리케이션 사용 사례를 지원한다.
Anroid 앱에 카메라 기능을 추가하려는 경우 크게 세 가지 옵션들이 있지만 이중 Camera는 더 이상 지원을 안하고 중단되었다.
대부분 개발자는 CameraX를 사용하여 간단하게 카메라 기능을 사용할 수 있다.
하지만 카메라의 복잡한 기능들을 사용하기 위해서는 Camera2를 사용하는 것이 좋다.(예를 들어 수동 초점, 고속 연사, 10비트 컬러 등의 기능을 사용할 때)
간단히 얘기하면 CameraX 또한 Camera2 패키지 기반으로 동작하지만 간단한 구현방식으로 만들어 놓은 것(대신 세세한 기능 구현 x)이라고 생각하면 된다.
대부분의 개발자들이 단순히 사진과 동영상 캡처, 카메라 이미지 스트림 분석할 때 주로 사용하는 방법이다.
간단한 예시를 만들어보려고 했는데 안드로이드 공식 사이트에 있는 Codelab이 있기 때문에 이를 참조하여 실제로 한 번 사용해봤다.
Codelab은 현재 XML로 되어 있기 때문에 Compose로만 따로 변형해서 구현했다.
val camerax_version = "1.4.2"
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}")

class MainActivity : ComponentActivity() {
//권한 요청. 일단 권한 허용 안하면 종료하기로 설정
val permissions = registerForActivityResult(
contract = ActivityResultContracts.RequestMultiplePermissions()
) { permissions ->
if (REQUIRED_PERMISSIONS.any {
permissions[it] == false
}) {
Toast.makeText(
this,
"do not permission camera feature",
Toast.LENGTH_SHORT
).show()
finish()
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
setContent {
CameraXExTheme {
//권한이 허용되지 않으면 권한 요청하기
if (!allPermissionsGranted()) {
permissions.launch(REQUIRED_PERMISSIONS)
}
//카메라 화면
Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding ->
CameraScreen(
modifier = Modifier.padding(innerPadding),
takePicture = { takePhoto(it) }
)
}
}
}
}
//사진 찍기 함수
private fun takePhoto(imageCapture: ImageCapture) {
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")
}
}
// Create output options object which contains file + metadata
val outputOptions = ImageCapture.OutputFileOptions
.Builder(
contentResolver,
MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
contentValues
)
.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.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)
}
}
)
}
//카메라 관련 권한이 전부 허용이 됐는지 판별하는 함수
private fun allPermissionsGranted() = REQUIRED_PERMISSIONS.all {
ContextCompat.checkSelfPermission(
baseContext, it
) == PackageManager.PERMISSION_GRANTED
}
companion object {
//로그 기록 확인용
private const val TAG = "CameraXApp"
//파일 이름 설정
private const val FILENAME_FORMAT = "yyyy-MM-dd-HH-mm-ss-SSS"
//권한 리스트
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()
}
}
//카메라 화면
@Composable
fun CameraScreen(
modifier: Modifier = Modifier,
takePicture: (ImageCapture) -> Unit,
) {
val context = LocalContext.current
val lifecycle = LocalLifecycleOwner.current
val imageCapture = ImageCapture.Builder().build()
var currentZoomRatio by remember { mutableStateOf(1f) }
val scope = rememberCoroutineScope()
//카메라 컨트롤러 라이프사이클에 맞게 적용하기
val cameraController = remember {
LifecycleCameraController(context).apply {
bindToLifecycle(
lifecycle,
)
}
}
//카메라 화면을 보여주는 PreviewView
val previewView = remember {
PreviewView(context).apply {
scaleType = PreviewView.ScaleType.FILL_END
implementationMode = PreviewView.ImplementationMode.COMPATIBLE
controller = cameraController
}
}
//카메라 컨트롤과 정보들을 받는 곳
val cameraControl = remember { mutableStateOf<CameraControl?>(null) }
val cameraInfo = remember { mutableStateOf<CameraInfo?>(null) }
//렌즈 위치(전면, 후면)
var lensFacing by remember { mutableStateOf(CameraSelector.LENS_FACING_BACK) }
//카메라 선택
val cameraSelector = remember(lensFacing) {
CameraSelector.Builder().requireLensFacing(lensFacing).build()
}
var zoomJob by remember { mutableStateOf<Job?>(null) }
DisposableEffect(Unit) {
//카메라 줌 이벤트를 하면 카메라 줌 설정하기
val scaleGestureDetector = ScaleGestureDetector(context,
object : ScaleGestureDetector.SimpleOnScaleGestureListener() {
override fun onScale(detector: ScaleGestureDetector): Boolean {
val control = cameraControl.value ?: return false
val info = cameraInfo.value ?: return false
val scale = detector.scaleFactor
currentZoomRatio *= scale
val minZoom = info.zoomState.value?.minZoomRatio ?: 1f
val maxZoom = info.zoomState.value?.maxZoomRatio ?: 4f
val clamped = currentZoomRatio.coerceIn(minZoom, maxZoom)
control.setZoomRatio(clamped)
return true
}
})
//위에 구현한 이벤트를 아래에 등록하기
previewView.setOnTouchListener { view: View, event ->
scaleGestureDetector.onTouchEvent(event)
view.performClick()
}
//종료 시 이벤트 삭제
onDispose {
previewView.setOnTouchListener(null)
}
}
//카메라 위치가 바뀔 때마다 이벤트 등록하기
LaunchedEffect(cameraSelector) {
val cameraProviderFuture = ProcessCameraProvider.getInstance(context)
cameraProviderFuture.addListener({
// Used to bind the lifecycle of cameras to the lifecycle owner
val cameraProvider: ProcessCameraProvider = cameraProviderFuture.get()
// Preview
val preview = androidx.camera.core.Preview.Builder()
.build()
.also {
it.surfaceProvider = previewView.surfaceProvider
}
try {
// Unbind use cases before rebinding
cameraProvider.unbindAll()
// Bind use cases to camera
val camera = cameraProvider.bindToLifecycle(
lifecycleOwner = lifecycle, cameraSelector, preview, imageCapture
)
cameraControl.value = camera.cameraControl
cameraInfo.value = camera.cameraInfo
} catch (exc: Exception) {
Log.e("Chan", "Use case binding failed", exc)
}
}, ContextCompat.getMainExecutor(context))
}
//카메라가 나타나는 화면
Box(modifier = modifier.fillMaxSize()) {
AndroidView(
modifier = Modifier
.fillMaxSize(),
factory = { previewView },
onRelease = {
cameraController.unbind()
}
)
//전면, 후면 전환
IconButton(
modifier = Modifier
.align(Alignment.TopEnd)
.padding(16.dp),
onClick = {
lensFacing =
if (lensFacing == CameraSelector.LENS_FACING_BACK) CameraSelector.LENS_FACING_FRONT else CameraSelector.LENS_FACING_BACK
}
) {
Icon(
modifier = Modifier.size(24.dp),
imageVector = Icons.Filled.Refresh,
contentDescription = null,
tint = MaterialTheme.colorScheme.primary
)
}
//클릭 시 사진 저장하기
Box(
modifier = Modifier
.padding(bottom = 36.dp)
.size(75.dp)
.clip(CircleShape)
.background(MaterialTheme.colorScheme.primary.copy(alpha = 0.3f))
.align(Alignment.BottomCenter)
.clickable(onClick = { takePicture(imageCapture) }),
contentAlignment = Alignment.Center
) {
Box(
modifier = Modifier
.size(50.dp)
.clip(CircleShape)
.background(MaterialTheme.colorScheme.primary)
.padding(12.dp),
)
}
}
}
코드랩만 보고도 간단하게 사진을 찍는 기능을 구현하고 줌인 아웃기능 또한 사용할 수 있었다.
물론 세세한 기능들도 많이 있고 코드가 하나의 파일만으로 구현되어 있어서 정리가 제대로 되지 않았기 때문에 리팩토링하고 다른 기능들도 사용하면서 cameraX 라이브러리들을 조금씩 익혀봐야겠다.