Android Kotlin : CameraX로 카메라 앱 만들기 - CameraController를 사용하여 플래시(Torch) 켜고 끄기
CameraX 라이브러리와 PreviewView를 사용한 카메라 앱에서 플래시를 켜고 끄는 방법을 소개
합니다.안드로이드 코틀린 : CameraX 로 사진 촬영하기
안드로이드 코틀린 : CameraX 라이브러리로 사진 찍기 CameraX 는 카메라 앱 개발을 쉽게 할 수 있도록 만들어진 Jetpack 지원 라이브러리 입니다. CameraX 라이브러리를 사용하여 Snow 어플과 같은 카메라
juahnpop.tistory.com
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 |