본문 바로가기
Android

[Doit 깡샘의 안드로이드 앱 프로그래밍 with 코틀린] 정리 16 - 갤러리 앱 연동하기

by 들풀민들레 2022. 2. 28.
본 글은 [Doit 깡샘의 안드로이드 앱 프로그래밍 with 코틀린 - 이지스퍼블리싱 (2022)] 의 내용을 발췌한 것입니다.

좀더 자세한 내용은 책 혹은 인강을 통해 확인해 주세요.

 

 

 

 

 

갤러리 앱을 연동해 사진 이미지를 가져오는 방법도 살펴보겠습니다. 갤러리 앱 연동은 인텐트로 갤러리 앱의 목록 화면을 띄우거나 갤러리 앱의 콘텐츠 프로바이더로 데이터를 가져오는 작업입니다.

 

이미지 작업 시 고려 사항


먼저 안드로이드 앱에서 이미지 데이터를 이용할 때 고려해야 할 사항은 다음과 같습니다.


• 안드로이드에서 이미지는 Drawable이나 Bitmap 객체로 표현합니다.
• Bitmap 객체는 BitmapFactory로 생성합니다.
• BitmapFactory로 이미지를 생성할 때는 OOM 오류를 고려해야 합니다.
• Glide나 Picasso 같은 이미지 처리 라이브러리를 이용하는 것이 효율적일 수 있습니다.

 

안드로이드에서 이미지는 Drawable이나 Bitmap 객체로 표현합니다. Drawable은 주로 리소스 이미지를 표현할 때, Bitmap은 파일에서 읽은 이미지나 네트워크에서 내려받은 이미지를 표현할 때 사용합니다. Bitmap과 Drawable은 서로 호환하므로 Drawable 타입의 이미지를 Bitmap 타입으로, 또는 그 반대로 바꿀 수 있습니다.
Bitmap 이미지는 BitmapFactory 클래스의 ‘decode’로 시작하는 다음과 같은 함수로 생성합니다.

 

• BitmapFactory.decodeByteArray(): byte[] 배열의 데이터로 비트맵 생성
• BitmapFactory.decodeFile(): 파일 경로를 매개변수로 지정하면 그 파일에서 데이터를 읽을 수 있는 FileInputStream을 만들어 decodeStream() 함수 이용
• BitmapFactory.decodeResource(): 리소스 이미지로 비트맵 생성
• BitmapFactory.decodeStream(): InputStream으로 읽은 데이터로 비트맵 생성

 

BitmapFactory를 이용하면 비트맵을 만들 때 아이콘처럼 작은 이미지를 불러오는 데는 문제가 없지만 카메라로 찍은 사진이나 서버에서 내려받은 이미지처럼 크기가 큰 이미지를 불러올 때는 OOMout of memory 오류가 발생할 수 있습니다. OOM이란 앱의 메모리가 부족해서 발생하는 오류입니다. 이 오류는 주로 용량이 큰 이미지를 불러올 때 발생하므로 이미지의 크기를 줄이면 해결됩니다.

 

val bitmap = BitmapFactory.decodeStream(inputStream)

이처럼 decodeStream() 함수로 Bitmap 객체를 만들 때 매개변수에 옵션을 지정하지 않으면 원본 데이터 그대로 불러오므로 OOM 오류가 발생할 수 있습니다. 따라서 다음처럼 옵션을 적용합니다. 이미지 크기를 줄일 때는 BitmapFactory.Option 객체의 inSampleSize 속성을 이용합니다.

 

val option = BitmapFactory.Options()
option.inSampleSize = 4
val bitmap = BitmapFactory.decodeStream(inputStream, null, option)

Option 객체의 inSampleSize에 값을 적용하면 이 값만큼의 비율로 데이터를 줄여서 불러옵니다. 예를 들어 inSampleSize값을 4로 지정하면 해상도가 2048×1536(약 12MB)인 이미지를 약 512×384(약 0.75MB)의 비트맵으로 생성해 줍니다.

 

갤러리 앱 연동 방법


이제 갤러리 앱을 연동하는 방법을 살펴보겠습니다. 인텐트로 갤러리 앱의 사진 목록을 띄우고 사용자가 선택한 사진을 읽어 화면에 출력하는 방법입니다.

 

