본문 바로가기
Project/안드로이드 프로젝트(RandomColorChart)

Android Studio, Kotlin] 12. CameraX로 사진을 찍고 미리보기를 띄우기

by 김마리님 2021. 3. 15.

앞서 이야기 한 적 있는데, 일반 Camera2를 사용하면 기기마다 다 다른 옵션을 설정해야한다.

(이거랑 이어서 보면 죠습니다)

 

itstudy-mary.tistory.com/359

 

Android Studio, Kotiln] 11. 카메라 셔터st한 깜빡 애니메이션 구현

안드로이드 jetpack cameraX로 카메라를 구현한다. 근데 이 때 좀.. 문제가 있는데, 셔터 애니메이션을 제공하지 않는다(!) 그래서 토스트 등으로 처리해주지 않으면 사진이 찍혔는지 아닌지 전혀 알

itstudy-mary.tistory.com

 

안드로이드와 코틀린이 익숙하지 않은 사람이라면 어려울 수도 있다.

 

codelabs.developers.google.com/codelabs/camerax-getting-started?hl=ko#5

 

Getting Started with CameraX  |  Google Codelabs

This codelab introduces how to create a camera app that uses CameraX to show a viewfinder, take photos and analyze an image stream from the camera.

codelabs.developers.google.com

저도 cameraX codelab를 참조해서 만들었습니다.

 

일단 화면의 구조부터 보자.

 

- activity_camera.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=".camera.CameraActivity">

    <androidx.camera.view.PreviewView
        android:id="@+id/previewView"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />

    <ImageView
        android:id="@+id/imageViewPhoto"
        android:layout_width="60dp"
        android:layout_height="60dp"
        android:layout_marginBottom="40dp"
        android:background="@drawable/bg_round_button_a593e0"
        android:padding="15dp"
        android:src="@drawable/ic_camera_fffff3"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent" />

    <FrameLayout
        android:id="@+id/frameLayoutShutter"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:background="@color/color_1E1E1E"
        android:visibility="gone" />

    <FrameLayout
        android:id="@+id/frameLayoutPreview"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:visibility="gone"
        >

        <ImageView
            android:id="@+id/imageViewPreview"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:background="@color/color_1E1E1E"
             />

...

    </FrameLayout>


</androidx.constraintlayout.widget.ConstraintLayout>

<androidx.camera.view.PreviewView> 부분이 카메라에 비치는 화면의 역할,

 

<ImageView
android:id="@+id/imageViewPhoto"> 부분이 버튼 역할,

 

<ImageView
android:id="@+id/imageViewPreview"> 이 부분이 사진을 찍고 난 후의 화면을 도출할 것이다.

 

 

먼저 이 셔터 로직은 다음과 같다

카메라로 사진을 찍고 -> EXTERNAL_STORAGE에 저장 -> EXTERNAL_STORAGE에 저장된 파일 경로를 URI로 가지고 와서 ImageView에 전사

 

사용하는 매개변수

    private var imageCapture: ImageCapture? = null

    private lateinit var outputDirectory: File
    private lateinit var cameraExecutor: ExecutorService

    private lateinit var cameraAnimationListener: Animation.AnimationListener

    private var savedUri: Uri? = null

 

그렇기 때문에, 카메라와 EXTERNAL_STORAGE를 읽을 권한을 요청한다.

 

    private fun permissionCheck() {

        var permissionList =
            listOf(Manifest.permission.CAMERA, Manifest.permission.READ_EXTERNAL_STORAGE)

        if (!PermissionUtil.checkPermission(this, permissionList)) {
            PermissionUtil.requestPermission(this, permissionList)
        } else {
            openCamera()
        }
    }

 

저는 퍼미션 체크를 유틸로 관리하는 것이 익숙해서 습관적으로 유틸을 만드는데, 유틸은 다음과 같다.

 

- PermissionUtil.kt


object PermissionUtil {

