카메라 기능 구현 w cameraX in kotlin
참조: https://www.youtube.com/watch?v=HjXJh_vHXFs
Spring으로 쇼핑몰 구현했을 때도 느꼈지만 파일 업로드/저장은 언어, 프레임 무관하게 어렵다.
게다가 안드로이드는 보안이나 기타 설정 등으로 그 난이도가 제곱은 되는 듯하다.
이것저것 다 시도하다가 가장 디버깅 요소가 적었던 cameraX를 여기에 적어보며 솔직히 아직도 알고리즘이 아니라 프로세스에 대한 이해만 있는 만큼 재차 복습할 것 😬
❓ emulator의 자체 카메라 사용이 아닌 실제 웹캠 사용을 원할 경우 AVD 설정 바꿔줄 것
AVD MANAGER -> 시뮬할 모델 Edit -> Show advanced settings -> Camera (Front: Webcam, Back: Webcam)
1. 환경 설정(+ minSdk 26/ targetSdk 31)
2. CONSTANTS
3. 로직
주안점
- ouputdir 설정
- permissions 여부에 따라 if-else문으로 처리(앱 카메라 비허용일 경우 ActivityCompat의 requestPermissions 호출)
- externalMEdiaDirs.firstOrNull()?.let
- 사진촬영 로직 중 imageCapture nullable 설정
- imageCApture의 takePicture - two functions overriden : onImageSaved, onError
- preview 화면 - processCameraProvider 객체 생성 후 이벤트 처리
- onDestory ... ?
class MainActivity : AppCompatActivity() {
private lateinit var binding: ActivityMainBinding
private var imageCapture: ImageCapture? = null
private lateinit var outputDirectory: File
// private lateinit var cameraExecutor: ExecutorService
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
outputDirectory = getOutputDirectory()
// cameraExecutor = Executors.newSingleThreadExecutor()
if(allPermissionGranted()){
startCamera()
}else{
ActivityCompat.requestPermissions(
this, Constants.REQUIRED_PERMISSIONS,
Constants.REQUEST_CODE_PERMISSIONS
)
}
binding.btnTakePicture.setOnClickListener{
takePhoto()
}
}
private fun getOutputDirectory(): File {
val mediaDir = externalMediaDirs.firstOrNull()?.let { mFile ->
File(mFile, resources.getString(R.string.app_name)).apply {
mkdirs()
}
}
return if(mediaDir != null && mediaDir.exists())
mediaDir else filesDir
}
private fun takePhoto() {
val imageCapture = imageCapture ?: return
val photoFile = File(
outputDirectory,
SimpleDateFormat(Constants.FILE_NAME_FORMAT,
Locale.getDefault()).format(System.currentTimeMillis())+".jpg")
val outputOption = ImageCapture
.OutputFileOptions
.Builder(photoFile)
.build()
imageCapture.takePicture(
outputOption, ContextCompat.getMainExecutor(this),
object :ImageCapture.OnImageSavedCallback{
override fun onImageSaved(outputFileResults: ImageCapture.OutputFileResults) {
val savedUri = Uri.fromFile(photoFile)
val msg = "Photo saved successfully"
Toast.makeText(this@MainActivity,
"$msg $savedUri",
Toast.LENGTH_LONG
).show()
}
override fun onError(exception: ImageCaptureException) {
Log.e(Constants.TAG, "onERROR: ${exception.message}", exception)
}
}
)
}
private fun startCamera(){
val cameraProviderFuture = ProcessCameraProvider.getInstance(this)
cameraProviderFuture.addListener({
val cameraProvider: ProcessCameraProvider = cameraProviderFuture.get()
val preview = Preview.Builder()
.build()
.also { mPreview ->
mPreview.setSurfaceProvider(
binding.viewFinder.surfaceProvider
)
}
imageCapture = ImageCapture.Builder()
.build()
val cameraSelector = CameraSelector.DEFAULT_BACK_CAMERA
try {
cameraProvider.unbindAll()
cameraProvider.bindToLifecycle(
this, cameraSelector, preview, imageCapture
)
}catch (e: Exception){
Log.d(Constants.TAG, "startCamera Fail: ", e)
}
}, ContextCompat.getMainExecutor(this))
}
override fun onRequestPermissionsResult(
requestCode: Int,
permissions: Array<String>,
grantResults: IntArray) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
if(requestCode == Constants.REQUEST_CODE_PERMISSIONS){
if(allPermissionGranted()){
startCamera()
}else{
Toast.makeText(this, "Permissions not granted", Toast.LENGTH_SHORT).show()
finish()
}
}
}
private fun allPermissionGranted() =
Constants.REQUIRED_PERMISSIONS.all {
ContextCompat.checkSelfPermission(
baseContext, it
) == PackageManager.PERMISSION_GRANTED
}
// override fun onDestory(){
// super.onDestroy()
// cameraExecutor.shutdown()
// }
}