[Android] 9. 카메라와 갤러리

7 minute read


카메라 사용하기

안드로이드 6.0 (API Level 23, targetSdkVersion 23)버전 이후부터 카메라 관련 작업도 위험 권한으로 분류되어 부가적인 코드 처리가 필요합니다.


UI 화면 만들고 권한 명세하기


UI는 다음과 같이 구성합니다.

image-20210817153803455

  • 이미지뷰: id = imagePreview
  • 버튼1: id = btnCamera, text = CAMERA
  • 버튼2: id=btnGallery, text = GALLERY


다음으로 AndroidMenifest.xml 파일에 카메라와 캘러리에 대한 권한을 명세합니다.

카메라를 사용하기 위해서는 < uses-features /> 태그도 같이 설정해야 합니다.

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="kr.co.hanbit.cameraandgallery">

    <!-- 카메라 권한 -->
    <uses-permission android:name="android.permission.CAMERA"/>
    <!-- 저장소 읽기 권한 -->
    <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
    <!-- 저장소 쓰기 권한 -->
    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>

    <!-- 카메라 사용 선언 -->
    <uses-feature android:name="android.hardware.camera2"/>

    <application
				...
    </application>

</manifest>



권한 처리를 위한 코드 작성하기


카메라 및 촬영한 사진을 저장할 외부 저장소의 권한을 요청하는 코드를 작성합니다.


1. 권한을 처리하는 BaseActivity 불러오기

앞선 포스팅 중 6(3) 번 포스팅에서 권한을 처리하는 클래스인 BaseActivity를 작성했었습니다. 이 클래스를 현재 프로젝트에 추가합니다.

BaseActivity를 열어서 패키지명이 현재 프로젝트와 다르면 현재 패키지명에 맞게 수정합니다.


2. MainActivity.kt 파일 수정하기

먼저 MainActivity 클래스가 BaseActivity를 상속하도록 수정하고 [Ctrl+I]를 눌러 2개의 추상 메서드를 생성합니다.

그리고 바인딩을 연결하고, 권한 처리와 카메라 요청에 관한 상수 3개를 선언합니다.

// 권한을 처리하는 BaseActivity 상속
// class MainActivity : AppCompatActivity() {
class MainActivity: BaseActivity(){

    // 카메라 관련 상수 선언
    // 클래스 내에서 상수 선언 시 companion object 블록 필요
    companion object{
        const val PERM_STORAGE = 99 // 외부 저장소 권한 요청
        const val PERM_CAMERA = 100 // 카메라 권한 요청
        const val REQ_CAMERA = 101  // 카메라 호출
    }

    val binding by lazy{ActivityMainBinding.inflate(layoutInflater)}

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(binding.root)

    }

    override fun permissionGranted(requestCode: Int) {
    }

    override fun permissionDenied(requestCode: Int) {
    }



MainActivity에서 카메라 앱 호출하기


1. 외부 저장소 권한 요청

첫번째로 메인 액티비티가 띄워질 때 바로 외부 저장소 열람에 대한 권한을 요청하도록 onCreate 메서드 안에 코드를 추가합니다.

// 카메라에서 찍은 사진을 외부 저장소(포토갤러리)에 저장할 것이기 때문에 저장소 권한을 요청하는 코드 작성
// 파라미터: 요청할 저장소 권한, requestCode(앞에서 미리 정의)
requirePermissions(arrayOf(Manifest.permission.WRITE_EXTERNAL_STORAGE), PERM_STORAGE)


2. 외부 저장소 권한 승인 여부 처리

승인을 요청하고 나면 항상 메인 액티비티에서 요청의 승인/미승인 여부에 따른 처리를 해주어야 합니다.

‘승인’일 경우 permissionGranted( ) 메서드가 호출됩니다.

    override fun permissionGranted(requestCode: Int) {
        // 권한 승인 시 적절한 메서드 호출
        when(requestCode){
            // 외부 저장소 권한 승인 시 카메라 권한 요청
            PERM_STORAGE -> setViews()
        }
    }

‘거부’일 경우 permissionDenied( ) 메서드가 호출됩니다.

    override fun permissionDenied(requestCode: Int) {
        when(requestCode){
            PERM_STORAGE -> {
                Toast.makeText(baseContext,
                                "외부 저장소 권한을 승인해야 앱을 사용할 수 있습니다.",
                                Toast.LENGTH_LONG).show()
                finish()
            }
        }
    }


3. 카메라 권한 요청

