[Android/Kotlin] 카메라로 찍은 후 Image 불러오기 (Image 파일 저장)

Notepad96

·

2022. 8. 15. 23:51

300x250

 

 

1. 요약

 

결과

 

이 글에서는 카메라를 실행하여 사진을 찍은 후 이미지를 불러와 나타내는 방법에 관하여 기술한다.

 

이를 위해서는 크게 아래의 3가지 기능을 구현할 필요가 있다.

 

  • 카메라 실행 권한 및 파일 Read/Write 할 수 있는 권한을 얻기 위하여 사용자에게 요청
  • 카메라를 실행하고 사진을 찍은 후 해당 Image를 저장
  • 기기의 저장되어 있는 Image 파일의 접근하여 Image 불러오기

 

위 기능들을 구현하기 위해서는 registerForActivityResult를 사용할 것이며 registerForActivityResult는 기존 사용하던 startActivityForResult가 deprecated 되어 그 대안으로 사용 가능한 방법으로서 더욱 간단하게 해당 기능들을 구현할 수 있다.

 

 

 

 

2. 레이아웃

 

2-1. activity_main.xml

메인 레이아웃으로서  Dialog를 실행하는 Button 한 개와 Image를 표시할 ImageView로 구성된다.

 

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    tools:context=".MainActivity">

    <Button
        android:id="@+id/main_btn"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="center"
        android:text="이미지 로드"/>

    <ImageView
        android:id="@+id/main_img"
        android:layout_width="match_parent"
        android:layout_height="match_parent"/>

</LinearLayout>

 


 

2-2. dialog_select_image.xml

Dialog에 대한 레이아웃으로서 카메라 실행 혹은 파일을 불러올 수 있는 동작을 할 수 있도록 2개의 Button으로 구성하였다.

 

 

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:orientation="vertical"
    android:padding="30dp"
    android:gravity="center">

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="추가 방법"
        android:textColor="@color/black"
        android:textSize="20sp"/>
    <View
        android:layout_width="15dp"
        android:layout_height="1dp"
        android:background="#777"
        android:layout_marginVertical="10dp"/>

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:gravity="center">
        <Space
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_weight="1"/>
        <androidx.appcompat.widget.AppCompatButton
            style="@style/Widget.MaterialComponents.Button.TextButton"
            android:id="@+id/dialog_btn_camera"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="카 메 라"/>
        <Space
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_weight="1"/>
        <androidx.appcompat.widget.AppCompatButton
            style="@style/Widget.MaterialComponents.Button.TextButton"
            android:id="@+id/dialog_btn_file"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="파 일"/>
        <Space
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_weight="1"/>
    </LinearLayout>

</LinearLayout>

 

 

 

 

3. 코드 및 설명

 

3-1. MainActivity.kt

 

package com.example.activityresultapi

import android.Manifest
import android.content.ContentValues
import android.content.Context
import android.net.Uri
import android.os.Bundle
import android.provider.MediaStore
import android.widget.Button
import android.widget.Toast
import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatActivity
import com.example.activityresultapi.databinding.ActivityMainBinding
import java.text.SimpleDateFormat
import java.util.*


class MainActivity : AppCompatActivity() {
    private lateinit var binding: ActivityMainBinding

    // 파일 불러오기
    private val getContentImage = registerForActivityResult(ActivityResultContracts.GetContent()) { uri ->
        uri.let { binding.mainImg.setImageURI(uri) }
    }

    // 카메라를 실행한 후 찍은 사진을 저장
    var pictureUri: Uri? = null
    private val getTakePicture = registerForActivityResult(ActivityResultContracts.TakePicture()) {
        if(it) {
            pictureUri.let { binding.mainImg.setImageURI(pictureUri) }
        }
    }

    // 카메라를 실행하며 결과로 비트맵 이미지를 얻음
    private val getTakePicturePreview = registerForActivityResult(ActivityResultContracts.TakePicturePreview()) { bitmap ->
        bitmap.let { binding.mainImg.setImageBitmap(bitmap) }
    }

    // 요청하고자 하는 권한들
    private val permissionList = arrayOf(
        Manifest.permission.CAMERA,
        Manifest.permission.WRITE_EXTERNAL_STORAGE,
        Manifest.permission.READ_EXTERNAL_STORAGE)

    // 권한을 허용하도록 요청
    private val requestMultiplePermission = registerForActivityResult(ActivityResultContracts.RequestMultiplePermissions()) { results ->
        results.forEach {
            if(!it.value) {
                Toast.makeText(applicationContext, "권한 허용 필요", Toast.LENGTH_SHORT).show()
                finish()
            }
        }
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = ActivityMainBinding.inflate(layoutInflater)
        setContentView(binding.root)
        requestMultiplePermission.launch(permissionList)

        binding.mainBtn.setOnClickListener {
            openDialog(this)
        }
    }