    fun checkPermission(context: Context, permissionList: List<String>): Boolean {
        for (i: Int in permissionList.indices) {
            if (ContextCompat.checkSelfPermission(
                    context,
                    permissionList[i]
                ) == PackageManager.PERMISSION_DENIED
            ) {
                return false
            }
        }

        return true

    }

    fun requestPermission(activity: Activity, permissionList: List<String>){
        ActivityCompat.requestPermissions(activity, permissionList.toTypedArray(), 10)
    }

}

저는 권한 요청할 것이 두개고, 한번만 요청할 것이라 

ActivityCompat.requestPermissions(activity, permissionList.toTypedArray(), 10)

이 줄의 마지막 매개변수 10을 상수로 뒀지만, 만일 독자의 프로젝트에 다양한 퍼미션을 요구한다면 당연히 requestCode 부분을 변수로 조절해야한다.

 

요청한 퍼미션은 작성해둔 activity 코드에서 result를 override해서 받을 수 있다.

    override fun onRequestPermissionsResult(
        requestCode: Int,
        permissions: Array<out String>,
        grantResults: IntArray
    ) {
        super.onRequestPermissionsResult(requestCode, permissions, grantResults)

        if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
            DlogUtil.d(TAG, "승인")
            openCamera()
        } else {
            DlogUtil.d(TAG, "승인 거부")
            onBackPressed()
        }
    }

퍼미션이 모두 승인되면 카메라를 열고, 거부되면 onBackPressed를 통해 이전 화면으로 돌아간다.

 

그리고 외부 저장소 디렉터를 먼저 선언해둔다.

    private fun getOutputDirectory(): 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 openCamera() {
        DlogUtil.d(TAG, "openCamera")

        val cameraProviderFuture = ProcessCameraProvider.getInstance(this)

        cameraProviderFuture.addListener({
            val cameraProvider: ProcessCameraProvider = cameraProviderFuture.get()

            val preview = Preview.Builder()
                .build()
                .also {
                    it.setSurfaceProvider(previewView.surfaceProvider)
                }

            imageCapture = ImageCapture.Builder().build()

            val cameraSelector = CameraSelector.DEFAULT_BACK_CAMERA

            try {

                cameraProvider.unbindAll()
                cameraProvider.bindToLifecycle(this, cameraSelector, preview, imageCapture)
                DlogUtil.d(TAG, "바인딩 성공")

            } catch (e: Exception) {
                DlogUtil.d(TAG, "바인딩 실패 $e")
            }
        }, ContextCompat.getMainExecutor(this))

    }

 

cameraX의 특징은 카메라의 생명주기가 액티비티의 생명주기와 함께한다는 점에 있다. 그래서, 처음에 카메라 프로바이더를 찾고, 그 인스턴스를 찾아 프리뷰의 표면을 찾아, 그 화면 표면에 화면을 띄우도록 빌드한다.

그리고 cameraSelector을 통해 카메라가 전방인지 후방인지를 명기하고, try-catch 구문을 퉁해 프로바이더에 현재 화면과 카메라가 인식하는 화면을 바인딩 시킨다. 그리고 카메라 스레드를 메인 스레드로 옮기면 작업이 끝난다.

 

    private fun setListener() {
        imageViewPhoto.setOnClickListener {
            savePhoto()
        }
    }

 