외부 저장소에 대한 권한 요청이 승인되었다면 바로 이어 setView( ) 메서드가 호출됩니다. setViews( ) 메서드는 카메라 권한을 요청합니다.

    // 외부 저장소 권한이 승인되었을 때 호출되는 메서드
    fun setViews(){
        // 카메라 권한 요청
        // 권한 요청의 결과에 따라 승인되었을 경우에만 permissionGranted() 메서드에서 카메라 요청
        binding.btnCamera.setOnClickListener {
            requirePermissions(arrayOf(Manifest.permission.CAMERA),PERM_CAMERA)
        }
    }


4. 카메라 권한 요청 승인 여부 처리

마찬가지로 permissionGranted, permissionDenied 메서드에 카메라 권한 요청 승인 여부에 대한 코드를 추가합니다.

    override fun permissionGranted(requestCode: Int) {
        // 권한 승인 시 적절한 메서드 호출
        when(requestCode){
            // 외부 저장소 권한 승인 시 카메라 권한 요청
            PERM_STORAGE -> setViews()
            // 카메라 권한 승인 시 카메라 호출
            PERM_CAMERA -> openCamera()
        }
    }

    override fun permissionDenied(requestCode: Int) {
        when(requestCode){
            PERM_STORAGE -> {
                Toast.makeText(baseContext,
                                "외부 저장소 권한을 승인해야 앱을 사용할 수 있습니다.",
                                Toast.LENGTH_LONG).show()
                finish()
            }
            PERM_CAMERA -> {
                Toast.makeText(baseContext,
                                "카메라 권한을 사용해야 카메라를 사용할 수 있습니다.",
                                Toast.LENGTH_LONG).show()
            }
        }
    }


5. 카메라 호출

카메라 권한 요청까지 승인받았다면, 바로 카메라를 호출합니다.

카메라를 요청할 때에도 액티비티를 요청하듯 인텐트를 전달하며 startActivityForResult( ) 메서드를 호출하면 됩니다.

    // 실질적인 카메라 호출 메서드
    fun openCamera(){
        // 카메라 호출 시 보낼 인텐트
        val intent = Intent(MediaStore.ACTION_IMAGE_CAPTURE)
        // 카메라 호출
        startActivityForResult(intent, REQ_CAMERA)
    }


6. 촬영된 사진 정보 받아서 처리하기

카메라에서 사진을 촬영하면 onActivityResult( ) 메서드로 그 결괏값이 전달됩니다.

촬영한 사진 정보는 세 번째 파라미터인 data에 인텐트로 전달되며, 전달받은 data 파라미터에서 사진을 꺼낸 후 이미지뷰에 세팅합니다.

    // 카메라로부터 촬영된 사진 정보를 받아서 텍스트뷰에 출력
    override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
        super.onActivityResult(requestCode, resultCode, data)

        if(resultCode == RESULT_OK)
            when(requestCode){
                  // data 파라미터를 통해 전달되는 사진은 data.extras.get("data")로 꺼낼 수 있습니다.
                  if (data?.extras?.get("data") != null){
                      // Bitmap으로 형변환 (원본 타입은 Object)
                      val bitmap = data?.extras?.get("data") as Bitmap
                      binding.imagePreview.setImageBitmap(bitmap)
                  }

            }
    }



Uri 사용하기


여기까지의 과정만으로 촬영한 이미지를 이미지 프리뷰 화면에 띄울 수 있습니다. 하지만 띄워진 이미지를 보면 약간 흐릿합니다.

image-20210817165017859

이는 onActivityResult의 세번째 파라미터로 전달되는 data에는 해당 이미지의 프리뷰가 들어있기 때문입니다. 따라서 실제 이미지를 미디어 스토어에 저장하고 저장된 이미지를 가져와서 화면에 보여주는 것이 좋습니다.

MainActivity에서 카메라 앱 호출하기의 5번 과정부터 다시 진행해보겠습니다.


5. 촬영한 이미지를 저장할 Uri 생성

카메라가 호출되고 촬영을 하기 전에 몇 가지 과정을 거쳐야 합니다.

먼저 MainActivity의 프로퍼티로 사용할 Uri 인스턴스를 선언합니다. Uri는 특정 리소스 자원을 고유하게 식별할 수 있는 식별자입니다.

// 이미지의 Uri를 가져와서 저장할 프로퍼티 선언
var realUri: Uri? = null

