본문 바로가기

안드로이드/코틀린

[안드로이드/코틀린] 카메라와 갤러리에서 이미지 가져오기

반응형

 

해당 포스팅은 제 블로그 조회수에 상당수를 기록했습니다. 그만큼 앱을 개발하는데 있어 이미지는 필수사항이라고 해도 과언이 아닙니다. 많이 부족함에도 불구하고 찾아주셔서 감사합니다. 조금 더 도움이 되길 바라면서 코틀린 기반으로 재 업로드 합니다. 

자바로 작성된 코드를 보시고 싶으신 분은 여기서 확인해주세요.

권한 추가

카메라로 찍은 사진이나 갤러리에 있는 사진을 앱에서 사용하기 위해선 Androidmanifest.xml 에 권한을 선언해야 합니다.

<!--갤러리 권한-->
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/> 
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>

<!--갤러리 권한-->
<uses-permission android:name="android.permission.CAMERA"/>

카메라 및 갤러리 권한과 일부 위험 권한으로 분류된 경우 앱 이용자에게 명시적으로 허용을 받아야 합니다. 권한 체크는 아래 포스팅을 참고해주세요.

 

[안드로이드/Android] 권한 체크하기

안녕하세요. 오늘은 권한(Permission)에 관한 포스팅입니다. 권한은 앱에서 사용자 기기에 접근하여 사용자의 정보를 얻기 위해 얻는걸 말합니다. 마시멜로 이전 버전에서는 사용자가 인지하지 못한 상태에서 권한..

superwony.tistory.com

카메라로 찍은 사진 가져오기

 private fun selectCamera() {
        var permission = ContextCompat.checkSelfPermission(this, Manifest.permission.CAMERA)
        if (permission == PackageManager.PERMISSION_DENIED) {
            // 권한 없어서 요청
            ActivityCompat.requestPermissions(this, arrayOf(Manifest.permission.CAMERA), REQ_CAMERA_PERMISSION)
        } else {
            // 권한 있음
            var state = Environment.getExternalStorageState()
            if (TextUtils.equals(state, Environment.MEDIA_MOUNTED)) {
                var intent = Intent(MediaStore.ACTION_IMAGE_CAPTURE)
                intent.resolveActivity(packageManager)?.let {

                    var photoFile: File? = createImageFile()
                    photoFile?.let {
                        var photoUri = FileProvider.getUriForFile(this, BuildConfig.APPLICATION_ID + ".provider", it)
                        intent.putExtra(MediaStore.EXTRA_OUTPUT, photoUri)
                        startActivityForResult(intent, REQ_IMAGE_CAPTURE)
                    }
                }
            }
        }
    }

resolveActivity는 intent('ACTION_IMAGE_CAPTURE')에 해당하는 엑티비티들중 어떤 것을 실행할지 선택하게 합니다.

photoFile은 디바이스 내부에 카메라로 찍은 사진을 저장하기 위해 생성한 파일 변수 입니다. 

 

 private fun createImageFile(): File {
        // 사진이 저장될 폴더 있는지 체크
        var file = File(Environment.getExternalStorageDirectory(), "/path/")
        if (!file.exists()) file.mkdir()

        var imageName = "fileName.jpeg"
        var imageFile = File("${Environment.getExternalStorageDirectory().absoluteFile}/path/", "$imageName")
        imagePath = imageFile.absolutePath
        return imageFile
    }

디바이스에는 내부 저장소와 외부 저장소가 존재하며 경로를 선택해서 파일을 저장할 수 있습니다.  'Environment.getExternalStorageDirectory()' 외부 저장소의 경로를 가져오는 코드입니다.

외부 저장소에 path 폴더를 생성해 'fileName' 이름의 파일을 생성합니다. 이미지 파일 타입은 jpeg 와 png중 선택할 수 있는데 png 파일로 선택할 경우 이미지 파일 압축시 시간이 많이 소요되기 때문에 jpeg를 추천합니다.

이렇게 생성된 파일은 디바이스 '내 파일' > '내부저장소' > 'path' 에서 확인할 수 있습니다. ( s9+, os10 기준 ) 

 

