본문 바로가기
Development/Android (Kotlin)

Kotlin 카메라 & 갤러리 사진 처리하기1 (Old Camera1 Ver.)

by 알 수 없는 사용자 2020. 6. 11.

안녕하세요 휴몬랩 쌩초보 안드로이드 개발자 호박입니다.
오랜만에 글을 올리게 되었는데 이번에는 Kotlin에서 어떻게 카메라를 사용할 수 있는지에 대해 올리고자 합니다.


처음 핸드폰이 나올땐 카메라는 그저 거둘뿐..이라는 mind 였던거 아시나요?
하지만 시간이 지남에따라 사람들은 카메라에 더 관심을 가지게 되었고 이제는 카메라의 성능이 최우선으로 되는 상태까지 이르렀죠! 이렇게 카메라의 인기도가 높아지면서 더 많은 관심이 생겨나고 카메라의 개발또한 많이 이루어졌습니다. 그렇게해서 지금은 Camera1은 Deprecated 되었고 Camera2 API 까지 나오게 되었지만 이번 글에서는 Old Version의 Camera1에 대해서 알아볼까 합니다.

 

- 간단하게 두 API의 큰 차이점은?
Camera1 : Deprecated (계속 사용 가능), 단순 사진찍기

Camera2 : minSdkVersion 이 21(Marshmellow) 이상 사용가능, 카메라의 professional한 다양한 기능들, 속도개선

 

- 그럼 어떤 Camera API를 사용해야하나요?

결과적으로는 카메라를 메인으로 사용하는 앱을 개발해야한다면 Camera2 API를 사용하고
단순하게 카메라를 사용하여 수정없이 이미지만 올린다고 한다면 Camera1 API를 사용하는것도 괜찮겠죠?
다만 저라면 이미 Deprecated 된 API는 더 이상 개발이 안되기도 하고 아직은 Camera2에 생기는 많은 버그들이 문제라면 지금 당장은 Camera1을 사용 하더라도 추후에는 새로운 버전으로 교체해주는 것이 맞다고 생각됩니다.
뭐..제일 Best는 Camera1 -> Camera1 + Camera2 -> Camera2 (Android API lvl 21 미만의 기종들이 멸종한다면..)가 아닐까요?! 저도 이런식으로 가볼까 합니다 ^^
=> 즉 정답은 없고 원하는 방식을 사용하시면 됩니다~~

 

- Camera1은 어떻게 진행되나요?

1. Manifest에 권한 설정

2. Camera 권한 요청 및 권한 체크

3. Camera로 사진찍기

4. OnActivityResult에서 Data(사진)받기

5. ImageView에 받은 Data 뿌려주기

 

 

1. Manifest 권한 설정

// 카메라 권한 설정
<uses-permission android:name="android.permission.CAMERA" />  

// 저장 권한 설정 (이미지나 동영상을 기기의 외부 장치에 저장할경우)
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />

// 카메라기능이 무조건적으로 필요하지 않을 경우
<uses-feature android:name="android.hardware.camera" android:required="false" />

안드로이드에는 수많은 권한이 있습니다. 대표적으로 인터넷, 카메라 등
이러한 권한들을 Manifest에 설정해야만 사용할 수 있습니다.

이렇게 Camera1 API 를 사용하기 위한 권한 설정들을 해줍니다.

 

 

2. 카메라 권한 요청 및 권한 체크

val REQUEST_IMAGE_CAPTURE = 1

// 카메라 권한 요청 
private fun requestPermission() {
    ActivityCompat.requestPermissions(this, arrayOf(READ_EXTERNAL_STORAGE, CAMERA),
        REQUEST_IMAGE_CAPTURE)
}