그 다음으로 촬영한 이미지를 저장할 Uri를 미디어스토어에 생성하는 createImageUri( ) 메서드를 생성합니다.

    // 촬영한 이미지를 저장할 Uri를 미디어스토어에 생성하는 메서드
    fun createImageUri(filename: String, mimeType: String): Uri?{
        // ContentValues 클래스를 사용해 파일명과 파일의 타입을 입력한 후,
        // ContentResolver의 insert() 메서드를 통해 저장
        var values = ContentValues()
        values.put(MediaStore.Images.Media.DISPLAY_NAME, filename)
        values.put(MediaStore.Images.Media.MIME_TYPE, mimeType)
        // 파라미터: 저장소명, 저장할 콘텐트
        return contentResolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, values)

    }


6. Uri에 촬영한 이미지 저장

이제 카메라를 호출하고 촬영 시 미디어스토어에 이미지를 저장하도록 openCamera( ) 메서드를 수정합니다.

    // 실질적인 카메라 호출 메서드
    fun openCamera(){
        // 카메라 호출 시 보낼 인텐트
        val intent = Intent(MediaStore.ACTION_IMAGE_CAPTURE)

        // 카메라 호출
        // startActivityForResult(intent, REQ_CAMERA)

        // 촬영된 이미지를 바로 사용하지 않고 Uri로 생성하여 미디어스토어에 저장하고 사용하기
        // createImageUri 파라미터: filename, mimetype
        createImageUri(newFileName(), "image/jpg")?.let{uri->
            realUri = uri
            intent.putExtra(MediaStore.EXTRA_OUTPUT, realUri)
            startActivityForResult(intent, REQ_CAMERA)
        }


    }

createImageUri( ) 메서드를 호출하는 부분을 보면 파라미터로 newFilename( ) 메서드의 반환값을 전달하는데, 이 새로운 파일명을 생성해주는 메서드를 정의합니다.

// 새로운 파일명을 만들어주는 메서드 (파일명이 중복되지 않도록 현재 시각 사용)
fun newFileName(): String{
    // SimpleDateFormat(java.text) 사용
    val sdf = SimpleDateFormat("yyyyMMdd_HHmmss")
    val filename = sdf.format(System.currentTimeMillis())
    // 연월일_시간.jpg
    return "$filename.jpg"
}

파일명은 중복되지 않도록 현재 시각을 기반으로 생성했습니다.



7. 미디어스토어에 저장된 이미지 읽어오기

카메라에서 촬영을 하면 촬영된 이미지는 미디어스토어에 저장되고 우리는 파라미터로 선언된 realUri 변수를 이용하여 원하는 이미지를 불러오면 됩니다.

카메라로 촬영한 이미지를 불러올 때는 Uri를 Bitmap으로 변환한 후에 출력해야 합니다.


먼저 미디어스토어에서 저장된 이미지를 읽어오는 메서드를 구현합니다.

입력 파라미터로 Uri를 받아서 결괏값을 Bitmap으로 반환해줍니다. API 버전이 27 이하이면 MediaStore에 있는 getBitmap 메서드를 사용하고, 27보다 크면 ImageDecoder를 사용합니다.

    // Uri를 이용해서 미디어스토어에 저장된 이미지를 읽어오는 메서드
    // 파라미터로 Uri를 받아 결괏값을 Bitmap으로 반환
    fun loadBitmap(photoUri: Uri): Bitmap?{
        var image: Bitmap? = null
        //API 버전이 27 이하이면 MediaStore에 있는 getBitmap 메서드를 사용하고, 27보다 크면 ImageDecoder를 사용
        try{
            image = if (Build.VERSION.SDK_INT > 27){
                val source: ImageDecoder.Source =
                        ImageDecoder.createSource(this.contentResolver, photoUri)
                ImageDecoder.decodeBitmap(source)
            }else{
                MediaStore.Images.Media.getBitmap(this.contentResolver, photoUri)
            }

        }catch(e: IOException){
            e.printStackTrace()
        }

        return image
    }


그리고 카메라가 촬영을 마치면 호출되는 onActivityResult 메서드를 수정합니다.

이미지를 가져온 후에 realUri 프로퍼티는 null 처리해주어야 다음에도 사용할 수 있습니다.

    // 카메라로부터 촬영된 사진 정보를 받아서 텍스트뷰에 출력
    override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
        super.onActivityResult(requestCode, resultCode, data)

        if(resultCode == RESULT_OK)
            when(requestCode){
                REQ_CAMERA -> {
                    realUri?.let { uri ->
                        val bitmap = loadBitmap(uri)
                        binding.imagePreview.setImageBitmap(bitmap)

                        realUri = null
                    }

//                  // data 파라미터를 통해 전달되는 사진은 data.extras.get("data")로 꺼낼 수 있습니다.
//                  if (data?.extras?.get("data") != null){
//                      // Bitmap으로 형변환 (원본 타입은 Object)
//                      val bitmap = data?.extras?.get("data") as Bitmap
//                      binding.imagePreview.setImageBitmap(bitmap)
//                  }

                }
            }
    }


