Android Kotlin : CameraX로 카메라 앱 만들기 - CameraController를 사용하여 플래시(Torch) 켜고 끄기
CameraX 라이브러리와 PreviewView를 사용한 카메라 앱에서 플래시를 켜고 끄는 방법을 소개
합니다.
CameraX 라이브러리
CameraX 라이브러리는 아래와 같은 특징을 가지는 Jetpack 지원 라이브러리입니다. 상세 정보는 링크를 참조해주세요.
- CameraX는 미리보기, 이미지 분석, 이미지 캡처 등의 use case를 활용해 카메라 앱을 제작할 수 있습니다.
- 추가적으로 인물 사진, HDR, 야간, 뷰티 등의 네이티브 카메라 기능을 편리하게 사용 할 수 있습니다.
- CameraX는 저수준의 기기별 코드를 포함할 필요가 없습니다.
- API 21부터 사용 가능
주요 Interfaces
CameraControl
CameraControl은 아래의 기능들을 카메라가 실행되는 동안 조정할 수 있게 해주는 androidx.camera.core 의 인터페이스입니다.
- Set/Cencal Focus and Metering
- Enable/Disable Torch
- Set ExposureCompensationIndex
- Set Linear/Zoom Ratio
CameraControl로 설정된 값은 지정된 Camera 인스턴스의 (Zoom, Flash....)상태를 변경합니다. 말로 설명하면 어려운데 아래와 같이 사용 가능합니다.
// Obtain Camera, CameraControl Instance
val camera = cameraProvider.bindToLifecycle(this, cameraSelector, preview, imageCapture)
val cameraController = camera.cameraControl
// Torch On
cameraController.enableTorch(true)
// Torch Off
cameraController.enableTorch(false)
CameraInfo
CameraInfo는 아래와 같이 카메라 정보를 불러올 수 있는 메서드를 지원하는 androidx.camera.core 인터페이스입니다.
- getExposureState() : 카메라의 노출 정보
- getSensorRotationDegrees() : 기기의 기울어짐 정보
- hasFlashUnit() : 플래시가 있는지 없는지를 boolean으로 반환
- getTorchState() : 플래시의 상태를 반환
- getZoomState() : 줌의 상태
CameraInfo를 사용해 Torch가 있는지 확인 후 Torch를 On/Off 하는 코드 예시는 아래와 같습니다.
// Obtain Camera, CameraControl Instance
val camera = cameraProvider.bindToLifecycle(this, cameraSelector, preview, imageCapture)
val cameraController = camera.cameraControl
val cameraInfo = camera.cameraInfo
// Torch On/OFF
when(cameraInfo?.torchState?.value){
TorchState.ON -> cameraController.enableTorch(false)
TorchState.OFF -> cameraController.enableTorch(true)
}
CameraX 카메라 앱에 적용
프로젝트 생성
아래와 같이 안드로이드 프로젝트를 생성합니다.
- 템플릿 : Empty Activity
- 프로젝트명 : CameraX
- 사용언어 : Kotlin
※ 이 포스트는 Android 4.1.2, Kotlin 1.4.31 버전으로 작성하였습니다.
환경 설정 : ViewBinding, CameraX 라이브러리, 권한
App > Gradle Scripts > buidl.gradle에 Viewbinding 설정 코드와 CameraX 라이브러리 종속 항목 선언 코드를 추가합니다. CameraX의 최신 버전은 여기를 클릭하여 확인하세요.
android {
...
viewBinding {
enabled = true
}
}
dependencies {
...
// CameraX core library using the camera2 implementation
def camerax_version = "1.0.0-rc03"
// The following line is optional, as the core library is included indirectly by camera-camera2
implementation "androidx.camera:camera-core:${camerax_version}"
implementation "androidx.camera:camera-camera2:${camerax_version}"
// If you want to additionally use the CameraX Lifecycle library
implementation "androidx.camera:camera-lifecycle:${camerax_version}"
// If you want to additionally use the CameraX View class
implementation "androidx.camera:camera-view:1.0.0-alpha22"
// If you want to additionally use the CameraX Extensions library
implementation "androidx.camera:camera-extensions:1.0.0-alpha22"
}
AndroidManifest.xml 파일에서 카메라와 저장소 권한 코드를 추가합니다.
<!--Camera Permission-->
<uses-feature android:name="android.hardware.camera.any" />
<uses-permission android:name="android.permission.CAMERA" />
<!--Storage Permission-->
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
레이아웃 작성
activity_main.xml 레이아웃 파일에 아래와 같이 코드를 작성합니다.
- PreviewView : 카메라 미리보기를 할 수 있는 View
- Button(id : btnTakePicture) : 사진 찍기 버튼
- Button(id: btnTorch) : 플래시 On/Off 버튼
<?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" />
<Button
android:id="@+id/btnTorch"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Torch On"
android:layout_margin="8dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
코틀린 코드 작성
MainActivity.kt에 아래와 같이 코드를 작성합니다. 지난 포스트에서 추가된 주요 코드 설명은 아래와 같습니다.
- 28 ~ 30줄 : MainActivity에서 사용할 수 있는 Camera, CameraControl, CameraInfo 객체 선언
- 45 ~ 56줄 : Torch On/Off 버튼 리스너 코드 추가
- 111 ~ 118줄 : camera, cameraControlller, cameraInfo 설정
- 83 ~ 86줄 : 앱에서 촬영한 사진을 갤러리에서 바로 볼 수 있게 해주는 코드 추가
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.concurrent.ExecutorService
import java.util.concurrent.Executors
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"
}
}
}
}
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
} catch(exc: Exception) {
Log.d("CameraX-Debug", "Use case binding failed", exc)
}
}, ContextCompat.getMainExecutor(this))
}
private fun newJpgFileName() : String {
val sdf = SimpleDateFormat("yyyyMMdd_HHmmss")
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()
}
}
실행 결과
위 코드를 실행하면 아래와 같이 Flash가 동작하는 것을 확인할 수 있습니다. 앱을 실행하기 전에 권한 설정을 수동으로 해야 합니다. 권한 요청 코드를 추가하면 코드가 길어져 생략했습니다.
끝까지 읽어 주셔서 감사합니다.^^
'Programming > Android App(Kotlin)' 카테고리의 다른 글
안드로이드 코틀린 : CameraX 카메라 앱 - Zoom In/Out 배율 확대 축소 조정하기 (0) | 2021.05.22 |
---|---|
안드로이드 코틀린 : 쉬운 말로 설명하는 Thread 와 Handler 개념 이해 및 타바타 타이머 예시 (5) | 2021.05.21 |
안드로이드 코틀린 : Android Studio 에서 코틀린 언어 연습 하는 방법 (0) | 2021.05.05 |
웹에서 코틀린 프로그래밍 언어 연습 및 학습 하는 방법 - try.kotlinlang.org (1) | 2021.05.05 |
안드로이드 코틀린 : 동영상 촬영 후 외부저장소(앱 디렉토리)에 저장하고 미리보기 with FileProvider (3) | 2021.05.04 |