// 카메라 권한 체크
private fun checkPersmission(): Boolean {		
    return (ContextCompat.checkSelfPermission(this, android.Manifest.permission.CAMERA) ==
            PackageManager.PERMISSION_GRANTED && ContextCompat.checkSelfPermission(this,
        android.Manifest.permission.READ_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED)
}

// 권한요청 결과
override fun onRequestPermissionsResult(
    requestCode: Int,
    permissions: Array<out String>,
    grantResults: IntArray
) {
    super.onRequestPermissionsResult(requestCode, permissions, grantResults)
    if (requestCode == 1 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
        Log.d("TAG", "Permission: " + permissions[0] + "was " + grantResults[0] + "카메라 허가 받음 예이^^")
    }else{
        Log.d("TAG","카메라 허가 못받음 ㅠ 젠장!!")
    }
}
  • 카메라 권한 요청 - Manifest에서 권한 설정을 하였으니 이젠 사용자에게 권한 요청을 해야겠죠?
                             이 함수를 사용하면 권한 요청 메세지가 뜹니다. 

  • 카메라 권한 체크 - 카메라를 사용할 시 항상 이 체크 함수가 제일 먼저 사용됩니다. 카메라 사용을 사용자로부터 허용을 받았는지 거부 당했는지 체크를 하고 그 결과에 따라서 카메라가 실행이 될지 안될지 결정됩니다.
  • 권한요청 결과 - 권한 요청 결과값이 이 함수를 통해 어떻게 해야할지 정해지게 됩니다.
                         권한을 받지 못했다면 앱을 종료시킨다던지 권한이 필요하다는 메세지를 띄울수도 있습니다.

카메라를 열기위한 버튼을 하나 만들어서 클릭리스너를 사용해 카메라 권한설정을 만들어 보았습니다.
버튼을 눌렀을 때, 권한요청을 받았다면 카메라가 실행이되지만 그렇지 않을 경우엔 권한 요청 메세지가 뜨게됩니다.

참 쉽죠?

 

3. Camera로 사진찍기

// 카메라 열기
    private fun dispatchTakePictureIntent() {
        Intent(MediaStore.ACTION_IMAGE_CAPTURE).also { takePictureIntent ->
            if (takePictureIntent.resolveActivity(this.packageManager) != null) {
                // 찍은 사진을 그림파일로 만들기
                val photoFile: File? =
                        try {
                            createImageFile()
                        } catch (ex: IOException) {
                            Log.d("TAG", "그림파일 만드는도중 에러생김")
                            null
                        }

                // 그림파일을 성공적으로 만들었다면 onActivityForResult로 보내기
                photoFile?.also {
                    val photoURI: Uri = FileProvider.getUriForFile(
                            this, "com.example.cameraonly.fileprovider", it
                    )
                    takePictureIntent.putExtra(MediaStore.EXTRA_OUTPUT, photoURI)
                    startActivityForResult(takePictureIntent, REQUEST_IMAGE_CAPTURE)
                }
            }
        }
    }
    

// 카메라로 촬영한 이미지를 파일로 저장해준다
    @Throws(IOException::class)
    private fun createImageFile(): File {
        // Create an image file name
        val timeStamp: String = SimpleDateFormat("yyyyMMdd_HHmmss").format(Date())
        val storageDir: File? = getExternalFilesDir(Environment.DIRECTORY_PICTURES)
        return File.createTempFile(
                "JPEG_${timeStamp}_", /* prefix */
                ".jpg", /* suffix */
                storageDir /* directory */
        ).apply {
            // Save a file: path for use with ACTION_VIEW intents
            currentPhotoPath = absolutePath
        }
    }

위 코드에서 초록색으로 되있는 부분, "com.example.cameraonly.fileprovider" 에서 하이라이트 된 부분은 제가 만든 테스트앱의 패키지명입니다. 본인이 사용하고있는 패키지명으로 바꿔서 사용하시면 되요~~


여기까지 되셨다면 카메라가 문제없이 실행이 됩니다!! 뚜둥..
하지만 이제부터 시작이라는ㅠㅠ!!

카메라로 사진을 찍으면 바로 이미지 파일로 저장(createImageFile())을 하게 됩니다. 
그렇게 성공적으로 파일이 만들어진다면 Intent를 사용하여 OnActivityForResult로 받아지게 되는거죠.


여기서 한가지 중요한 점이 있는데요?!!
만약 현재 사용하고 있는 폰 기종이 Android 7.0 API lvl24 Nougat 미만이라면?

안드로이드 7.0 Nougat 버전을 기준으로 설정이 조금 변경됩니다.
카메라열기 하단부분에 보면 photoFile 부분에서 Nougat 버전을 기준으로 코드가 다른걸 볼 수 있는데
이렇게 나눈 이유는 Nougat버전 이후로는 보안상의 문제로 provider을 추가해서 사용해야지 오류가 나지 않습니다.
물론 Nougat 이전의 버전에서는 provider을 사용하면 카메라 오류가 나옵니다..ㅠㅠ
그렇기 때문에 이렇게 안드로이드 버전을 기준으로 사용해야 한다는거죠!!
만들고자하는 앱의 버전에 맞춰서 꼭 필요한 코드만 작성하면 됩니다 :)

