일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | |||
5 | 6 | 7 | 8 | 9 | 10 | 11 |
12 | 13 | 14 | 15 | 16 | 17 | 18 |
19 | 20 | 21 | 22 | 23 | 24 | 25 |
26 | 27 | 28 | 29 | 30 | 31 |
- android ar 개발
- 안드로이드 라이선스
- 안드로이드 레트로핏 crud
- 플러터 설치 2022
- 자바 다형성
- 스택 자바 코드
- ANR이란
- 큐 자바 코드
- 서비스 vs 쓰레드
- rxjava cold observable
- Rxjava Observable
- 안드로이드 레트로핏 사용법
- 안드로이드 유닛 테스트 예시
- 멤버변수
- 스택 큐 차이
- android retrofit login
- rxjava hot observable
- 안드로이드 라이선스 종류
- jvm 작동 원리
- 안드로이드 유닛 테스트
- 클래스
- jvm이란
- 안드로이드 유닛테스트란
- 2022 플러터 설치
- 안드로이드 os 구조
- rxjava disposable
- 객체
- 서비스 쓰레드 차이
- ar vr 차이
- 2022 플러터 안드로이드 스튜디오
- Today
- Total
나만을 위한 블로그
[Android] CameraX 코드랩 뜯어보기 - 3 - 본문
https://onlyfor-me-blog.tistory.com/490
https://onlyfor-me-blog.tistory.com/714
1편에선 환경설정과 뼈대 코드를 작성했고 2편에선 후면 카메라 렌즈를 통해 핸드폰 화면에 미리보기를 띄우는 처리를 했다. 이번에는 사진을 찍는 코드를 작성한다.
아래 takePhoto() 코드를 프로젝트에 복붙한다.
private fun takePhoto() {
// Get a stable reference of the modifiable image capture use case
val imageCapture = imageCapture ?: return
// Create time stamped name and MediaStore entry.
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")
}
}
// 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)
}
}
)
}
먼저 ImageCapture의 참조를 얻는다. 이 때 null이면 takePhoto()를 종료한다. 안드로이드 스튜디오 Electric Eel 기준으로 return을 클릭하면 return을 담은 함수명에 표시가 될 것이다. 이것을 통해 return이 함수를 종료하는지, 람다나 코루틴을 사용할 경우 람다 또는 코루틴을 종료하는지 확인할 수 있다.
그리고 ContentValues의 인스턴스를 만든 뒤 MediaStore 값들을 설정한다. 이 때 표시되는 MediaStore 이름이 고유하도록 타임스탬프를 활용한 걸 볼 수 있다. 추가로 안드로이드 파이보다 상위 버전인지를 if로 체크해서 RELATIVE_PATH 값을 별도로 설정하는 코드가 보인다. 이것은 이미지를 저장할 상대 경로를 설정하는 코드다.
에뮬레이터 기준으로 take photo 버튼을 눌러 사진을 찍은 뒤 갤러리로 들어가면 "CameraX-Image"라는 폴더에 내가 찍은 사진이 들어가 있는 걸 볼 수 있다.
그럼 왜 안드로이드 파이부터인지 짚고 가자. 왜일까? 그 이유는 안드로이드 파이의 상위 버전인 안드로이드 10부터 도입된 Scoped Storage 기능 때문이다. 관련 디벨로퍼 링크는 아래에 첨부한다.
https://developer.android.com/about/versions/10/privacy/changes?hl=ko
https://developer.android.com/training/data-storage?hl=ko#scoped-storage
2번째 링크를 확인하면 이런 내용이 있다.
사용자에게 더 많은 권한을 제공하고 파일이 복잡해지지 않게 하기 위해 안드로이드 10(API 레벨 29) 이상을 타겟팅하는 앱에는 기본적으로 외부 저장소로 범위가 지정된 접근 권한 또는 범위 지정 저장소(scoped storage)가 부여된다. 이런 앱은 외부 저장소의 앱별 디렉토리와 앱에서 만든 특정 타입의 미디어에만 접근할 수 있다. 앱이 런타임에 저장소 관련 권한을 요청하는 경우 사용자가 보는 대화상자에는 범위 지정 저장소가 설정돼 있어도 앱이 외부 저장소에 대한 광범위한 접근을 요청한다고 표시된다
앱이 앱별 디렉토리 외부 및 MediaStore API가 접근할 수 있는 디렉토리 외부에 저장된 파일에 접근해야 하는 경우가 아니면 범위 지정 저장소를 사용하라. 앱별 파일을 외부 저장소에 저장하면 이런 파일을 외부 저장소의 앱별 디렉토리에 배치함으로써 범위 지정 저장소를 더 쉽게 채택할 수 있게 된다. 이렇게 하면 범위 지정 저장소가 사용 설정됐을 때 앱이 해당 파일에 대한 접근 권한을 유지한다.
scoped storage는 왜 도입됐을까? 안드로이드 파이가 사용될 때 안드로이드 개발자들은 권한 중 WRITE_EXTERNAL_STORAGE 권한만 얻으면 외부 저장소에 별도의 폴더를 만들어서 거기에 파일을 다운로드하거나 만들 수 있었고, 또 어지간해선 사용자가 안 찾아볼 위치에 온점으로 시작하는 폴더, 파일 등을 만들 수 있었다. 즉 사용자 프라이버시가 전혀 지켜지지 않았다는 뜻이다. 그래서인지 핸드폰을 쓰다가 폴더 목록을 확인해보니 뭐에 쓰는지 모르겠는 폴더들이 즐비했던 걸 본 기억이 있다.
또한 안드로이드의 저장소는 내부 / 외부 저장소로 크게 2개가 존재한다. 내부 저장소는 각 앱이 혼자 사용할 수 있는 공간이 격리된 상태로 제공되었고(샌드박스), 외부 저장소는 모든 앱이 위에서 말한 WRITE_EXTERNAL_STORAGE 권한만 얻으면 자유롭게 사용할 수 있었다. 이것이 안드로이드 10부터는 외부 저장소도 앱별로 격리되게 되었다. 그래서 사진, 음악, 파일, 다운로드 등의 형태로 폴더가 분리되었고 지금도 이 구조를 유지하고 있다.
만약 A 앱에서 만들어진 파일을 B 앱에서 접근해 사용하려면 추가 절차가 필요하고 앱은 사진, 다운로드 등 공용 폴더에 접근할 순 있지만 권한을 요청해서 얻어야 읽고 쓸 수 있다. 이 때 추가 절차 중 하나가 위 코드의 MediaStore API를 사용하는 것이다. 이것으로 사용자의 프라이버시를 보호할 수 있게 된 것이다.
본론으로 돌아와서, 이후 OutputFileOptions 객체를 만든다. 이것은 카메라로 캡쳐한 이미지 또는 동영상을 파일로 저장할 때 이름, 경로, 파일 형식, 저장 옵션 등을 설정할 수 있게 해주는 클래스다.
이후 앞서 만든 imageCapture 객체에서 takePicture()를 호출하면서 outputOptions 등의 매개변수를 넘긴다. 그리고 OnImageSavedCallback 인터페이스를 구현해서 onError, onImageSaved 콜백을 구현한다.
이제 startCamera()의 코드를 수정한다.
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
val preview = Preview.Builder()
.build()
.also {
it.setSurfaceProvider(viewFinder.surfaceProvider)
}
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.e(TAG, "Use case binding failed", exc)
}
}, ContextCompat.getMainExecutor(this))
}
여기까지 완료됐다면 에뮬레이터 기준 사진을 찍고 갤러리로 들어가면 아래와 같이 표시된다.
CameraX-Image라는 폴더에 찍힌 사진이 들어있다. 이 사진을 클릭해서 세부정보를 확인하면 아래와 같다.
코드에서 지정한 경로에 타임스탬프 형태의 이름으로 사진이 저장된 걸 확인할 수 있다.
'Android' 카테고리의 다른 글
[Android] 페이징 라이브러리, Hilt, LiveData로 Github API 사용하기 (0) | 2023.04.09 |
---|---|
[Android] withContext란? (0) | 2023.04.08 |
[Android] CameraX 코드랩 뜯어보기 - 2 - (0) | 2023.03.26 |
[Android] dataUrl이란? 웹뷰로 dataUrl 전송하는 법 (0) | 2023.03.25 |
[Android] Coroutine + Retrofit + Hilt + LiveData를 써서 네트워크 상태 별 처리하기 & 리사이클러뷰 페이징 (0) | 2023.03.21 |