이제 이미지뷰에 출력되는 이미지를 보면 선명한 이미지가 출력되는 것을 확인할 수 있습니다.

image-20210817171436503

또한 기본 앱은 Photos에 들어가보면 촬영한 사진이 저장되어 있습니다.

image-20210816185012812



갤러리에서 사진 가져오기

갤러리에서 사진을 가져오는 것은 간단합니다.

갤러리를 호출하고, 이미지를 선택하면 해당 이미지 데이터를 가지고 있는 data를 그대로 사용하면 됩니다.


1. 갤러리 호출을 나타내는 requestCode 상수 추가

카메라 호출을 위해 상수들을 추가한 것처럼, 갤러리 호출을 의미하는 requestCode를 상수로 추가합니다.

    companion object{
        const val PERM_STORAGE = 99 // 외부 저장소 권한 요청
        const val PERM_CAMERA = 100 // 카메라 권한 요청
        const val REQ_CAMERA = 101  // 카메라 호출

        const val REQ_STORAGE = 102 // 갤러리 호출
    }

2. setViews 메서드에 갤러리 호출 코드 추가하기

갤러리를 열람할 때도 외부 저장소에 대한 권한 승인이 필요하지만, 앱을 시작할 때 허가를 받았으므로 생략할 수 있습니다.

외부 저장소에 대한 승인을 받았다면 따로 갤러리에 대한 권한 승인은 받지 않아도 됩니다.

     fun setViews(){
         binding.btnCamera.setOnClickListener {
             requirePermissions(arrayOf(Manifest.permission.CAMERA),PERM_CAMERA)
         }
         // 갤러리 열람 메서드 호출
         binding.btnGallery.setOnClickListener {
             openGallery()
         }
     }


3. openGallery 메서드 정의

카메라 호출과 마찬가지로 갤러리를 호출할 때도 인텐트와 함께 startActivityForResult 메서드를 호출합니다.

intent의 파라미터로 ACTION_PICK을 사용하면 intent.type에서 설정한 종류의 데이터를 MediaStore에서 불러와 목록으로 나열한 후 선택할 수 있는 앱이 실행됩니다.

    // 갤러리 열람 메서드
    fun openGallery(){
        val intent = Intent(Intent.ACTION_PICK)
        intent.type = MediaStore.Images.Media.CONTENT_TYPE
        startActivityForResult(intent, REQ_STORAGE)
    }


4. onActivityResult 메서드에 갤러리 관련 처리 코드 추가하기

갤러리에서 이미지를 선택하고 나면 onActivityResult 메서드가 호출됩니다.

onActivityResult 메서드에 전달받은 이미지 정보를 처리하는 코드를 추가합니다.

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

        if(resultCode == RESULT_OK)
            when(requestCode){
                REQ_CAMERA -> {
                    realUri?.let { uri ->
                        val bitmap = loadBitmap(uri)
                        binding.imagePreview.setImageBitmap(bitmap)

                        realUri = null
                    }
                }
              
                // 갤러리에서 선택된 이미지를 이미지뷰에 출력
                REQ_STORAGE -> {
                    data?.data?.let{ uri ->
                        binding.imagePreview.setImageURI(uri)
                    }
                }
            }
    }

갤러리에서 선택된 이미지는 이미 저장된 선명한 이미지이기 때문에 세번째 파라미터인 data를 바로 이용합니다.



결과는 다음과 같습니다.

image-20210817173558403



정리


  • 카메라로 촬영한 사진을 이미지뷰에 출력하는 과정은 다음과 같습니다.
    • 권한 명세하기 ➡ 외부 저장소 권한 요청 ➡ 카메라 권한 요청 ➡ 카메라 호출 ➡ 촬영한 이미지를 Uri에 저장 ➡ Uri를 통해 미디어스토어에 있는 Bitmap 이미지 가져오기 ➡ Bitmap을 이용해 이미지뷰에 출력하기
  • 갤러리에서 사진을 가져와 이미지뷰에 출력하는 과정은 다음과 같습니다.
    • (권한 명세하기) ➡ (외부 저장소 권한 요청) ➡ 갤러리 열람 ➡ 선택한 이미지를 Uri를 통해 이미지뷰에 출력하기

Categories:

Updated:

Leave a comment