Android Kotlin : ImageView에imageView에 올바르게 회전 된 Bitmap 삽입하기(with CameraX)
이미지 파일의 메타데이터(Exif)에서 방향(Orientation)을 읽어와서 올바르게 회전된 Bitmap을 ImageView에 넣는 방법
을 소개 합니다. 이 포스트는 CameraX로 카메라를 촬영하는 방법을 사용합니다. 필요하시면 아래의 관련 포스트를 참조해주세요.
사진 회전 하기
사진을 회전 하기 위해 아래 두 가지가 필요합니다.
- 사진파일의 메타데이터에서 방향(Orientation) 정보를 읽어 오기
- 방향 정보에 맞게 사진을 회전하기
이미지 파일에서 Orientation 정보 읽어 오기
이미지 파일에서 회전 정보를 불러오기 위해 ExifInterface 클래스를 사용하여 Exif 메타 데이터를 읽어 옵니다. ExifInterface에서 회전 정보를 읽어 오기 위해 TAG_ORIENTATION 태그를 사용합니다. 참고로, 아래 when(orientation) 에 0, 90, 180, 270 4가지 경우만 정의했지만, getAttribute(ExifInterface.TAG_ORIENTATION)의 결과는 좌우 반전, 상하 반전 등의 추가적인 결과 값도 더 있으니 필요에 따라 코드를 추가 해야 합니다. 참고해주세요.
fun getOrientationOfImage(filepath : String): Int? {
var exif :ExifInterface? = null
var result: Int? = null
try{
exif = ExifInterface(filepath)
}catch (e: Exception){
e.printStackTrace()
return -1
}
val orientation = exif.getAttributeInt(ExifInterface.TAG_ORIENTATION, -1)
if(orientation != -1){
result = when(orientation){
ExifInterface.ORIENTATION_ROTATE_90 -> 90
ExifInterface.ORIENTATION_ROTATE_180 -> 180
ExifInterface.ORIENTATION_ROTATE_270 -> 270
else -> 0
}
}
return result
}
각도 정보를 이용해 올바르게 회전된 Bitmap 을 얻기
비트맵으로 부터 회전된 비트맵을 가져오기 위해 matrix 클래스를 사용합니다. 새로운 bitmap을 생성하기 위해 Bitmap 클래스의 createBitmap() 메서드를 사용하는데, createBitmap() 메서드는 목적에 따라 다양한 인자를 받을 수 있어, 아래 코드에 사용한 매개변수 설명은 여기를 참고하세요.
private fun rotatedBitmap(bitmap: Bitmap?): Bitmap? {
val matrix = Matrix()
var resultBitmap : Bitmap? = null
when(getOrientationOfImage(filepath)){
0 -> matrix.setRotate(0F)
90 -> matrix.setRotate(90F)
180 -> matrix.setRotate(180F)
270 -> matrix.setRotate(270F)
}
resultBitmap = try{
bitmap?.let { Bitmap.createBitmap(it, 0, 0, bitmap.width, bitmap.height, matrix, true) }
}catch (e: Exception){
e.printStackTrace()
null
}
return resultBitmap
}
써먹어 보기
CameraX 라이브러리를 사용하여 사진을 찍은 후, 사진 원본을 미리보기로 보는 예시입니다.
프로젝트 생성
아래와 같이 프로젝트를 생성합니다.
- 템플릿 : Empty Activity
- 프로젝트 이름 : CameraX
- 사용언어 : Kotlin
※ 이 포스트는 Android 4.1.2, Kotlin 1.4.31 버전으로 작성하였습니다.
ViewBinding 설정
App > Gradle Scripts > build.gradle 에 ViewBinding 설정을 위해 아래의 코드를 추가합니다. 코드 추가가 완료되면 Sync Now 를 클랫해서 Gradle 변경 사항을 적용합니다.
android {
...
viewBinding {
enabled = true
}
}
※ ViewBinding은 Layout에 있는 View의 Id를 코틀린 코드에서 직접 사용 할 수 있도록 해주는 도구입니다. View Binding과 관련된 설명은 아래의 링크를 참조해주세요.
권한 설정
아래의 두 가지 권한을 AndroidManifest.xml 에 추가합니다.
- 카메라 : CameraX를 사용한 카메라 사용
- 저장소 : CameraX를 사용해 촬영한 사진을 저장 및 불러오기
<!--Camera-->
<uses-feature android:name="android.hardware.camera.any" />
<uses-permission android:name="android.permission.CAMERA" />
<!--Storage-->
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
액티비티 추가
예시 앱은 2개의 Activity를 사용할 예정입니다. 아래와 같이 CameraX이름의 Activity를 추가합니다.
- MainActivity : 권한 확인, CameraX 액티비티 실행 및 사진 결과 보기
- CameraX(Activity) : CameraX를 사용한 카메라 동작
레이아웃 코드 작성
activity_main.xml 파일의 코드는 아래와 같이 하나의 ImageView와 Button을 추가하였습니다. 각 기능은 아래와 같이 사용합니다.
- Button
- CameraX Activity 를 실행함
- CameraX Activity 실행시 사진을 저장할 경로를 함께 보냄
- ImageView
- CameraX Activity에서 정상적으로 사진 촬영이 완료된 경우 마지막에 촬영한 사진을 보여줌
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<ImageView
android:id="@+id/imageView"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_margin="8dp"
app:layout_constraintBottom_toTopOf="@+id/button"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<Button
android:id="@+id/button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="8dp"
android:text="Open Camera and Get Image"
android:textSize="13sp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
activity_camera_x.xml 파일의 코드는 아래와 같이 작성하였습니다. 카메라 미리보기를 위한 PreView 하나와 사진 찍는 이벤트 처리를 위한 Button 하나를 추가하였습니다.
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/black"
tools:context=".CameraX">
<Button
android:id="@+id/button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:scaleType="fitCenter"
android:text="Take Photo"
android:layout_margin="8dp"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintBottom_toBottomOf="parent" />
<androidx.camera.view.PreviewView
android:id="@+id/preView"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_margin="10dp"
app:layout_constraintBottom_toTopOf="@+id/button"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:scaleType="fitCenter" />
</androidx.constraintlayout.widget.ConstraintLayout>
MainActivity.kt 코드 작성
mainActivity.kt 의 코드는 아래와 같습니다. 코드를 큰 부분으로 나누면 아래와 같습니다. 권한처리 액티비티 호출과 관련된 코드는 이전 포스트들을 참조해주세요.
- 권한처리
- 엑티비티 호출 및 결과 처리
- 엑티비티 호출시 전달할 파일경로 및 파일명 생성
onActivityResult 함수내에서 회전된 Bitmap을 ImageView에 적용 시키기 위해 아래와 같이 코드를 작성하였습니다.
binding.imageView.setImageBitmap(rotatedBitmap(bitmap))
package com.blacklog.camerax
import android.Manifest
import android.app.Activity
import android.content.Intent
import android.content.pm.PackageManager
import android.graphics.Bitmap
import android.graphics.BitmapFactory
import android.graphics.Matrix
import android.media.ExifInterface
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.widget.Toast
import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat
import com.blacklog.camerax.databinding.ActivityMainBinding
import java.io.File
import java.text.SimpleDateFormat
class MainActivity : AppCompatActivity() {
// ViewBinding
private lateinit var binding : ActivityMainBinding
// Permissions
private val PERMISSIONS = arrayOf(
Manifest.permission.CAMERA,
Manifest.permission.READ_EXTERNAL_STORAGE,
Manifest.permission.WRITE_EXTERNAL_STORAGE
)
private val PERMISSIONS_REQ = 100
// Main Program...
private lateinit var filepath : String
private val CAMERAX_REQUEST = 100
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
val view = binding.root
setContentView(view)
binding.button.setOnClickListener {
if(checkPermissions(PERMISSIONS, PERMISSIONS_REQ)){
takePictureAndGetImage()
}
}
}
/* Main Code Start */
private fun takePictureAndGetImage() {
val intent = Intent(this, CameraX::class.java)
filepath = File(getMyExternalMediaDirs(), newJpgFileName()).absolutePath
intent.putExtra("filePath", filepath)
startActivityForResult(intent, CAMERAX_REQUEST)
}
/* Main Code End */
/* Permission Code Start */
private fun checkPermissions(permissions: Array<String>, permissionsRequest: Int): Boolean {
val permissionList : MutableList<String> = mutableListOf()
for(permission in permissions){
val result = ContextCompat.checkSelfPermission(this, permission)
if(result != PackageManager.PERMISSION_GRANTED){
permissionList.add(permission)
}
}
if(permissionList.isNotEmpty()){
ActivityCompat.requestPermissions(this, permissionList.toTypedArray(), permissionsRequest)
return false
}
return true
}
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<out String>, grantResults: IntArray) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
for(result in grantResults){
if(result != PackageManager.PERMISSION_GRANTED){
Toast.makeText(this, "권한 승인 부탁드립니다.", Toast.LENGTH_SHORT).show()
finish()
return
}
}
takePictureAndGetImage()
}
/* Permission Code End */
/* On Activity Result Start */
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
if(resultCode == Activity.RESULT_OK){
when(requestCode){
CAMERAX_REQUEST -> {
binding.imageView.setImageBitmap(BitmapFactory.decodeFile(filepath))
// val bitmap = BitmapFactory.decodeFile(filepath)
// binding.imageView.setImageBitmap(rotatedBitmap(bitmap))
}
}
}
}
/* On Activity Result End */
/* MainActivity Private Method Start */
private fun newJpgFileName(): String {
val sdf = SimpleDateFormat("yyyyMMdd_HHmmss")
val filename = sdf.format(System.currentTimeMillis())
return "${filename}.jpg"
}
private fun getMyExternalMediaDirs(): File {
val mediaDir = externalMediaDirs.firstOrNull()?.let {
File(it, resources.getString(R.string.app_name)).apply {
mkdirs()
}
}
return if (mediaDir != null && mediaDir.exists()) mediaDir
else filesDir
}
private fun rotatedBitmap(bitmap: Bitmap?): Bitmap? {
val matrix = Matrix()
var resultBitmap : Bitmap? = null
when(getOrientationOfImage(filepath)){
0 -> matrix.setRotate(0F)
90 -> matrix.setRotate(90F)
180 -> matrix.setRotate(180F)
270 -> matrix.setRotate(270F)
}
resultBitmap = try{
bitmap?.let { Bitmap.createBitmap(it, 0, 0, bitmap.width, bitmap.height, matrix, true) }
}catch (e: Exception){
e.printStackTrace()
null
}
return resultBitmap
}
private fun getOrientationOfImage(filepath : String): Int? {
var exif :ExifInterface? = null
var result: Int? = null
try{
exif = ExifInterface(filepath)
}catch (e: Exception){
e.printStackTrace()
return -1
}
val orientation = exif.getAttributeInt(ExifInterface.TAG_ORIENTATION, -1)
if(orientation != -1){
result = when(orientation){
ExifInterface.ORIENTATION_ROTATE_90 -> 90
ExifInterface.ORIENTATION_ROTATE_180 -> 180
ExifInterface.ORIENTATION_ROTATE_270 -> 270
else -> 0
}
}
return result
}
/* MainActivity Private Method End */
}
CameraX.kt 코드 작성
CameraX 를 사용하여 사진찍기를 위한 CameraX.kt 의 코드는 아래와 같이 작성하였습니다. CameraX Activity의 버튼을 누르고 정상적으로 사진 찍기가 완료되면 Intent에 Result값으로 RESULT_OK를 입력 후 액티비티를 종료합니다. 액티비티가 종료되면 MainAcivity의 OnActivityResult() 메서드가 호출 됩니다.
package com.blacklog.camerax
import android.app.Activity
import android.content.Intent
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.util.Log
import androidx.camera.core.CameraSelector
import androidx.camera.core.ImageCapture
import androidx.camera.core.ImageCaptureException
import androidx.camera.core.Preview
import androidx.camera.lifecycle.ProcessCameraProvider
import androidx.core.content.ContextCompat
import com.blacklog.camerax.databinding.ActivityCameraXBinding
import java.io.File
class CameraX : AppCompatActivity() {
lateinit var binding : ActivityCameraXBinding
// CameraX
private var preview : Preview? = null
private var imageCapture: ImageCapture? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityCameraXBinding.inflate(layoutInflater)
val view = binding.root
setContentView(view)
startCamera()
val filepath = intent.getStringExtra("filePath")
binding.button.setOnClickListener {
takePicture(filepath!!)
}
}
private fun takePicture(filepath : String) {
val photoFile = File(filepath)
// Create output options object which contains file + metadata
val outputOptions = ImageCapture
.OutputFileOptions
.Builder(photoFile)
.build()
// Set up image capture listener, which is triggered after photo has
// been taken
imageCapture?.takePicture(
outputOptions,
ContextCompat.getMainExecutor(this),
object : ImageCapture.OnImageSavedCallback {
override fun onError(exc: ImageCaptureException) {
Log.e("CameraX_Debug", "Photo capture failed: ${exc.message}", exc)
}
override fun onImageSaved(output: ImageCapture.OutputFileResults) {
val resultIntent = Intent()
setResult(Activity.RESULT_OK, resultIntent)
finish()
}
})
}
private fun startCamera() {
val cameraProviderFuture = ProcessCameraProvider.getInstance(this)
cameraProviderFuture.addListener(Runnable {
// Used to bind the lifecycle of cameras to the lifecycle owner
val cameraProvider: ProcessCameraProvider = cameraProviderFuture.get()
// Preview
preview = Preview.Builder()
.setTargetRotation(windowManager.defaultDisplay.rotation)
.build()
.also {
it.setSurfaceProvider(binding.preView.surfaceProvider)
}
// ImageCapture
imageCapture = ImageCapture.Builder()
.build()
val cameraSelector = CameraSelector.Builder()
.requireLensFacing(CameraSelector.LENS_FACING_BACK)
.build()
try {
// Unbind use cases before rebinding
cameraProvider.unbindAll()
// Bind use cases to camera
cameraProvider.bindToLifecycle(
this,
cameraSelector,
preview,
imageCapture)
} catch(exc: Exception) {
Log.e("CameraX_Debug", "Use case binding failed", exc)
}
}, ContextCompat.getMainExecutor(this))
}
}
실행 결과
위 프로그램에서 회전된 이미지 적용유무 코드에 따라 실행 결과는 아래와 같이 차이가 납니다.
onActivityResult() 콜백 함수에서 회전된 이미지 적용 코드
val bitmap = BitmapFactory.decodeFile(filepath)
binding.imageView.setImageBitmap(rotatedBitmap(bitmap))
onActivityResult() 콜백 함수에서 회전된 이미지 미적용 코드
binding.imageView.setImageBitmap(BitmapFactory.decodeFile(filepath))
끝까지 읽어 주셔서 감사합니다.^^
'Programming > Android App(Kotlin)' 카테고리의 다른 글
안드로이드 코틀린 : CustomView 생성하여 화면 터치 위치 원 그리기 (0) | 2021.04.12 |
---|---|
안드로이드 코틀린 : RecyclerView Item 버튼 이벤트 추가 하기(with ViewBinding) (0) | 2021.04.10 |
안드로이드 코틀린 : 사진의 Exif 데이터 읽기, 쓰기 - ExifInterface (0) | 2021.04.08 |
안드로이드 코틀린 : CameraX 로 사진 촬영하기 (3) | 2021.04.05 |
안드로이드 코틀린 : 일반 클래스 또는 Fragment에서 MainActivity의 메서드 사용하기 (0) | 2021.04.05 |