카메라 앱을 실행하여 사진을 촬영하면 해당 사진이 외부 파일 앱에 저장이 되고 파일 앱을 여는 버튼을 누르면 해당 사진을 가져와서 이미지를 다이얼로그로 미리보기를 표시하는 것을 작업 하였고 해당 부분을 샘플로 해서 작성하였습니다.
카메라 및 저장소를 사용하기 위한 것
<uses-feature android:name="android.hardware.camera.any" />
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"
android:maxSdkVersion="32" />
<uses-permission
android:name="android.permission.WRITE_EXTERNAL_STORAGE"
android:maxSdkVersion="28" />
android.hardware.camera.any
: 카메라 하드웨어(전면, 후면 등)를 사용할 수 있음을 나타냄android.permission.CAMERA
: 카메라에 접근하기 위한 권한을 요청android.permission.READ_EXTERNAL_STORAGE
:android.permission.WRITE_EXTERNAL_STORAGE
:카메라 사용 권한 및 저장소에 이미지를 선택하고 받기 위해 선언
private lateinit var cameraLauncher: ActivityResultLauncher<Intent>
private lateinit var permissionLauncher: ActivityResultLauncher<String>
private lateinit var storageLauncher: ActivityResultLauncher<Intent>
private lateinit var storageLauncher2: ActivityResultLauncher<Intent>
private var currentPhotoUri: Uri? = null
private var currentPhotoUri2: Uri? = null
private var currentCameraState: CameraState = CameraState.CAMERA1
private var isCameraAppClosed = false
cameraLauncher
: 카메라 앱을 시작하고 결과를 받기 위한 변수
permissionLauncher
: 카메라 사용 권한을 요청하고 결과를 받기 위한 변수
storageLauncher
: 파일 저장소에서 이미지를 선택하고 결과를 받기 위한 변수
currentPhotoUri
: 현재 선택된 또는 캡처된 사진의 URI를 저장
isCameraAppClosed
: 카메라 앱이 종료되었는지 여부를 추적하는 변수
currentCameraState
: 현재 사용 중인 카메라 상태를 추적
선언한 변수에는 ActivityResultLauncher
를 사용하여 설정된 카메라 앱과 파일 저장소 앱으로부터 사용자 상호작용의 결과를 처리하기 위해 로직을 설정
private fun cameraResult() {
cameraLauncher =
registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
if (result.resultCode == Activity.RESULT_OK) {
isCameraAppClosed = false
when (currentCameraState) {
CameraState.CAMERA1 -> {
currentPhotoUri?.let { uri ->
handleButtonUpload(
binding.buttonFileUploadEnd,
binding.buttonFileUploadDelete,
uri
)
isCameraAppClosed = true
}
}
CameraState.CAMERA2 -> {
currentPhotoUri2?.let {uri ->
handleButtonUpload(
binding.buttonFileUploadEnd2,
binding.buttonFileUploadDelete2,
uri
)
isCameraAppClosed = true
}
}
}
} else {
isCameraAppClosed = true
}
}
storageLauncher = registerLauncher {
handleStorageResult(
it,
binding.buttonFileUploadEnd,
binding.buttonFileUploadDelete,
binding.error
) { uri -> currentPhotoUri = uri }
}
storageLauncher2 = registerLauncher {
handleStorageResult(
it,
binding.buttonFileUploadEnd2,
binding.buttonFileUploadDelete2,
binding.error2
) { uri -> currentPhotoUri2 = uri }
}
permissionLauncher =
registerForActivityResult(ActivityResultContracts.RequestPermission()) { isGranted ->
if (isGranted) {
launchCamera { uri -> currentPhotoUri = uri }
}
}
}
Activity.RESULT_OK
일 때, 즉 사용자가 성공적으로 사진을 찍었을 경우:isCameraAppClosed
변수를 false로 설정 (카메라 앱이 아직 닫히지 않았음을 나타냄)currentCameraState
에 따라 currentPhotoUri
또는 currentPhotoUri2
를 사용하여 handleButtonUpload 함수를 호출, 이 함수는 UI에 촬영된 사진을 표시하고 관련 버튼을 설정isCameraAppClosed
를 true로 설정하여 카메라 앱이 닫혔음을 나타냄Activity.RESULT_OK
가 아닌 경우에도 isCameraAppClosed
를 true
로 설정storageLauncher
와 storageLauncher2
에 할당currentPhotoUri
또는 currentPhotoUri2
에 저장currentPhotoUri
에 할당합니다.카메라 권한을 요청하는 과정을 처리
private fun requestCameraPermission(callback: () -> Unit) {
val permission = Manifest.permission.CAMERA
when {
ContextCompat.checkSelfPermission(
requireContext(),
permission
) == PackageManager.PERMISSION_GRANTED -> {
callback()
}
// 카메라 권한 거절로 되어 있을 시
shouldShowRequestPermissionRationale(permission) -> {
AlertDialog.Builder(requireContext())
.setMessage("카메라 권한이 필요합니다.")
.setPositiveButton("확인") { _, _ ->
permissionLauncher.launch(permission)
}
.setNegativeButton("취소", null)
.show()
}
else -> {
permissionLauncher.launch(permission)
}
}
}
Manifest.permission.CAMERA
를 사용하여 카메라 권한을 나타내는 문자열을 permission
변수에 할당(PackageManager.PERMISSION_GRANTED)
callback 함수를 호출하고 콜백 함수는 권한이 있을 때 실행할 작업을 정의확인
버튼을 누를 경우 권한 요청을 다시 시도하는 로직(permissionLauncher.launch(permission))
과 취소
버튼이 포함permissionLauncher
를 사용하여 카메라 권한을 요청카메라를 사용하여 사진을 찍고, 그 사진을 애플리케이션에서 접근할 수 있는 위치에 저장하는 과정을 단순화 함
private fun launchCamera(updateUri: (Uri) -> Unit) {
val contentValues = ContentValues().apply {
put(MediaStore.Images.Media.DISPLAY_NAME,"JPEG_${System.currentTimeMillis()}_")
put(MediaStore.Images.Media.MIME_TYPE,"image/jpeg") }
val uri = requireContext().contentResolver.insert(
MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
contentValues
)
val cameraIntent =Intent(MediaStore.ACTION_IMAGE_CAPTURE)
uri?.let {
cameraIntent.putExtra(MediaStore.EXTRA_OUTPUT,uri)
cameraLauncher.launch(cameraIntent)
updateUri(uri)
}
}
ContentValues
객체를 생성하고 이를 사용하여 새로 촬영될 사진의 데이터를 정의MediaStore.Images.Media.DISPLAY_NAME
에는 파일 이름으로 현재 시간을 기반으로 한 문자열을 지정MediaStore.Images.Media.MIME_TYPE
에는 파일의 MIME 타입으로 "image/jpeg"을 지정(MediaStore.ACTION_IMAGE_CAPTURE)
cameraIntent.putExtra(MediaStore.EXTRA_OUTPUT, uri)
카메라 앱이 촬영한 사진을 해당 URI에 저장하도록 지시cameraLauncher.launch(cameraIntent)
를 호출하여 카메라 앱을 실행updateUri
콜백 함수를 호출하여 새로 생성된 사진의 URI를 전달하고 호출하는 측에서 URI를 처리하는 데 사용이 함수는 파일 저장소에서 파일을 선택하거나, 다른 유사한 액티비티로부터 결과를 받을 때 사용
private fun registerLauncher(callback: (Uri?) -> Unit) =
registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
if (result.resultCode == Activity.RESULT_OK) {
val uri = result.data?.data
callback(uri)
}
}
callback
이라는 이름의 람다 함수로, Uri?
타입의 인자를 받음파일의 Uri와 관련된 여러 UI 컴포넌트 (Button과 TextView)의 상태를 처리하며 또한 파일의 확장자와 크기를 검사하여 에러 메시지를 표시 함
private fun handleStorageResult(
uri: Uri?,
buttonEnd: Button,
buttonDelete: Button,
errorView: TextView,
updateUri: (Uri) -> Unit
) {
uri?.let {
val extension = getFileName(it).substringAfterLast('.', "").lowercase(Locale.ROOT)
val fileSize =
requireContext().contentResolver.openFileDescriptor(it, "r")?.statSize ?: 0
when {
!listOf("jpg", "jpeg").contains(extension) -> {
errorView.visibility = View.VISIBLE
errorView.text = "형식이 틀립니다"
return
}
// 크기 -> byte
fileSize > 300000 -> {
errorView.visibility = View.VISIBLE
errorView.text = "파일 크기가 초과 했습니다"
return
}
else -> {
errorView.visibility = View.GONE
buttonEnd.text = getFileName(it)
buttonEnd.visibility = View.VISIBLE
buttonDelete.visibility = View.VISIBLE
updateUri(it)
}
}
}
}
uri
: 선택된 파일의 URIbuttonEnd
: 파일이 유효하면 보여질 버튼buttonDelete
: 파일을 삭제하는 버튼errorView
: 오류 메시지를 표시하는 텍스트 뷰updateUri
: 파일의 URI를 업데이트하는 콜백 함수uri
가 null
이 아닌 경우에만 작업을 수행'.'
이후의 문자열을 추출하는 방식바이트
단위로 얻음"jpg"
또는 "jpeg"
가 아니면, 오류 메시지를 표시하고 함수를 종료buttonEnd
에 파일 이름을 표시하며 buttonEnd
와 buttonDelete
버튼을 보이게 함 그리고 updateUri 함수를 호출하여 선택된 파일의 URI를 업데이트주어진 Uri를 사용하여 파일 이름을 가져오는 기능
private fun getFileName(uri: Uri): String {
var name = ""
val returnCursor = requireContext().contentResolver.query(uri, null, null, null, null)
returnCursor?.let { cursor ->
val nameIndex = cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME)
cursor.moveToFirst()
name = cursor.getString(nameIndex)
cursor.close()
}
return name
}
uri
를 받아 해당 URI에서 파일의 이름을 추출하여 문자열로 반환Cursor
객체(returnCursor)를 사용하여, 실제 파일 이름을 찾음OpenableColumns.DISPLAY_NAME
(파일의 표시 이름)에 해당하는 컬럼의 인덱스를 찾음 cursor.close()
를 호출하여 리소스를 해제 name
변수에 저장한 후, 이를 반환카메라를 사용하여 사진을 찍거나 파일을 선택한 후에 해당 사진을 표시하고 필요한 추가 작업(미리보기)을 수행할 수 있도록 UI를 관리
private fun handleButtonUpload(buttonEnd: Button, buttonDelete: Button, uri: Uri?) {
uri?.let {
if (isCameraAppClosed) { // 카메라 앱이 종료되지 않았을 때만 미리보기 표시
showDialogImage(it)
}
// 데이터 베이스 저장 별도 처리 필요
}
buttonEnd.text = uri?.let { getFileName(it) }
buttonEnd.visibility = View.VISIBLE
buttonDelete.visibility = View.VISIBLE
}
uri
가 null
이 아닌 경우에만 작업을 수행isCameraAppClosed
변수를 확인하여, 카메라 앱이 이미 종료된 경우에만 showDialogImage 함수를 호출하여 이미지의 미리보기를 표시buttonEnd
에 파일의 이름을 표시하며 이는 getFileName 함수를 사용하여 파일 이름을 가져옴buttonEnd
와 buttonDelete
버튼을 보이게 설정하고 이를 통해 선택된 파일을 확인하고 필요한 경우 삭제할 수 있음 현재 안드로이드에서는 기존의 카메라 라이브러리는 지원이 중단되어 카메라2 또는 카메라X를 추천을 하고 있다.
그래서 접근 방식 및 처리 방법도 변경이 되어 최신 안드로이드 버전에서 앱을 개발할 때는 이러한 변화를 고려해야 함
지금은 기존의 카메라 라이브러리로 하였지만 카메라X도 샘플로 만들어서 정리를 할려고 한다.
깃허브 : https://github.com/GEUN-TAE-KIM/CameraApiAndStorageSave_Sample.git