카메라로 찍은 사진 이미지뷰 할당 

 override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
        super.onActivityResult(requestCode, resultCode, data)
        if (resultCode == Activity.RESULT_OK) { 
            when (requestCode) { 

                REQ_IMAGE_CAPTURE -> {
                    imagePath?.apply {
                        ctSelectImage.visibility = View.VISIBLE
                        GlideUtil.loadImage(activity = this@MealsCommentActivity,
                                requestOptions = RequestOptions().diskCacheStrategy(DiskCacheStrategy.NONE).skipMemoryCache(true),
                                image = imagePath,
                                imageView = ivSelectImage,
                                requestListener = object : RequestListener<Drawable> {
                                    override fun onLoadFailed(e: GlideException?, model: Any?, target: Target<Drawable>?, isFirstResource: Boolean): Boolean {
                                        hideLoading()
                                        return false
                                    }

                                    override fun onResourceReady(resource: Drawable?, model: Any?, target: Target<Drawable>?, dataSource: DataSource?, isFirstResource: Boolean): Boolean {
                                        hideLoading()
                                        return false
                                    }

                                })
                        checkInput()
                    }
                } 
            }

        }
    }

위에서 미리 만든 파일에 경로를 전역변수인 'imagePath'에 저장하고 카메라로 사진을 찍었을 경우(RESULT_OK) 경로에 있는 이미지를 글라이더로 가져왔습니다. 글라이더와 같은 라이브러리가 아닌 비트맵으로 생성해서 이미지 뷰에 할당해도 무관 합니다.

저는 이미지가 클 경우를 대비해서 글라이드를 사용했습니다.

 

프로바이더 설정

OS 7.0 이상부터 보안상의 이유로 'fileProvider'를 사용해서 가져와야 합니다. Androidmanifest.xml에 아래와 같이 명시해줘야 합니다.

 <provider
 	android:name="androidx.core.content.FileProvider"
    android:authorities="${applicationId}.provider"
    android:exported="false"
    android:grantUriPermissions="true">
    <meta-data
        android:name="android.support.FILE_PROVIDER_PATHS
        android:resource="@xml/provider_paths" />
</provider>

그리고 resource 파일을 xml 폴더에 생성해야 합니다. xml 폴더가 res 폴더 내에 존재하지 않으면 xml 폴더를 먼저 생성하고, 'provider_paths.xml'을 생성해서 아래 코드를 채워줍니다.

<?xml version="1.0" encoding="utf-8"?>
<paths xmlns:android="http://schemas.android.com/apk/res/android">
    <external-path name="hidden" path="path"/>
    <external-path name="external_files" path="."/>

</paths>

갤러리에서 이미지 가져오기

private fun selectGallery() {

        var writePermission = ContextCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE)
        var readPermission = ContextCompat.checkSelfPermission(this, Manifest.permission.READ_EXTERNAL_STORAGE)

        if (writePermission == PackageManager.PERMISSION_DENIED || readPermission == PackageManager.PERMISSION_DENIED) {
            // 권한 없어서 요청

            ActivityCompat.requestPermissions(this, arrayOf(Manifest.permission.WRITE_EXTERNAL_STORAGE, Manifest.permission.READ_EXTERNAL_STORAGE), REQ_STORAGE_PERMISSION)
        } else {
            // 권한 있음
            var intent = Intent(Intent.ACTION_PICK)
            intent.data = MediaStore.Images.Media.EXTERNAL_CONTENT_URI
            intent.type = "image/*"
            startActivityForResult(intent, REQ_GALLERY)
        }
    }

저장소 읽기/쓰기 권한을 확인하고 허용된 경우 갤러리 앱을 실행시키는데 읽기와 쓰기 권한은 나뉘어져 있지만 하나의 형태라서 and 가 아닌 or 로 확인해도 무관합니다.

 

이미지 표현하기