먼저 인텐트로 갤러리 앱의 사진 목록을 출력하는 코드를 다음과 같이 작성합니다. 인텐트의 액션 문자열은 Intent.ACTION_PICK으로, 데이터는 MediaStore.Images.Media.EXTERNAL_CONTENT_URI로, 그리고 타입을 image/*로 지정한 인텐트를 전달하면 갤러리 앱의 목록 화면이 실행됩니다.

 

val intent = Intent(Intent.ACTION_PICK, MediaStore.Images.Media.EXTERNAL_CONTENT_URI)
intent.type = "image/*"
requestGalleryLauncher.launch(intent)

다음으로 갤러리 앱의 사진 목록에서 사용자가 선택한 사진을 액티비티 화면에 출력할 차례입니다. 이때 앞에서 살펴본 OOM 문제가 발생할 수도 있으므로 BitmapFactory.Option 객체의 inSampleSize값을 지정해 이미지 크기를 줄여서 불러와야 합니다. 그런데 inSample Size값을 임의의 숫자로 지정할 수도 있지만 원본 데이터의 크기와 화면에 출력되는 이미지의 크기를 비교해서 적절한 비율로 지정하면 더 효율적입니다.
다음 코드에 작성한 함수는 inSampleSize에 지정할 값을 계산하는 예를 보여 줍니다. 이 함수를 호출하면서 사진의 Uri값과 화면에 이미지가 출력되는 크기를 매개변수로 지정하면, 해당 이미지 정보를 얻어 실제 화면에 출력되는 크기와 비교해서 inSampleSize값을 계산합니다.

 

private fun calculateInSampleSize(fileUri: Uri, reqWidth: Int, reqHeight: Int): Int {
    val options = BitmapFactory.Options()
    options.inJustDecodeBounds = true
    try {
        var inputStream = contentResolver.openInputStream(fileUri)
        BitmapFactory.decodeStream(inputStream, null, options)
        inputStream!!.close()
        inputStream = null
    } catch (e: Exception) {
    	e.printStackTrace()
    }
    val (height: Int, width: Int) = options.run { outHeight to outWidth }
    var inSampleSize = 1
    // inSampleSize 비율 계산
    if (height > reqHeight || width > reqWidth) {
        val halfHeight: Int = height / 2
        val halfWidth: Int = width / 2
        while (halfHeight / inSampleSize >= reqHeight &&
            halfWidth / inSampleSize >= reqWidth) {
            inSampleSize *= 2
    	}
    }
    return inSampleSize
}

위 코드에서는 decodeStream() 함수로 이미지를 메모리에 불러오는 것처럼 보이지만, 세 번째 매개변수에 지정한 Option 객체의 inJustDecodeBounds값을 true로 설정했으므로 실제로 Bitmap 객체가 만들어지지 않습니다. 그 대신 읽은 이미지의 각종 정보가 option 객체에 설정됩니다.
이제 갤러리 앱의 목록에서 사용자가 사진을 하나 선택해서 되돌아왔을 때 위의 calculateIn SampleSize() 함수를 이용해 이미지를 불러오는 코드를 살펴보겠습니다.

 

requestGalleryLauncher = registerForActivityResult(
ActivityResultContracts.StartActivityForResult())
{
    try {
        // inSampleSize 비율 계산, 지정
        val calRatio = calculateInSampleSize(it!!.data!!.data!!,
            resources.getDimensionPixelSize(R.dimen.imgSize),
            resources.getDimensionPixelSize(R.dimen.imgSize))
        val option = BitmapFactory.Options()
        option.inSampleSize=calRatio
        // 이미지 로딩
        var inputStream = contentResolver.openInputStream(it!!.data!!.data!!)
        val bitmap = BitmapFactory.decodeStream(inputStream, null, option)
        inputStream!!.close()
        inputStream = null
        bitmap?. let {
        	binding.galleryResult.setImageBitmap(bitmap)
        } ?: let {
        	Log.d("kkang", "bitmap null")
        }
    } catch (e: Exception) {
    	e.printStackTrace()
    }
}

resources.getDimensionPixelSize(R.dimen.imgSize) 구문은 출력할 이미지의 화면 크기를 나타내며 dimen 리소스값을 얻어 지정했습니다. 임의의 숫자를 직접 지정해도 됩니다. 그리고 contentResolver.openInputStream(data!!.data!!) 구문은 갤러리 앱의 콘텐츠 프로바이더가 제공하는 InputStream 객체를 가져옵니다. 이 객체에는 사용자가 갤러리 앱에서 선택한 사진 데이터가 담겨 있습니다.