버튼을 누르면 화면을 저장하고 미리보기를 띄워보자.

    private fun savePhoto() {
        imageCapture = imageCapture ?: return

        val photoFile = File(
            outputDirectory,
            SimpleDateFormat("yy-mm-dd", Locale.US).format(System.currentTimeMillis()) + ".png"
        )
        val outputOption = ImageCapture.OutputFileOptions.Builder(photoFile).build()

        imageCapture?.takePicture(
            outputOption,
            ContextCompat.getMainExecutor(this),
            object : ImageCapture.OnImageSavedCallback {
                override fun onImageSaved(outputFileResults: ImageCapture.OutputFileResults) {
                    savedUri = Uri.fromFile(photoFile)
                    DlogUtil.d(TAG, "savedUri : $savedUri")

                    val animation =
                        AnimationUtils.loadAnimation(this@CameraActivity, R.anim.camera_shutter)
                    animation.setAnimationListener(cameraAnimationListener)
                    frameLayoutShutter.animation = animation
                    frameLayoutShutter.visibility = View.VISIBLE
                    frameLayoutShutter.startAnimation(animation)


                    DlogUtil.d(TAG, "imageCapture")
                }

                override fun onError(exception: ImageCaptureException) {
                    exception.printStackTrace()
                    onBackPressed()
                }

            })

    }

처음에 화면에서 인식된 화면이 있는지 없는지 테스트 하고, 화면에 인식이 없다면 함수를 종료한다.

 

photoFile 부분에서 저장된 파일의 이름과 확장자를 설정하고, 출력된 옵션을 저장해둔다.

 

takePicuture 매서드를 통해 

설정해둔 옵션(이름), 현재 스레드에서 동작하는 화면을 넣고 콜백을 받는다(이건 인터페이스라 object를 통해 콜백 받으셔야 합니다.)

onImageSaved에 들어왔다면 사진이 올바르게 저장된거고,

savedUri에 화면에 저장되는 사진의 Uri가 출력됩니다. 이 값은 전역변수에 저장된다.

onError을 띄운다면 코드를 한번 더 확인해야한다.

 

이 전역변수는 미리 만들어둔 ImageView에 띄울건데, 숨겨놨던 ImageView 영역을 Visible 시키고, 값을 넣으면 된다.

 

    private fun showCaptureImage(): Boolean {
        if (frameLayoutPreview.visibility == View.GONE) {
            frameLayoutPreview.visibility = View.VISIBLE
            imageViewPreview.setImageURI(savedUri)
            return false
        }

        return true

    }

 

 

- 전체 코드 (셔텨 애니메이션 코드는 빠져있습니다.)

더보기
package com.mary.kotlinprojectstudy.camera

import android.Manifest
import android.animation.AnimatorListenerAdapter
import android.content.Context
import android.content.pm.PackageManager
import android.media.Image
import android.net.Uri
import android.os.Bundle
import android.view.MotionEvent
import android.view.View
import android.view.animation.Animation
import android.view.animation.AnimationUtils
import android.widget.FrameLayout
import android.widget.ImageView
import androidx.appcompat.app.AppCompatActivity
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.camera.view.PreviewView
import androidx.core.content.ContextCompat
import com.mary.kotlinprojectstudy.R
import com.mary.kotlinprojectstudy.camera.photo.ColorAverageActivity
import com.mary.kotlinprojectstudy.util.ActivityUtil
import com.mary.kotlinprojectstudy.util.DlogUtil
import com.mary.kotlinprojectstudy.util.PermissionUtil
import java.io.File
import java.lang.Exception
import java.text.SimpleDateFormat
import java.util.*
import java.util.concurrent.Executor
import java.util.concurrent.ExecutorService
import java.util.concurrent.Executors

class CameraActivity : AppCompatActivity() {

    companion object {
        private const val TAG = "CameraActivity"
    }

    private lateinit var previewView: PreviewView
    private lateinit var imageViewPhoto: ImageView
..
    private lateinit var frameLayoutPreview: FrameLayout
    private lateinit var imageViewPreview: ImageView
..

    private var imageCapture: ImageCapture? = null

    private lateinit var outputDirectory: File
    private lateinit var cameraExecutor: ExecutorService

..

    private var savedUri: Uri? = null

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_camera)

        findView()
        permissionCheck()
        setListener()