if (Build.VERSION.SDK_INT < 24) {
    if(photoFile != null){
    val photoURI = Uri.fromFile(photoFile)
    takePictureIntent.putExtra(MediaStore.EXTRA_OUTPUT, photoURI)
    startActivityForResult(takePictureIntent, REQUEST_IMAGE_CAPTURE)
    }
}
else{
    photoFile?.also {
    val photoURI: Uri = FileProvider.getUriForFile(
    this, "com.example.cameraonly.fileprovider", it
    )
    takePictureIntent.putExtra(MediaStore.EXTRA_OUTPUT, photoURI)
    startActivityForResult(takePictureIntent, REQUEST_IMAGE_CAPTURE)
    }
}

위 코드에서 초록색으로 되있는 부분, "com.example.cameraonly.fileprovider" 에서 하이라이트 된 부분은 제가 만든 테스트앱의 패키지명입니다. 본인이 사용하고있는 패키지명으로 바꿔서 사용하시면 되요~~

 

그럼 fileprovider 설정은 어찌할까요?

<application
	<activity>
        </activity>
            <provider
                android:name="androidx.core.content.FileProvider"
                android:authorities="com.example.cameraonly.fileprovider"
                android:exported="false"
                android:grantUriPermissions="true">
                    <meta-data
                    android:name="android.support.FILE_PROVIDER_PATHS"
                    android:resource="@xml/file_paths"/>
            </provider>
</application>

제일먼저 manifest에 <provider> 부분을 추가해줍니다.
여기서 중요한 부분은 :
1. android:authorities="com.example.cameraonly.fileprovider" 의 색깔 칠한부분은 manifest 최상단에 있는 자신의 앱 패키지명으로 바꿔주시면 됩니다. 여기에 나와있는 예시는 제 테스트 앱의 패키지명이니깐요 :)

2. meta-data안에 있는 resource="@xml/file_paths" 가있는데 xml폴더를 새로 만들어서 file_paths로 이름을 지어줍니다.

 

=> res 폴더에서 new Folder를 xml로 정하고 새로만든 xml 폴더에 file_paths.xml 이라고 만들어 줍니다.

<?xml version="1.0" encoding="utf-8"?>
<paths xmlns:android="http://schemas.android.com/apk/res/android">
    <external-path name="my_images" path="Android/data/com.example.cameraonly/files/Pictures" />
</paths>

 

여기서 또 주의할 점이 하나있죠? path="Android/data/com.example.cameraonly/files/Pictures" />

이 부분 역시 위와 마찬가지로 각자의 패키지명으로 바꿔주시면 됩니다.

자 이제 마지막 단계로 넘어가볼까요?

 

 

4. OnActivityResult에서 Data(사진)받기

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

        when (requestCode){
            1 -> {
                if(requestCode == REQUEST_IMAGE_CAPTURE && resultCode == Activity.RESULT_OK){

                    // 카메라로부터 받은 데이터가 있을경우에만
                    val file = File(currentPhotoPath)
                    if (Build.VERSION.SDK_INT < 28) {
                        val bitmap = MediaStore.Images.Media
                                .getBitmap(contentResolver, Uri.fromFile(file))  //Deprecated
                        image.setImageBitmap(bitmap)
                    }
                    else{
                        val decode = ImageDecoder.createSource(this.contentResolver,
                                Uri.fromFile(file))
                        val bitmap = ImageDecoder.decodeBitmap(decode)
                        image.setImageBitmap(bitmap)
                    }
                }
            }
        }
    }

마지막으로 onAcitivtyResult로 Data를 받아서 처리를 하는 과정입니다.

아까 카메라를 찍고나서 Intent로 사진파일을 만들어서 보냈었죠? 그 파일을 여기서 받아서 처리를 하게 됩니다.
SDK 버전이 28 미만일경우와 아닐경우로 나뉘었는데요 그 이유는 getBitmap 함수가 Deprecated 되었기 때문입니다.
위 코드에 나와있는 image는 사진을 표시할 이미지뷰 입니다.

받은 사진파일을 bitmap으로 만들어서 이미지뷰에 뿌려주게 되는거죠.

 

 

5. 이미지뷰에 사진 뿌리기

