앞서 이야기 한 적 있는데, 일반 Camera2를 사용하면 기기마다 다 다른 옵션을 설정해야한다.
(이거랑 이어서 보면 죠습니다)
안드로이드와 코틀린이 익숙하지 않은 사람이라면 어려울 수도 있다.
codelabs.developers.google.com/codelabs/camerax-getting-started?hl=ko#5
저도 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")
}
}
}
'Project > 안드로이드 프로젝트(RandomColorChart)' 카테고리의 다른 글
Android Studio, Kotiln] 11. 카메라 셔터st한 깜빡 애니메이션 구현 (2) | 2021.03.12 |
---|---|
Android Studio, Kotlin] 9. 애니메이션을 통해 플로팅 메뉴 만들기 (0) | 2021.03.11 |
Android Studio, Kotiln] 8. 커스텀 토스트 만들기 (0) | 2021.02.18 |
Android Studio, Kotiln] 7. 지정 텍스트 복사 (0) | 2021.02.18 |
Android Studio, Kotlin] 6. (Open Source) Holo Color Picker (0) | 2021.02.17 |