    private fun openDialog(context: Context) {
        val dialogLayout = layoutInflater.inflate(R.layout.dialog_select_image, null)
        val dialogBuild = AlertDialog.Builder(context).apply {
            setView(dialogLayout)
        }
        val dialog = dialogBuild.create().apply { show() }

        val cameraAddBtn = dialogLayout.findViewById<Button>(R.id.dialog_btn_camera)
        val fileAddBtn = dialogLayout.findViewById<Button>(R.id.dialog_btn_file)

        cameraAddBtn.setOnClickListener {
            // 1. TakePicture
            pictureUri = createImageFile()
            getTakePicture.launch(pictureUri)   // Require Uri

            // 2. TakePicturePreview
//            getTakePicturePreview.launch(null)    // Bitmap get

            dialog.dismiss()
        }
        fileAddBtn.setOnClickListener {
            getContentImage.launch("image/*")
            dialog.dismiss()
        }
    }

    private fun createImageFile(): Uri? {
        val now = SimpleDateFormat("yyMMdd_HHmmss").format(Date())
        val content = ContentValues().apply {
            put(MediaStore.Images.Media.DISPLAY_NAME, "img_$now.jpg")
            put(MediaStore.Images.Media.MIME_TYPE, "image/jpg")
        }
        return contentResolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, content)
    }

}

 


 

 

● 권한 요청

 

- 요청하고자 하는 권한들을 permissionList란 이름의 리스트로 선언하였다.

 

- registerForActivityResult를 사용하며 인수로 ActivityResultContracts.RequestMultiplePermissions()를 줌으로써 권한을 요청하도록 한다. 

 

결과로 Map 형식의 results를 얻으며 value 값을 확인하여 요청을 허용하였는지 거부하였는지 여부를 판단할 수 있으며 이에 따른 동작 또한 구현할 수 있다.

 

이러한 동작을 requestMultiplePermission 이름으로 선언한 후 onCreate에서 아래와 같이 호출함으로써 애플리케이션 실행 시 권한을 요청하도록 만들 수 있다.

requestMultiplePermission.launch(permissionList)

인수로는 앞서 선언하였던 요청하고자 하는 권한들이 있는 리스트 permissionList를 준다.

 

 

 

 

● Dialog

 

해당 예시에서는 버튼 클릭시 Dialog를 보여주며 Dialog에서 카메라 or 파일 중 선택하여 동작하도록 만들었다. 따라서 Dialog에 관해서 모르는 부분이 있다면 아래 글을 참고하면 될 것 같다.

 

Android Kotlin - AlertDialog(알림창) 기본 및 커스텀

1. 결과 2. Custom Dialog Layout (layout/custom_dialog.xml) <?xml version="1.0" encoding="utf-8"?> 3. MainActivity.kt package com.example.alertdialog import android.content.DialogInterface import a..

notepad96.tistory.com

 

 

 

 

● 카메라 실행 및 파일 Read/Write

 

- 파일 Read/Write 및 카메라 실행과 같은 동작도 마찬가지로 registerForActivityResult 사용하여 구현을 하며, 파일을 불러오기 위해서 GetContent, 카메라를 실행하기 위하여 TakePicture를 이처럼 다른 인수를 넣음으로써 동작을 구현하며, 결과에 따른 동작을 미리 선언해둠으로써 재사용 또한 가능하게 한다.

 

 

- 파일을 불러오기 위해서 fun openDialog서 아래와 같이 선언되어 있으며, Dialog의 "파일" 버튼을 클릭할 경우 앞서 선언한 getContentImage를 사용하여 파일을 불러올 수 있다.

fileAddBtn.setOnClickListener {
    getContentImage.launch("image/*")
    dialog.dismiss()
}

여기서 인수로 "image/*"를 주었는데 이는 파일 형식을 제한하도록하며, 파일을 선택할 때 보면 이미지 파일만 보이는 것을 확인할 수 있다.

 


 

- 카메라를 호출하기 위해서도 전체적인 흐름은 동일하다. registerForActivityResult 사용하여 동작에 맞는 값을 인수로 주며, 그 결과에 따른 동작을 선언한 후 이를 호출하여 동작하도록 한다.

 

 

여기서는 TakePicture와 TakePicturePreview 2가지가 있는데 각각 Uri와 Bitmap으로 다른 결과값을 반환하며 TakePicture는 사진을 파일로 저장 가능하며, TakePicturePreview는 이름 그대로 Previwe로서 찍은 사진을 파일로 저장하지 않고 바로 표시할 수 있다. (위 결과는 TakePicture 사용)

 

단, TakePicturePreview를 사용하여 로드된 이미지를 확인하면 해상도가 매우 깨진 상태로 보여진다. 이를 해결하기 위해서는 추가적으로 이미지 처리를 구현해야 하며, 이를 해결하기 위해서 TakePicture를 사용하여 찍은 사진을 저장한 후 해당 이미지를 불러옴으로써 간단하게 해결 가능하다.

(물론 이미지 파일을 남기지 않도록 만들려면 이미지 처리를 구현해야한다)

 

 

TakePicture를 사용할 경우 이미지를 파일로 저장하기 위해서 아래와 같은 createImageFile 함수를 선언하였으며 SimpleDateFormat을 사용하여 현재 날짜, 시간을 사용하여 파일명을 지정하였다.

 

 

 

 

4. 전체 파일

 

GitHub - Notepad96/BlogExample

Contribute to Notepad96/BlogExample development by creating an account on GitHub.

github.com

 

 

300x250