안드로이드 코틀린 : CameraX, CameraControl - Flash(Torch) On/OFF
Lucy Archive
Lucy / Facilitate4U
2021. 5. 18. 00:59

Android Kotlin : CameraX로 카메라 앱 만들기 - CameraController를 사용하여 플래시(Torch) 켜고 끄기

지난 포스트에서는 CameraX 라이브러리로 사진을 찍는 코드를 소개하였습니다. 이번 포스트는

CameraX 라이브러리와 PreviewView를 사용한 카메라 앱에서 플래시를 켜고 끄는 방법을 소개

합니다.

CameraX 라이브러리로 사진 찍기 앱 만들기

 

안드로이드 코틀린 : 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가 동작하는 것을 확인할 수 있습니다. 앱을 실행하기 전에 권한 설정을 수동으로 해야 합니다. 권한 요청 코드를 추가하면 코드가 길어져 생략했습니다. 

(좌) 권한 수동 설정, (우) Flash On/Off

끝까지 읽어 주셔서 감사합니다.^^

관련포스트

🤞 안드로이드(코틀린) 앱 제작하기 관련글 목록 보기

🤞 안드로이드(코틀린) 카메라 관련글 목록 보기

🤞 안드로이드(코틀린) CameraX 라이브러리 관련글 목록 보기