본문 바로가기

안드로이드

안드로이드 카메라, 갤러리 연동

Android 앱에서 카메라로 사진을 찍어 가져오거나, 갤러리에 저장된 사진을 가져오는 방법을 소개하려 한다.

 

먼저 카메라 사용을 위해서는 권한 획득이 필요하다.

필요권한은 아래와 같다.

 

android.permission.CAMERA
android.permission.WRITE_EXTERNAL_STORAGE
android.permission.READ_EXTERNAL_STORAGE

위의 권한을 AndroidManifest.xml에 추가해준 후, 카메라 권한 및 외부 저장소 권한의 경우 마시멜로 이후 (API 23, Android 6.0) 사용자에게 직접 권한을 요청해야 한다.

 

관련 내용은 아래의 Android Developer 에서 자세하게 확인할 수 있다.

 

https://developer.android.com/training/permissions/requesting?hl=ko

 

앱 권한 요청  |  Android 개발자  |  Android Developers

모든 Android 앱은 액세스가 제한된 샌드박스에서 실행됩니다. 자체 샌드박스 밖에 있는 리소스나 정보를 앱이 사용해야 하는 경우에는 앱이 적절한 권한을 요청해야 합니다. 앱에 권한이 필요하다고 선언하려면 권한을 앱 manifest에 표시한 후 사용자가 런타임에 각 권한을 승인하도록 요청합니다(Android 6.0 이상). 이 페이지에서는 Android 지원 라이브러리를 사용하여 권한을 확인하고 요청하는 방법을 설명합니다. Android 프레임워크는 A

developer.android.com

권한 획득을 완료하였으면 이제 Intent를 사용해 외부 앱과 연동하는 것이 필요하다.

연동해야 할 앱이 카메라와 갤러리 이므로 해당되는 각각의 Intent를 선언 후 선택할 수 있게 사용자에게 제공해주면 된다. 아래는 예시 코드이다.

 

    companion object {
        private const val IMG_MIME_TYPE = "image/*"
    }
    
    private var mCameraPhotoPath: Uri? = null
    
    ...
    
    private fun imageChooser(requestCode: Int) {
        val pictureIntent = Intent(MediaStore.ACTION_IMAGE_CAPTURE).apply {
            resolveActivity(packageManager)?.run {
                createImageFile()?.let {
                    if (fileUri != null) {
                        putExtra(MediaStore.EXTRA_OUTPUT, fileUri)
                    }
                }
            }
        }

        val selectionIntent = Intent(Intent.ACTION_GET_CONTENT).apply {
            addCategory(Intent.CATEGORY_OPENABLE)
            type = IMG_MIME_TYPE
            putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true)
        }
        val chooserIntent = Intent(Intent.ACTION_CHOOSER).apply {
            putExtra(Intent.EXTRA_INTENT, selectionIntent)
            putExtra(Intent.EXTRA_TITLE, "Choose action type")
            putExtra(Intent.EXTRA_INITIAL_INTENTS, arrayOf(pictureIntent))
        }
        startActivityForResult(chooserIntent, requestCode)
    }

    private fun createImageFile(): Uri? {
        val contentValues = ContentValues().apply {
            put(MediaStore.MediaColumns.MIME_TYPE, IMG_MIME_TYPE)
        }
        val resolver = applicationContext.contentResolver
        return resolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, contentValues)
            .also { uri ->
                mCameraPhotoPath = uri
            }
    }

fun imageChooser(requestCode: Int)

카메라앱으로 찍은 사진 혹은 갤러리에서 사진을 가져오는 Intent를 구성 메서드

Intent.EXTRA_ALLOW_MULTIPLE : 갤러리에서 사진 여러개 선택하여 처리할 수 있도록 허용여부 설정 가능, Boolean 값으로 설정, 기본값은 false

 

fun createImageFile(): Uri?

카메라앱 연동시 촬영한 사진 uri 정보를 가져오는 메서드

카메라앱 같은 경우, 기본 카메라앱은 촬영한 임시 이미지를 저장할 위치를 지정해줘야 한다. 이때 ContentResolver를 활용하여 저장될 경로를 지정해 주며, MeadiaStore.EXTRA_OUTPUT의 경우 onActivityResult()에서 data가 null로 들어오기 때문에 별도의 맴버 변수를 선언해 해당 경로를 저장한 후 처리해야 한다.

아래는 MediaStore.EXTRA_OUTPUT에 대한 참고 링크이다.

 

https://stackoverflow.com/questions/16348757/mediastore-extra-output-renders-data-null-other-way-to-save-photos

 

MediaStore.EXTRA_OUTPUT renders data null, other way to save photos?

Google offers this versatile code for taking photos via an intent: @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.m...

stackoverflow.com


