카메라로 촬영한 사진을 이미지뷰에 불러오는 예제입니다.
앱 내부에서는 카메라를 실행할 수 있고, 촬영 후 확인버튼을 누르면 다시 메인 화면에 있는 이미지뷰에 해당 사진을 뿌립니다.
그리고 갤러리에서 불러온 사진을 이미지뷰에 가져올 수 있습니다.
0. 예제를 살펴보기 전에
카메라로 촬영한 사진을 이미지뷰에 띄워준다는 개념을 생각해보아야 합니다.
우선 이 예제에서 구상할 View의 구조는 두개의 버튼과 한개의 이미지뷰입니다.
카메라 실행버튼을 눌렀을 때 카메라가 켜지게 되고 사진을 촬영할 수 있게 됩니다. 셔터를 눌러 사진을 촬영하면 사진이 촬영되고, 카메라에 내장된 "확인"버튼을 누르면 사진이 갤러리에 저장될 것입니다.
이 때 사진의 이름을 특정한 패턴으로 저장해주는 함수가 필요할 것이며, 이 함수에 따라 사진이름이 명명되어 저장됩니다. 사진은 바로 이미지뷰에 출력됩니다.
갤러리 실행 버튼을 눌렀을 때 갤러리가 실행되고 사진을 선택하게 되면 갤러리로부터 사진을 꺼내와서 이미지뷰에 불러옵니다.
이러한 과정은 카메라와 스토리지의 사용 권한을 필요로하며 코드를 통해 구현할 것입니다.
1. 레이아웃 구성을 위한 xml
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity"
>
<ImageView
android:id="@+id/avatars"
android:layout_width="500dp"
android:layout_height="500dp"
tools:srcCompat="@tools:sample/avatars"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintVertical_bias="0.695"
/>
<Button
android:id="@+id/camera"
android:text="camera"
android:layout_width="170dp"
android:layout_height="70dp"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.193"
app:layout_constraintVertical_bias="0.196"
/>
<Button
android:id="@+id/image"
android:text="image"
android:layout_width="170dp"
android:layout_height="70dp"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.795"
app:layout_constraintVertical_bias="0.196"
/>
</androidx.constraintlayout.widget.ConstraintLayout>
0에서 소개한 레이아웃을 구성하기 위해 다음과 같이 activity_main.xml을 작성해 주었습니다.
2. MainActivity 작성
Manifest.xml에 추가할 사항
카메라와 스토리지를 사용하기 위해 <application>
상단에 다음과 같이 추가해 줍니다.
<uses-permission android:name="android.permission.CAMERA"></uses-permission>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"></uses-permission>
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"></uses-permission>
필요 변수 선언과 오버라이드
우선 카메라와 스토리지 권한 처리를 위해 필요한 변수를 선언해 줍시다.
CAMERA
와 STORAGE
는 배열형태로 권한 부여를 받았을 경우 리턴되는 값을 저장합니다.
자세한 사항은 여기를 참조하세요.
// storage 권한 처리에 필요한 변수
val CAMERA = arrayOf(Manifest.permission.CAMERA)
val STORAGE = arrayOf(Manifest.permission.READ_EXTERNAL_STORAGE, Manifest.permission.WRITE_EXTERNAL_STORAGE)
val CAMERA_CODE = 98
val STORAGE_CODE = 99
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
}
}
그리고 두 개의 함수를 재정의해야 합니다. onRequestPermissionsResult
와 onActivityResult
를 오버라이드 합니다.
// storage 권한 처리에 필요한 변수
val CAMERA = arrayOf(Manifest.permission.CAMERA)
val STORAGE = arrayOf(Manifest.permission.READ_EXTERNAL_STORAGE, Manifest.permission.WRITE_EXTERNAL_STORAGE)
val CAMERA_CODE = 98
val STORAGE_CODE = 99
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
}
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<out String>, grantResults: IntArray) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
}
}
함수 onRequestPermissionsResult
는 카메라와 저장소 권한을 취득하기 위한 함수입니다. onActivityResult
는 촬영된 사진이나 불러온 사진에 대해 처리할 작업을 수행한 후 결과를 보여줄 함수입니다.
각각의 함수를 구성하여 보겠습니다.
// storage 권한 처리에 필요한 변수
val CAMERA = arrayOf(Manifest.permission.CAMERA)
val STORAGE = arrayOf(Manifest.permission.READ_EXTERNAL_STORAGE, Manifest.permission.WRITE_EXTERNAL_STORAGE)
val CAMERA_CODE = 98
val STORAGE_CODE = 99
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
}
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<out String>, grantResults: IntArray) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
when(requestCode) {
CAMERA_CODE -> {
for(grant in grantResult) {
if(grant != PackageManager.PERMISSION_GRANTED) {
Toast.makeText(this, "카메라 권한을 승인해 주세요!", Toast.LENGTH_LONG).shot()
}
}
}
STORAGE_CODE -> {
for (grant in grantResults) {
if(grant != PackageManager.PERMISSION_GRANTED) {
Toast.makeText(this, "저장소 권한을 승인해주세요.", Toast.LENGTH_LONG).show()
}
}
}
}
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
}
}
onRequestPermissionsResult
는 requestPermissioned
에 의해 호출되며 권한 요청의 결과를 판단합니다.
함수의 매개변수인 requestCode
에 대해 위에서 선언한 CAMERA_CODE
와 STORAGE_CODE
를 케이스로 지정하여 작업을 처리합니다.
바로 requsetCode
가 퍼미션 체크를 위해 요청을 보낼 코드이며 grantResults
는 요청에 OK했을 때 정보를 담습니다.
grantResults[0]
에는 PackageManager.PERMISSION_GRANTED
값이 들어가게 되며, 요청에 거절당했을 때에는 내부에 아무것도 들어가지 않습니다.
그러므로 grantResults
배열을 돌면서 내부에 PackageManager.PERMISSION_GRANTED
가 없다면 토스트를 띄워서 카메라 권한 승인 요청을 받으라고 알려줍니다.
CAMERA_CODE
와 STORAGE_CODE
는 지정되어 있는 것이므로 각각 98과 99를 사용합니다.
권한 확인을 위한 함수 작성
이제 checkPermission
이라는 이름의 함수를 만들어주겠습니다. 이는 Boolean
값을 반환하여 권한이 부여되었는지 아닌지를 체크하게 해줍니다.
fun checkPermission(permissions: Array<out String>, type: Int):Boolean {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
for (permission in permissions) {
if(ContextCompat.checkSelfPermission(this, permission) != PackageManager.PERMISSION_GRANTED) {
ActivityCompat.requestPermissions(this, permissions, type)
return false
}
}
}
return true
}
여기서 매개변수 permission
은 카메라나 스토리지에 대한 사용 권한을 배열로 저장한 형태를 취합니다. type
에는 CAMERA_CODE
와 같은 접근하기 위해 필요한 숫자가 들어갈 것입니다.
자세한 내용은 여기를 참조해주세요.
안드로이드 SDK버전을 비교해서 6버전보다 상위 버전일 경우 매개변수로 받은 배열을 loop로 순회합니다.
ContextCompat.checkSelfPermission
는 권한을 체크하는데, 권한이 있는 경우 PackageManager.PERMISSION_GRANTED
를 반환하고 그렇지 않은 경우 PackageManager.PERMISSION_DENIED
를 반환합니다.
다시 말해서 ContextCompat.checkSelfPermission
메서드를 통해 권한을 부여받지 못했음을 리턴받은 경우 ActivityCompat.requestPermissions
를 통해 사용자로부터 권한을 받도록 합니다. 이후 false
를 리턴해줍니다.
카메라 촬영을 위해 필요한 함수 작성
이제 카메라 촬영을 하는데 필요한 함수를 작성해보겠습니다.
fun callCamera() {
if(checkPermission(CAMERA, CAMERA_CODE) && checkPermission(STORAGE, STORAGE_CODE)) {
val itt = Intent(MediaStore.ACTION_IMAGE_CAPTURE)
startActivityForResult(itt, CAMERA_CODE)
}
}
위에서 작성한 checkPermission
함수를 사용해서 카메라와 스토리지 모두 true
를 리턴받은 경우에만 Intent
를 통해 MediaStore.ACTION_IMAGE_CAPTURE
사진 촬영 화면으로 이동하게 합니다.
그리고 startActivityForResult
를 통해 카메라 코드를 전달하여 카메라를 띄워주도록 합니다.
여기서 startActivity
와 startActivityForResult
의 차이를 살펴볼 필요가 있습니다.
startActivity
는 단방향으로 새로운 액티비티를 열어주는 역할을 합니다. 인자는 하나만 사용하며, 새로 열어줄 액티비티의 클래스명을 입력해줍니다. startActivityForResult
는 새 액티비티를 열어줌과 동시에 결과값을 전달해주는 역할을 합니다. 따라서 startActivityForResult
는 두개의 인자를 받으며 넘겨줄 페이지를 첫번째 인자로 받으며 두번째 인자로는 requestCode
를 넣어서 다중 액티비티를 사용하는 경우 어떤 액티비티로 전환해 줄 지 결정합니다.
사진을 저장하기 위한 함수
사진을 저장해주기 위한 함수를 작성합니다. ContentValues
라는 클래스를 사용합니다.
This class is used to store a set of values that the ContentResolver can process.
이 클래스는 ContentResolver
가 처리하는 값의 집합입니다. ContentResolver
는 컨텐츠에 대한 엑세스를 제공합니다.
fun saveFile(fileName:String, mimeType:String, bitmap: Bitmap):Uri? {
val CV = ContentValues()
// MediaStore에 파일명, mimeType 지정
CV.put(MediaStore.Images.Media.DISPLAY_NAME, fileName)
CV.put(MediaStore.Images.Media.MIME_TYPE, mimeType)
// 안정성 검사
if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
CV.put(MediaStore.Images.Media.IS_PENDING, 1)
}
// MediaStore에 파일 저장
val uri = contentResolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, CV)
if(uri != null) {
val scriptor = contentResolver.openFileDescriptor(uri, "w")
val fos = FileOutputStream(scriptor?.fileDescriptor)
bitmap.compress(Bitmap.CompressFormat.PNG, 100, fos)
fos.close()
if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
CV.clear()
CV.put(MediaStore.Images.Media.IS_PENDING, 0) // IS_PENDING을 초기화
contentResolver.update(uri, CV, null, null)
}
}
return uri
}
ContentValues
에 사용하는 put
메서드는 다양한 것을 인자로 받을 수 있는데, 여기에서는 put(String key, String value)
형식을 사용합니다.
첫번째 인자는 파일 이름이며 두번째 인자는 데이터 타입입니다. 각각 saveFile
함수의 매개변수를 넣어줍니다.
IS_PENDING
은 저장소의 미디어 파일에 접근하기 위한 것으로 공식문서에서 아래와 같이 설명하고 있습니다.
앱이 미디어 파일에 쓰기 작업을 하는 것과 같이 시간이 많이 소요될 수 있는 작업을 실행한다면 작업을 처리하는 동안 파일에 독점적으로 액세스하는 것이 유용합니다. Android 10 이상을 실행하는 기기에서는 앱이 IS_PENDING 플래그 값을 1로 설정하여 이 독점 액세스 권한을 얻을 수 있습니다. IS_PENDING 값을 다시 0으로 변경할 때까지 이 앱에서만 파일을 볼 수 있습니다.
contentResolver
를 통해 ContentModel
에 접근하여 insert
해주며 첫번째 인자로 삽입할 테이블의 URL을 넣어줍니다. 이 값은 null
일 수 없습니다.
두번째 인자는 새로 삽입되는 초기값이며 null
일 수 있습니다.
이 값이 null
이 아닌 경우 openFileDescriptor
를 통해 파일을 열어줍니다. 여기에는 열고자 하는 파일의 Uri
값과 모드를 넣어줍니다.
모드에 대한 자세한 설명입니다.
Converts a string representing a file mode, such as "rw", into a bitmask suitable for use with open(File, int).
The argument must define at least one of the following base access modes:
- "r" indicates the file should be opened in read-only mode, equivalent to
OsConstants#O\_RDONLY
. - "w" indicates the file should be opened in write-only mode, equivalent to
OsConstants#O\_WRONLY
. - "rw" indicates the file should be opened in read-write mode, equivalent to
OsConstants#O\_RDWR
. - In addition to a base access mode, the following additional modes may requested:
- "a" indicates the file should be opened in append mode, equivalent to
OsConstants#O\_APPEND
. Before each write, the file offset is positioned at the end of the file. - "t" indicates the file should be opened in truncate mode, equivalent to
OsConstants#O\_TRUNC
. If the file already exists and is a regular file and is opened for writing, it will be truncated to length 0.
FileOutputStream
을 통해 파일을 출력해줍니다.
compress
를 통해 비트맵을 압축할 형식을 지정합니다. 여기서는 PNG로 포맷할 것이며 퀄리티는 100으로 주었습니다. 숫자가 높아질수록 화질이 올라갑니다. 두번째 파라미터로 압축할 사진을 지정합니다.
버전 체크를 통해 IS_PENDING
을 0으로 지정하여 초기화해주고 업데이트시켜줍니다.
마지막으로 uri
를 리턴해줍니다.
파일명을 날짜로 지정하는 함수
파일명을 날짜로 지정하는 함수를 작성해주겠습니다.
날짜와 시간으로 지정해줄 것입니다.
fun randomFileName():String {
val fileName= SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(System.currentTimeMillis())
return fileName
}
갤러리의 경로를 취득하는 함수 작성
fun getAlbumDirectory() {
if(checkPermission(STORAGE, STORAGE_CODE)) {
val itt = Intent(Intent.ACTION_PICK)
itt.type = MediaStore.Images.Media.CONTENT_TYPE
startActivityForResult(itt, STORAGE_CODE)
}
}
스토리지의 퍼미션 체크 결과가 true
일 경우 ACTION_PICK
을 통해 데이터로부터 아이템을 꺼내와서 다른 페이지로 넘어갑니다.
넘어가는 페이지 타입은 Uri
로부터 가져온 이미지 컨텐츠입니다.startActivityForResult(itt, STORAGE_CODE)
를 통해 스토리지로 페이지를 전환해줍니다.
오버라이드한 onActivityResult
작성
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
val imageView = findViewById<ImageView>(R.id.avatars)
if(resultCode == Activity.RESULT_OK) {
when(requestCode) {
CAMERA_CODE -> {
if(data?.extras?.get("data") != null) {
val img = data.extras?.get("data") as Bitmap
val uri = saveFile(randomFileName(), "image/png", img)
imageView.setImageURI(uri)
Toast.makeText(this, uri.toString(), Toast.LENGTH_LONG).show()
}
}
STORAGE_CODE -> {
val uri = data?.data
imageView.setImageURI(uri)
}
}
}
}
매개변수로 받는 resultCode
가 OK
일 때 요청 코드가 무엇인지에 따라 작업을 다르게 처리합니다.
CAMERA_CODE
인 경우 매개변수로 받은 Intent
타입의 data
를 비트맵으로 변환시켜주고 이미지파일로 저장시켜 uri
를 가져옵니다.
이미지뷰에 이미지 경로를 통해 사진을 넣어줍니다. 그리고 토스트를 띄워 경로를 확인할 수 있습니다.
STORAGE_CODE
인 경우 Intent
의 데이터를 이미지뷰에 넣어줍니다.
마무리
이제 가장 상단으로 올라와서 onCreate
에 작성한 함수를 사용해주겠습니다.
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
// 카메라
val camera = findViewById<Button>(R.id.camera)
camera.setOnClickListener {
callCamera()
}
// 사진 저장
val picture = findViewById<Button>(R.id.image)
picture.setOnClickListener {
getAlbumDirectory()
}
}
'Study > Android' 카테고리의 다른 글
[Android] Android 12에서 강화된 블루투스 퍼미션 (0) | 2022.05.20 |
---|---|
Android : Google Maps, 구글 지도 사용하기 (0) | 2022.02.16 |
Android : SQLite (0) | 2022.02.16 |
Android : 장면 전환 시 데이터 주고 받기 (0) | 2022.02.16 |
Android : JSON 파싱하기 (0) | 2022.02.16 |