아주 깔끔하게 잘 나옵니다 :) 
테스트한 기기는 갤럭시 S8 이였습니다.

그런데 말입니다. 몇 몇 다른 기종에 의해서 사진이 돌아가서 나오는 경우가 있습니다!!!

맞습니다...완전 유물이된 S3 기종인데요..
이 기종이 아직도 제 손에 있다는게 신기하네요 ㅎㅎㅎ
분명히 위와 같이 정면에서 똑바로 찍었는데 이미지뷰에 뿌리고 나서 보니 화면이 90도 돌아가있습니다.
어떤 기종은 180도 돌아간다고도 합니다...


이렇게 되는 이유에는 스마트폰이 사진을 찍을 때 중력방향? 스마트폰 방향? 스마트폰 회전각?
애시당초 폰의 카메라는 가로로 누워서 촬영하는게 기본으로 설정되어서 나왔다는 말이 있다는데..
가로로 찍었을때 제대로 나오는걸 보니 맞는 말인듯 합니다 ㅎㅎ


뭐 어떤이유에서든지 원하는대로만 나오게끔 하면 되는거 아니겠어요?! 하하하..

private fun rotateImageIfRequired(imagePath: String): Bitmap? {
        var degrees = 0
        try {
            val exif = ExifInterface(imagePath)
            val orientation = exif.getAttributeInt(
                ExifInterface.TAG_ORIENTATION,
                ExifInterface.ORIENTATION_NORMAL
            )
            when (orientation) {
                ExifInterface.ORIENTATION_ROTATE_90 -> degrees = 90
                ExifInterface.ORIENTATION_ROTATE_180 -> degrees = 180
                ExifInterface.ORIENTATION_ROTATE_270 -> degrees = 270
            }
        } catch (e: IOException) {
            Log.e("ImageError", "Error in reading Exif data of $imagePath", e)
        }

        val decodeBounds: BitmapFactory.Options = BitmapFactory.Options()
        decodeBounds.inJustDecodeBounds = true
        var bitmap: Bitmap? = BitmapFactory.decodeFile(imagePath, decodeBounds)
        val numPixels: Int = decodeBounds.outWidth * decodeBounds.outHeight
        val maxPixels = 2048 * 1536 // requires 12 MB heap
        val options: BitmapFactory.Options = BitmapFactory.Options()
        options.inSampleSize = if (numPixels > maxPixels) 2 else 1
        bitmap = BitmapFactory.decodeFile(imagePath, options)
        if (bitmap == null) {
            return null
        }

        val matrix = Matrix()
        matrix.setRotate(degrees.toFloat())
        bitmap = Bitmap.createBitmap(
            bitmap, 0, 0, bitmap.width,
            bitmap.height, matrix, true
        )
        return bitmap
    }

돌아간 이미지는 ExifInterface를 사용하여 원하는대로 맞춰주면 깔끔하게 고쳐집니다 :)


마지막으로 bitmap 대신 회전시킨 파일을 넣어주면 되겠죠?

image.setImageBitmap(rotateImageIfRequired(file.path))

 

 

추가적으로 갤러리로부터 사진을 가져오는 것 까지 빠르게 한번 해볼까요?

val REQUEST_GALLERY_TAKE = 2

//갤러리 열기
    private fun openGalleryForImage() {
        val intent = Intent(Intent.ACTION_PICK)
        intent.type = "image/*"
        startActivityForResult(intent, REQUEST_GALLERY_TAKE)
    }
    
    
    // onActivityResult 로 이미지 설정
    override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
        super.onActivityResult(requestCode, resultCode, data)

        when (requestCode){
            2 -> {
                if (resultCode == Activity.RESULT_OK && requestCode == REQUEST_GALLERY_TAKE){
                    image.setImageURI(data?.data) // handle chosen image
                }
            }
        }
    }
    

카메라로 권한설정을 다 끝내놨으니 갤러리는 매우 쉽게 할 수 있습니다!
카메라 대신 갤러리를 열고 onActivityResult에서 갤러리 ResultCode를 받아서 URI로 이미지를 설정해주면 짜잔!!
참 쉽죠?

 

 

이제 다음으로 다음 포스팅에서는 카메라와 앨범을 동시에 사용하도록하고 추가적으로 CROP 기능까지 달아볼까요?
그리고 최종보스인 Camera2 API까지...도저언!!

댓글