위에서 전달한 Intent의 requestCode로 onActivityResult()에서 해당 데이터를 처리할 수 있다.

 

    override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
        when (requestCode) {
            LOAD_IMG_VIEW_REQUEST_CODE -> {
                thread {
                    val uriArr = getResultUriArray(data)
                    runOnUiThread {
                        uriArr?.forEach { uri ->
                         // URI로 가져온 이미지 활용 코드 작성
                        }
                    }
                }
            }
        }

    }
    
    private fun getResultUriArray(data: Intent?): Array<Uri>? {
        val uriList = mutableListOf<Uri>()
        if (data?.data != null) {
            data.data?.let { uri ->
                if (!uri.toString().startsWith("content://"))
                    uriList.add(Uri.fromFile(File(uri.toString())))
                else
                    uriList.add(uri)
            }
        } else if (data?.clipData != null) {
            data.clipData?.let { clipData ->
                for (i in 0 until clipData.itemCount) {
                    uriList.add(clipData.getItemAt(i).uri)
                }
            }
        } else {
            if (mCameraPhotoPath != null) {
                uriList.add(mCameraPhotoPath!!)
            }
        }
        val results = getResizedFileList(uriList, 80)
        return if (results.isEmpty()) null else results.toTypedArray()
    }

    private fun getResizedFileList(uriList: List<Uri>, quality: Int): List<Uri> {
        val results = mutableListOf<Uri>()
        val dirPath = "${cacheDir}/Capture/"
        File(dirPath).let { dirs ->
            if (!dirs.exists()) {
                dirs.mkdirs()
            }
            dirs.listFiles()?.forEach { file ->
                file?.run {
                    if (System.currentTimeMillis() - file.lastModified() >= TEMP_IMG_REMOVE_INTERVAL) {
                        file.delete()
                    }
                }
            }
        }
        uriList.forEach { uri ->
            try {
                getRotatedBitmap(uri)?.let { bitmap ->
                    val fileName = "tmp_img_${System.currentTimeMillis()}.jpg"
                    val file = File("$dirPath$fileName")
                    file.outputStream().use { fos ->
                        bitmap.compress(Bitmap.CompressFormat.JPEG, quality, fos)
                    }
                    bitmap.recycle()
                    results.add(Uri.fromFile(file))
                }
            } catch (e: Exception) {
                e.printStackTrace()
            } finally {
                if (uri == mCameraPhotoPath) {
                    applicationContext.contentResolver.delete(uri, null, null)
                    mCameraPhotoPath = null
                }
            }
        }
        return results
    }

    private fun getFilePathFromUri(uri: Uri): String? {
        var path: String? = null
        val contentResolver = applicationContext.contentResolver
        contentResolver.query(uri, null, null, null, null)?.use { cursor ->
            cursor.moveToNext()
            val pathColumnIdx = cursor.getColumnIndex("_data")
            if (pathColumnIdx != -1) {
                path = cursor.getString(pathColumnIdx)
            } else {
                val idColumnIdx = cursor.getColumnIndex("document_id")
                if (idColumnIdx != -1) {
                    val documentId = cursor.getString(idColumnIdx)
                    val contentUri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI
                    val selection = "_id = ?"
                    val selectionArgs = arrayOf(documentId.split(':')[1])
                    contentResolver.query(contentUri, null, selection, selectionArgs, null)
                        ?.use { cursor2 ->
                            cursor2.moveToNext()
                            val pathColumnIdx2 = cursor2.getColumnIndex("_data")
                            if (pathColumnIdx2 != -1)
                                path = cursor2.getString(pathColumnIdx2)
                        }
                }
            }
        }
        return path
    }

    private fun getRotatedBitmap(uri: Uri): Bitmap? {
        contentResolver.openFileDescriptor(uri, "r")?.fileDescriptor?.let {
            var bitmap = BitmapFactory.decodeFileDescriptor(it)
            getFilePathFromUri(uri)?.let { path ->
                ExifInterface(path).run {
                    val orientation =
                        getAttributeInt(
                            ExifInterface.TAG_ORIENTATION,
                            ExifInterface.ORIENTATION_NORMAL
                        )
                    val degrees = when (orientation) {
                        ExifInterface.ORIENTATION_ROTATE_90 -> 90f
                        ExifInterface.ORIENTATION_ROTATE_180 -> 180f
                        ExifInterface.ORIENTATION_ROTATE_270 -> 270f
                        else -> 0f
                    }
                    if (degrees != 0f && bitmap != null) {
                        val matrix = Matrix().apply {
                            setRotate(degrees)
                        }
                        val converted = Bitmap.createBitmap(
                            bitmap, 0, 0, bitmap.width, bitmap.height, matrix, true
                        )

                        if (converted != bitmap) {
                            bitmap.recycle()
                            bitmap = converted
                        }
                    }
                }
                return bitmap
            }
        }
        return null
    }

override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?)

Activity에서 요청한 requestCode에 대한 결과값을 처리하는 메서드로 요청한 데이터를 Intent로 받아 처리할 수 있다.

 

fun getResultUriArray(data: Intent?): Array<Uri>?

onActivityResult()에서 받은 Intent 데이터를 확인해 Uri 배열을 반환한다.

 

fun getResizedFileList(uriList: List<Uri>, quality: Int): Array<Uri>

사진 파일의 크기를 변경하는 메서드

 

fun getFilePathFromUri(uri: Uri): String?

URI 데이터에 있는 경로를 가져오는 메서드

 

fun getRotatedBitmap(uri: Uri): Bitmap? 

사진촬영 시 기기 회전값을 확인해 사용자가 찍은 화면으로 회전 각도를 적용하여 Bitmap을 반환해주는 메서드

사진앱에서 촬영하면 핸드폰의 각도에 따라 사진의 회전이 보정되어 화면에 표시된다. 이는 사진에 포함된 정보에 회전 각도도 포함되기 때문인데 이미지를 그대로 불러올 경우 회전이 적용되지 않는 현상이 발생한다. 따라서 촬영했던 그대로 보여주기 위해 작업이 필요하다.