..

        outputDirectory = getOutputDirectory()
        cameraExecutor = Executors.newSingleThreadExecutor()
    }

    private fun permissionCheck() {

        var permissionList =
            listOf(Manifest.permission.CAMERA, Manifest.permission.READ_EXTERNAL_STORAGE)

        if (!PermissionUtil.checkPermission(this, permissionList)) {
            PermissionUtil.requestPermission(this, permissionList)
        } else {
            openCamera()
        }
    }

    override fun onRequestPermissionsResult(
        requestCode: Int,
        permissions: Array<out String>,
        grantResults: IntArray
    ) {
        super.onRequestPermissionsResult(requestCode, permissions, grantResults)

        if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
            DlogUtil.d(TAG, "승인")
            openCamera()
        } else {
            DlogUtil.d(TAG, "승인 거부")
            onBackPressed()
        }
    }

    private fun findView() {
        previewView = findViewById(R.id.previewView)
        imageViewPhoto = findViewById(R.id.imageViewPhoto)
..
        imageViewPreview = findViewById(R.id.imageViewPreview)
        frameLayoutPreview = findViewById(R.id.frameLayoutPreview)
..)
    }

    private fun setListener() {
        imageViewPhoto.setOnClickListener {
            savePhoto()
        }

...

    }


    private fun getOutputDirectory(): 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 openCamera() {
        DlogUtil.d(TAG, "openCamera")

        val cameraProviderFuture = ProcessCameraProvider.getInstance(this)

        cameraProviderFuture.addListener({
            val cameraProvider: ProcessCameraProvider = cameraProviderFuture.get()

            val preview = Preview.Builder()
                .build()
                .also {
                    it.setSurfaceProvider(previewView.surfaceProvider)
                }

            imageCapture = ImageCapture.Builder().build()

            val cameraSelector = CameraSelector.DEFAULT_BACK_CAMERA

            try {

                cameraProvider.unbindAll()
                cameraProvider.bindToLifecycle(this, cameraSelector, preview, imageCapture)
                DlogUtil.d(TAG, "바인딩 성공")

            } catch (e: Exception) {
                DlogUtil.d(TAG, "바인딩 실패 $e")
            }
        }, ContextCompat.getMainExecutor(this))

    }

    private fun savePhoto() {
        imageCapture = imageCapture ?: return

        val photoFile = File(
            outputDirectory,
            SimpleDateFormat("yy-mm-dd", Locale.US).format(System.currentTimeMillis()) + ".png"
        )
        val outputOption = ImageCapture.OutputFileOptions.Builder(photoFile).build()

        imageCapture?.takePicture(
            outputOption,
            ContextCompat.getMainExecutor(this),
            object : ImageCapture.OnImageSavedCallback {
                override fun onImageSaved(outputFileResults: ImageCapture.OutputFileResults) {
                    savedUri = Uri.fromFile(photoFile)
                    DlogUtil.d(TAG, "savedUri : $savedUri")

...


                    DlogUtil.d(TAG, "imageCapture")
                }

                override fun onError(exception: ImageCaptureException) {
                    exception.printStackTrace()
                    onBackPressed()
                }

            })

    }

    private fun setCameraAnimationListener() {
        cameraAnimationListener = object : Animation.AnimationListener {
            override fun onAnimationStart(animation: Animation?) {
            }

            override fun onAnimationEnd(animation: Animation?) {
...
                showCaptureImage()
            }

            override fun onAnimationRepeat(animation: Animation?) {

            }

        }
    }

    private fun showCaptureImage(): Boolean {
        if (frameLayoutPreview.visibility == View.GONE) {
            frameLayoutPreview.visibility = View.VISIBLE
            imageViewPreview.setImageURI(savedUri)
            return false
        }

        return true

    }

    private fun hideCaptureImage() {
        imageViewPreview.setImageURI(null)
        frameLayoutPreview.visibility = View.GONE

    }

    override fun onBackPressed() {
        if (showCaptureImage()) {
            DlogUtil.d(TAG, "CaptureImage true")
            hideCaptureImage()
        } else {
            onBackPressed()
            DlogUtil.d(TAG, "CaptureImage false")

        }
    }

}
반응형