선택한 이미지의 실제 경로를 사용해 이미지를 로딩하는 방식으로 글라이드를 사용한 이유는 고용량 이미지의 경우 실제로 이미지뷰의 이미지가 그려는데 오랜 시간이 소요돼 로딩을 표현해주기 위함입니다.

 REQ_GALLERY -> {
                    data?.data?.let { it ->
                        showLoading() 
                        imagePath = getRealPathFromURI(it) 
                        GlideUtil.loadImage(activity = this@MealsCommentActivity,
                                requestOptions = RequestOptions(),
                                image = imagePath,
                                imageView = ivSelectImage,
                                requestListener = object : RequestListener<Drawable> {
                                    override fun onLoadFailed(e: GlideException?, model: Any?, target: Target<Drawable>?, isFirstResource: Boolean): Boolean {
                                        hideLoading()
                                        return false
                                    }

                                    override fun onResourceReady(resource: Drawable?, model: Any?, target: Target<Drawable>?, dataSource: DataSource?, isFirstResource: Boolean): Boolean {
                                        hideLoading()
                                        return false
                                    }

                                })
 
                    }

                }

 

이미지 실제 경로 반환

 private fun getRealPathFromURI(uri: Uri): String {

        var buildName = Build.MANUFACTURER
        if (buildName.equals("Xiaomi")) {
            return uri.path
        }


        var columnIndex = 0
        var proj = arrayOf(MediaStore.Images.Media.DATA)
        var cursor = contentResolver.query(uri, proj, null, null, null)
        if (cursor.moveToFirst()) {
            columnIndex = cursor.getColumnIndexOrThrow(MediaStore.Images.Media.DATA)
        }
        return cursor.getString(columnIndex)
    }

샤오미의 경우 contentResolver를 이용하면 null 에러가 발생하기 때문에 예외처리를 해줬습니다.

 

고용량 이미지 리사이즈

갤러리에서 가져온 이미지중 외부에서 다운 받았을 경우 고용량 이미지가 있을수 있고 해당 파일을 서버로 전송해도 무관하나 클라이언트에서 이미지를 리사이즈 한다면 전송 시간도 절약하고 서버 자원 낭비도 막을 수 있습니다. 

private fun getResizePicture(imagePath: String): Bitmap {
        var options = BitmapFactory.Options()
        options.inJustDecodeBounds = true
        BitmapFactory.decodeFile(imagePath, options)

        var resize = 1000
        var width = options.outWidth
        var height = options.outHeight
        var sampleSize = 1
        while (true) {
            if (width / 2 < resize || height / 2 < resize)
                break
            width /= 2
            height /= 2
            sampleSize *= 2

        }
        options.inSampleSize = sampleSize
        options.inJustDecodeBounds = false

        var resizeBitmap = BitmapFactory.decodeFile(imagePath, options)

        // 회전값 조정
        var exit = ExifInterface(imagePath)
        var exifDegree = 0
        exit?.let {
            var exifOrientation = it.getAttributeInt(ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_NORMAL)
            exifDegree = exifOreintationToDegrees(exifOrientation)
        }

        return roteateBitmap(resizeBitmap, exifDegree)
    }
    

 'resize'는 이미지의 최대 크기로 해당 사이즈보다 작아지지 않도록 하는 코드 입니다.

 

이미지 저장

private fun saveBitmap(bitmap: Bitmap): String {
        var folderPath = Environment.getExternalStorageDirectory().absolutePath + "/path/"
        var fileName = "comment.jpeg"
        var imagePath = folderPath + fileName

        var folder = File(folderPath)
        if (!folder.isDirectory) folder.mkdirs()

        var out = FileOutputStream(folderPath + fileName)

        bitmap.compress(Bitmap.CompressFormat.JPEG, 100, out)

        return imagePath
    }

위와(카메라 사진 저장) 같은 방법으로 리사이즈한 이미지를 저장해 보다 작은 파일을 전송하기 위해 디바이스 내부에 저장합니다. 파일 확장자는 속도에 용이한 jpeg를 사용합니다. imagepath를 반환해 실제로 올리는 파일은 리사이즈 되어 저장된 이미지 파일이며 저장된 파일은 지워주는걸 권장합니다. ( 저장 공간에 민감한 사용자 배려 )

 

 

기존에 자바로 작성했던 포스팅을 참고해 작업하던 중 고용량 이미지 및 기기 파편화 이슈를 겪어 새로 정리하고자 했고, 다른 이슈 사항이 생기면 추가적으로 기재하도록 하겠습니다. 

 

반응형