Android Kotlin : CameraX 카메라 앱 만들기 - 카메라 줌 배율 조절하기
Zoom 기능을 추가하는 방법
에 대해 소개합니다. 이 포스트는 CameraX 관련된 이전 포스트의 연장선상에서 작성하였습니다. CameraX 관련 포스트 리스트는 상단의 태그를 클릭하시거나, 하단의 관련 포스트 링크를 참조해주세요.
Zoom 확대/축소
카메라 줌 확대/축소를 위해 CameraControl, CameraInfo 인터페이스를 사용합니다. 이전 포스트에 CameraControl 와 CameraInfo를 간략히 소개하였으니, 참고해주세요.
CameraControl을 사용해서 CameraX 카메라앱 플래시 제어
배율 설정 하기 : CameraControl
CameraControl은 카메라가 실행되는 동안 카메라 줌을 조정하는 2개의 메서드를 지원합니다. setZoomRatio는 1배, 2배, 5배와 같은 배율로 카메라 줌을 설정하고, setLinearZoom은 0.0(최소) ~ 1.0(최대)사이 값의 선형적인 비율로 카메라 줌을 설정합니다.
setZoomRatio(float ratio)- ratio : 설정하고자 하는 배율
setLinearZoom(float linearZoom)- linearZoom : 0.0 과 1.0 사이의 값
// Obtain Camera, CameraControl Instance
val camera = cameraProvider.bindToLifecycle(this, cameraSelector, preview, imageCapture)
val cameraController = camera.cameraControl
// setZoomRatio
cameraController.setZoomRatio(1F) // 1x Zoom
cameraController.setZoomRatio(2F) // 2x Zoom
cameraController.setZoomRatio(5F) // 2x Zoom
// setLinearZoom
cameraController.setLinearZoom(0F) // Mininum Zoom
cameraController.setLinearZoom(1F) // Maximum Zoom
지원하는 최대/최소 줌 알아보기 : CameraInfo
배율을 설정하기 위해 CameraControl의 2가지 메서드를 사용할 수 있습니다. CameraControl.setLinearZoom() 으로 배율을 설정하게 되면 0.0 ~ 1.0 사이의 값을 입력하면 되지만, CameraControl.setZoomRatio() 를 사용하는 경우 배율을 직접 입력해야 합니다. 여기서 문제가 하나 있습니다. 안드로이드 기기 종류마다 장착되는 카메라 모듈이 다르기 때문에, 지원되는 최대 줌 비율도 다릅니다. 앱이 실행되는 기기에서 지원하는 최대/최소 배율을 알고 싶은 경우 CameraInfo의 ZoomState 인터페이스를 사용 할 수 있습니다.
ZoomState 인스턴스는 CameraInfo.getZoomState() 의 반환값입니다. 이 메서드의 반환 값은 LiveData<ZoomState> 입니다. ZoomState는 아래의 메세드를 지원하지만, 이 반환값은 LiveData이기 때문에 CameraInfo.ZoomState.getMaxZoomRatio() 와 같이 메서드를 바로 사용할 수 없습니다. LiveData 개체를 사용하기 위해 observe() 메서드를 사용하여 LiveData 객체에 Observer 객체를 연결해서 사용할 수 있습니다.
- ZoomState Method
getLinearZoom(): 현재 줌 상태를 0 ~ 1범위로 반환getZoomRatio(): 현재 줌 상태를 배율로 반환getMaxZoomRatio(): 지원하는 최대 줌 배율 반환getMinZoomRatio(): 지원하는 최소 줌 배율 반환
// Obtain Camera, CameraControl Instance
val camera = cameraProvider.bindToLifecycle(this, cameraSelector, preview, imageCapture)
val cameraInfo = camera.cameraInfo
// Get Info from LiveData<zoomState>
lateinit var maxZoomRatio : Float
lateinit var minZoomRatio : Float
cameraInfo!!.zoomState.observe(this, androidx.lifecycle.Observer {
maxZoomRatio = it.maxZoomRatio.toString()
minZoomRatio = it.minZoomRatio.toString()
})
※ cameraInfo!!.zoomState.observe() 메서드를 Fragment 에서 사용하는 경우 this 대신에 viewLifecycleOwner 를 입력하면됩니다.
CameraX 카메라 앱에 적용하기
앱에 적용하기는 이전 포스트에서 코드를 추가한 것으로, 프로젝트 생성 및 환경 설정은 이전 포스트를 참고해주세요.
레이아웃 : activity_main.xml
activity_main.xml 레이아웃 파일에 아래의 코드를 입력 합니다.
<?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">
<Button
android:id="@+id/btnTakePicture"
android:layout_width="100dp"
android:layout_height="100dp"
android:layout_marginBottom="50dp"
android:scaleType="fitCenter"
android:text="Take Photo"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintBottom_toBottomOf="@id/viewFinder"
android:elevation="2dp" />
<androidx.camera.view.PreviewView
android:id="@+id/viewFinder"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_marginBottom="8dp"
app:layout_constraintBottom_toTopOf="@+id/btnTorch"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" >
</androidx.camera.view.PreviewView>
<Button
android:id="@+id/btnTorch"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Torch On"
android:layout_margin="5dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent" />
<Button
android:id="@+id/btn1X"
android:layout_width="60dp"
android:layout_height="wrap_content"
android:layout_margin="5dp"
android:text="1x"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@+id/btn2X" />
<Button
android:id="@+id/btn2X"
android:layout_width="60dp"
android:layout_height="wrap_content"
android:layout_margin="5dp"
android:text="2x"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@+id/btn5X" />
<Button
android:id="@+id/btn5X"
android:layout_width="60dp"
android:layout_height="wrap_content"
android:layout_margin="5dp"
android:text="5x"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@+id/btn10X" />
<Button
android:id="@+id/btn10X"
android:layout_width="60dp"
android:layout_height="wrap_content"
android:layout_margin="5dp"
android:text="10x"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent" />
<TextView
android:id="@+id/txtZoomState"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Hello"
android:textSize="20sp"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"/>
<TextView
android:id="@+id/textView3"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="20dp"
android:text="CameraInfo"
android:textSize="20sp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/textView4"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="MaxZoomRatio : "
android:textSize="20sp"
app:layout_constraintEnd_toStartOf="@+id/txtMaxZoom"
app:layout_constraintTop_toTopOf="@+id/txtMaxZoom" />
<TextView
android:id="@+id/txtMaxZoom"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="20sp"
app:layout_constraintEnd_toEndOf="@+id/textView3"
app:layout_constraintTop_toBottomOf="@+id/textView3" />
<TextView
android:id="@+id/textView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="MinZoomRatio : "
android:textSize="20sp"
app:layout_constraintEnd_toStartOf="@+id/txtMinZoom"
app:layout_constraintTop_toTopOf="@+id/txtMinZoom" />
<TextView
android:id="@+id/txtMinZoom"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="20sp"
app:layout_constraintEnd_toEndOf="@+id/txtMaxZoom"
app:layout_constraintTop_toBottomOf="@+id/txtMaxZoom"/>
</androidx.constraintlayout.widget.ConstraintLayout>
코틀린 코드 : MainActivity.kt
MainActivity.kt 에 아래의 코드를 입력합니다. Zoom 확대/축소 관련 코드는 아래와 같습니다.
- 30~32줄 : 카메라 인터페이스 클래스 내 전역 변수로 선언
- 59~70줄 : 배율 버튼 클릭시 배율 조정하는 코드
- 134~140줄 : 카메라 현재 줌 상태, 최대/최소 줌을 텍스트 뷰에 입력
package com.blacklog.camerax
import android.content.Intent
import android.net.Uri
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.util.Log
import android.widget.Toast
import androidx.camera.core.*
import androidx.camera.lifecycle.ProcessCameraProvider
import androidx.core.content.ContextCompat
import com.blacklog.camerax.databinding.ActivityMainBinding
import java.io.File
import java.text.SimpleDateFormat
import java.util.*
import java.util.concurrent.ExecutorService
import java.util.concurrent.Executors
import androidx.camera.core.Camera as Camera
class MainActivity : AppCompatActivity() {
// ViewBinding
lateinit private var binding : ActivityMainBinding
private var preview : Preview? = null
private var imageCapture: ImageCapture? = null
private lateinit var outputDirectory: File
private lateinit var cameraExecutor: ExecutorService
// CameraController
private var camera : Camera? = null
private var cameraController : CameraControl? = null
private var cameraInfo: CameraInfo? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
val view = binding.root
setContentView(view)
startCamera()
binding.btnTakePicture.setOnClickListener {
takePhoto()
}
outputDirectory = getOutputDirectory()
cameraExecutor = Executors.newSingleThreadExecutor()
binding.btnTorch.setOnClickListener {
when(cameraInfo?.torchState?.value){
TorchState.ON -> {
cameraController?.enableTorch(false)
binding.btnTorch.text = "Torch ON"
}
TorchState.OFF -> {
cameraController?.enableTorch(true)
binding.btnTorch.text = "Torch OFF"
}
}
}
binding.btn1X.setOnClickListener {
cameraController?.setZoomRatio(1F)
}
binding.btn2X.setOnClickListener {
cameraController?.setZoomRatio(2F)
}
binding.btn5X.setOnClickListener {
cameraController?.setZoomRatio(5F)
}
binding.btn10X.setOnClickListener {
cameraController?.setZoomRatio(8F)
}
}
private fun takePhoto() {
// Get a stable reference of the modifiable image capture use case
val imageCapture = imageCapture ?: return
// Create time-stamped output file to hold the image
val photoFile = File(
outputDirectory,
newJpgFileName())
// 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.d("CameraX-Debug", "Photo capture failed: ${exc.message}", exc)
}
override fun onImageSaved(output: ImageCapture.OutputFileResults) {
val savedUri = Uri.fromFile(photoFile)
val msg = "Photo capture succeeded: $savedUri"
Toast.makeText(baseContext, msg, Toast.LENGTH_SHORT).show()
Log.d("CameraX-Debug", msg)
Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE).also{
it.data = savedUri
sendBroadcast(it)
}
}
})
}
// viewFinder 설정 : Preview
private fun startCamera() {
val cameraProviderFuture = ProcessCameraProvider.getInstance(this)
cameraProviderFuture.addListener({
// Used to bind the lifecycle of cameras to the lifecycle owner
val cameraProvider: ProcessCameraProvider = cameraProviderFuture.get()
// Preview
preview = Preview.Builder()
.build()
.also {
it.setSurfaceProvider(binding.viewFinder.surfaceProvider)
}
// ImageCapture
imageCapture = ImageCapture.Builder()
.build()
// Select back camera as a default
val cameraSelector = CameraSelector.DEFAULT_BACK_CAMERA
try {
// Unbind use cases before rebinding
cameraProvider.unbindAll()
// Bind use cases to camera
camera = cameraProvider.bindToLifecycle(
this,
cameraSelector,
preview,
imageCapture)
cameraController = camera!!.cameraControl
cameraInfo = camera!!.cameraInfo
cameraInfo!!.zoomState.observe(this, androidx.lifecycle.Observer {
val currentZoomRatio = it.zoomRatio
Log.d("MyCameraXBasic", currentZoomRatio.toString())
binding.txtZoomState.text = currentZoomRatio.toString()
binding.txtMaxZoom.text = it.maxZoomRatio.toString()
binding.txtMinZoom.text = it.minZoomRatio.toString()
})
} catch(exc: Exception) {
Log.d("CameraX-Debug", "Use case binding failed", exc)
}
}, ContextCompat.getMainExecutor(this))
}
private fun newJpgFileName() : String {
val sdf = SimpleDateFormat("yyyyMMdd_HHmmss", Locale.KOREA)
val filename = sdf.format(System.currentTimeMillis())
return "${filename}.jpg"
}
private fun getOutputDirectory(): File {
val mediaDir = externalMediaDirs.firstOrNull()?.let {
File(it, resources.getString(R.string.app_name)).apply {
if(!this.exists()){
mkdirs()
}
}
}
return if (mediaDir != null && mediaDir.exists()) mediaDir
else filesDir
}
override fun onDestroy() {
super.onDestroy()
cameraExecutor.shutdown()
}
}
실행 결과
위 코드를 실행하면 아래와 같이 지원되는 카메라 배율과, 현재 배율이 표시되는 것을 확인 할 수 있고, 배율 버튼을 누르면 정상적으로 카메라 배율이 조정되는 것을 확인 할 수 있습니다. 참고로 아래 그림에 사용된 폰은 삼성 갤럭시 노트9입니다.
끝까지 읽어 주셔서 감사합니다.^^
'Programming > Android App(Kotlin)' 카테고리의 다른 글
안드로이드 코틀린 : CameraX 카메라 앱 - 수동 초점 Focus 기능 추가하기 (5) | 2021.05.25 |
---|---|
안드로이드 코틀린 : CameraX 카메라 앱 - 타이머 모드 추가하기 (3) | 2021.05.23 |
안드로이드 코틀린 : 쉬운 말로 설명하는 Thread 와 Handler 개념 이해 및 타바타 타이머 예시 (5) | 2021.05.21 |
안드로이드 코틀린 : CameraX, CameraControl - Flash(Torch) On/OFF (3) | 2021.05.18 |
안드로이드 코틀린 : Android Studio 에서 코틀린 언어 연습 하는 방법 (0) | 2021.05.05 |