먼저 코드를 보고 하나하나 설명하겠다.
fun isExternalStorageWritable(): Boolean {
return Environment.getExternalStorageState() == Environment.MEDIA_MOUNTED
}
private fun dispatchTakePictureIntent() {
if(!MyFileHelper.isExternalStorageWritable())
Toast.makeText(this, "저장공간이 부족해서 카메라앱을 실행시킬 수 없습니다.", Toast.LENGTH_SHORT).show()
Intent(MediaStore.ACTION_IMAGE_CAPTURE).also { takePictureIntent ->
// Ensure that there's a camera activity to handle the intent
takePictureIntent.resolveActivity(packageManager)?.also {
// Create the File where the photo should go
val photoFile: File? = try {
MyFileHelper.createImageTempFile(this).apply {
mCurrentPhotoPath = absolutePath
}
} catch (ex: IOException) {
// Error occurred while creating the File
null
}
// Continue only if the File was successfully created
photoFile?.also { file ->
val photoURI: Uri = FileProvider.getUriForFile(
this,
"com.example.firstapp.fileprovider",
file
)
takePictureIntent.putExtra(MediaStore.EXTRA_OUTPUT, photoURI)
startActivityForResult(takePictureIntent, REQUEST_IMAGE_CAPTURE)
}
}
}
}
카메라로 찍은 사진은 임시파일의 형태로라도 기기에 저장되어야한다. 왜냐하면 카메라앱을 실행시키는 MediaStore.ACTION_IMAGE_CAPTURE 인텐트는 액티비티의 결과인 원본사진을 인텐트로 전달할 수 없기 때문이다. 다시말해서 원본사진을 onActivityResult의 data로 전달할 수 없기 때문이다.
왜냐?
단순하게 원본사진의 크기가 너무 커서 그렇다. 인텐트는 KB 단위의 데이터를 전송하는 목적으로 설계되었다. 그 이상을 담으려고 하면 에러가 떠버린다.
따라서 MediaStore.ACTION_IMAGE_CAPTURE를 통해 찍은 원본사진을 파일로 저장하고, onActivityResult 에서는 저장한 사진을 FIle I/O를 통해 접근해서 불러오는 식으로 처리해야한다.
이때 참고할 점으로, 사진 같은 미디어파일은 외부저장소의 개별저장소에 저장된다. 아마 크기가 커서 그런것 같다.
(만약 미리보기 아이콘정도로 쓸 저화질의 사진도 괜찮다면 onActivityResult에 data로 넘어오는 비트맵을 이용해도된다. 그부분은 공식문서를 참고하시길 바란다.)
그럼 코드를 보자.
if(!MyFileHelper.isExternalStorageWritable())
Toast.makeText(this, "저장공간이 부족해서 카메라앱을 실행시킬 수 없습니다.", Toast.LENGTH_SHORT).show()
이 부분은 이미지파일을 외부저장소에 쓰기 전에 저장공간을 체크하는 코드이다.
Intent(MediaStore.ACTION_IMAGE_CAPTURE).also { takePictureIntent ->
// Ensure that there's a camera activity to handle the intent
takePictureIntent.resolveActivity(packageManager)?.also {
//Do Something
}
카메라 기능이 있는 앱을 찾아서 실행시킨다.
만약, 그런 앱이 없는데 startActivity()를 호출하면 앱이 정지된다.
따라서 resolveActivity로 그 결과가 null인지 아닌지에 따라서 startActivity를 수행한다.
@Throws(IOException::class)
fun createImageTempFile(context: Context): File {
// Create an image file name
val timeStamp: String = SimpleDateFormat("yyyyMMdd_HHmmss").format(Date())
val storageDir: File? = context.getExternalFilesDir(Environment.DIRECTORY_PICTURES)
return File.createTempFile(
"JPEG_${timeStamp}_", /* prefix */
".jpg", /* suffix */
storageDir /* directory */
).apply {
//jvm이 내려갈때 임시파일을 지운다.
deleteOnExit()
}
}
val photoFile: File? = try {
MyFileHelper.createImageTempFile(this).apply {
mCurrentPhotoPath = absolutePath
}
} catch (ex: IOException) {
// Error occurred while creating the File
null
}
외부저장소 중 파일을 저장하는 디렉토리를 가진, 고유한 이름의 임시파일을 만든다. 나중에 onActivityResult에서 이 파일을 읽을때 사용하기 위해 그 파일의 절대경로를 멤버변수로 담아둔다.
photoFile?.also { file ->
val photoURI: Uri = FileProvider.getUriForFile(
this,
"com.example.firstapp.fileprovider",
file
)
takePictureIntent.putExtra(MediaStore.EXTRA_OUTPUT, photoURI)
startActivityForResult(takePictureIntent, REQUEST_IMAGE_CAPTURE)
MediaStore.ACTION_IMAGE_CAPTURE가 찍은 사진을 외부저장소에 저장할때 MediaStore API를 이용하기 때문에.. 즉, content 스키마를 사용하기 때문에 file의 경로를 content 스키마로 변경한뒤 인텐트에 첨부한다.
file.absolutePath 가 storage/emulated/0/Android/data/com.example.firstapp/files/Pictures/JPEG_20201022_125001_910373070437525236.jpg 일때 photoURI 는content://com.example.firstapp.fileprovider/my_images/Pictures/JPEG_20201022_125001_910373070437525236.jpg 이렇게 된다.
여기서 my_images는 컨텐츠프로바이더에서 정의된 리소스의 name이다.
//매니페스트파일의 <application> 밑에 정의한다.
<provider
android:name="androidx.core.content.FileProvider"
android:authorities="com.manta.firstapp.fileprovider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/file_paths" />
</provider>
<paths xmlns:android="http://schemas.android.com/apk/res/android">
<!-- <external-files-path name="my_images" path="Android/data/com.example.firstapp/files/Pictures" />-->
<external-files-path name="my_images" path="." />
</paths>
file_paths.xml 의 external-files-path 의 path가 "." 인 이유는 솔직히 나도 잘 모르겠다. 공식문서를 따라하다가 안되서 구글링한 끝에 이렇게 하니까 됬다. 이 코드를 작성한 사람의 설명으로는 "." 로 해도 알아서 경로가 만들어지기 때문에 괜찮다는 식이였던거 같은데.. 그럴거면 콘텐츠 프로바이더를 정의할 필요가 뭐가 있는지 싶기도 하고 애초에 뭐하는 코드인지 잘 모르겠다.
콘텐츠 프로바이더에 대해서는 공부한뒤 나중에 포스팅 하겠다.
아무튼 이렇게 카메라를 실행시키고, 끝나면 onActivityResult 가 호출된다.
if (requestCode == REQUEST_IMAGE_CAPTURE) {
val f = File(mCurrentPhotoPath) // /storage/emulated/0/Android/data/com.example.firstapp/files/Pictures/JPEG_20201021_184543_5177963341725398557.jpg
//Do something
}
그러면 아까 저장한 파일의 경로로 파일객체를 만들고 파일을 불러와서 파일을 저장하든 뭘하든 마음대로 하면 된다. 참고로 필자는
if (requestCode == REQUEST_IMAGE_CAPTURE) {
val f = File(mCurrentPhotoPath) // /storage/emulated/0/Android/data/com.example.firstapp/files/Pictures/JPEG_20201021_184543_5177963341725398557.jpg
val uri = Uri.fromFile(f) // file:///storage/emulated/0/Android/data/com.example.firstapp/files/Pictures/JPEG_20201021_184543_5177963341725398557.jpg
launchImageCrop(uri)
}
이런식으로 해서 파일에서 uri를 얻어서 이미지크롭을 수행했다.
만약 파일에서 비트맵을 뽑아내고 싶다면
private fun getBitmapFromUri(uri: Uri): Bitmap? {
val bitmap = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
val source = ImageDecoder.createSource(contentResolver, uri)
ImageDecoder.decodeBitmap(source);
} else {
contentResolver.openInputStream(uri)?.use { inputStream ->
BitmapFactory.decodeStream(inputStream)
}
}
return bitmap
}
이런식으로 할 수 있다.
'안드로이드 앱개발' 카테고리의 다른 글
Architecture Components(MVVM 패턴) (0) | 2020.11.20 |
---|---|
안드로이드 서비스 (0) | 2020.11.20 |
파일입출력2 : 저장소 (Android Studio Internal/External Storage) (0) | 2020.10.27 |
파일입출력 1 : 쓰고 읽기 (File I/O in Kotlin) (0) | 2020.10.27 |
AdMob nativeAd 를 RecyclerView를 활용해 표시하기 (nativeAd with RecyclerView) (0) | 2020.10.27 |