앱에서 Android 5.0(API 수준 21)부터 지원 중단된 원래 Camera
클래스('Camera1')를 사용하는 경우
최신 Android 카메라 API로 업데이트하는 것이 좋습니다. Android는 표준화된 강력한 Jetpack 카메라 API인 CameraX와 하위 수준 프레임워크 API인 Camera2를 제공합니다. 대부분의 경우 앱을 CameraX로 이전하는 것이 좋습니다. 이유는 다음과 같습니다.
- 사용 편의성: CameraX는 하위 수준의 세부정보를 처리하므로, 카메라 환경을 처음부터 빌드하는 데 신경을 덜 쓰고 앱을 차별화하는 데 더 집중할 수 있습니다.
- CameraX가 세분화 자동 처리: CameraX는 장기 유지보수 비용과 기기별 코드를 줄이고 사용자에게 더 높은 품질의 환경을 제공합니다. 자세한 내용은 CameraX를 통한 기기 호환성 향상 블로그 게시물을 참고하세요.
- 고급 기능: CameraX는 고급 기능을 간편하게 앱에 통합할 수 있도록 세심하게 설계되었습니다. CameraX 확장 프로그램을 사용하여 빛망울 효과, 얼굴 보정, HDR(High Dynamic Range), 저조도 야간 캡처 모드 등을 사진에 쉽게 적용할 수 있습니다.
- 업데이트 가능성: Android는 연중 내내 CameraX에 새로운 기능 및 버그 수정을 출시합니다. CameraX로 이전하면 앱이 연간 Android 버전 출시뿐 아니라 CameraX 출시마다 최신 Android 카메라 기술을 활용하게 됩니다.
이 가이드에서는 카메라 애플리케이션의 일반적인 시나리오를 설명합니다. 각 시나리오에는 비교를 위해 Camera1 구현과 CameraX 구현이 포함되어 있습니다.
이전할 때 기존 코드베이스와 통합 시 유연성이 더 필요한 경우가 있습니다. 이 가이드의 모든 CameraX 코드에는 CameraController
구현(예: 가장 간단한 방법으로 CameraX를 사용하려는 경우 적합)과 CameraProvider
구현(유연성이 더 필요할 때 적합)이 있습니다. 적합한 방법을 선택할 때 다음과 같은 장점을 고려하세요.
CameraController |
CameraProvider |
설정 코드가 거의 필요 없음 | 보다 세부적인 제어 가능 |
CameraX에서 더 많은 설정 프로세스를 처리하도록 허용하면 탭하여 초점 맞추기 및 손가락을 모으거나 펼쳐 확대/축소하기 같은 기능이 자동으로 작동합니다. |
앱 개발자가 설정을 처리하므로 예를 들어 출력 이미지 회전을 사용 설정하거나 ImageAnalysis 의 출력 이미지 형식을 설정하는 등 구성을 맞춤설정하는 기회가 더 많습니다.
|
카메라 미리보기에 PreviewView 를 필수로 적용하면 얼굴 경계 상자 같은 ML 모델 결과 좌표를 미리보기 좌표에 직접 매핑할 수 있는 ML Kit 통합에서처럼 CameraX가 원활한 엔드 투 엔드 통합을 제공할 수 있습니다.
|
카메라 미리보기에 맞춤 `Surface`를 사용하는 기능은 앱의 다른 부분에 대한 입력이 될 수 있는 기존 `Surface` 코드를 사용하는 등 더 많은 유연성을 제공합니다. |
이전하는 중 문제가 발생하는 경우 CameraX 토론방에서 문의하세요.
이전하기 전에
CameraX와 Camera1 사용 비교
코드는 달라 보일 수 있지만 Camera1과 CameraX의 기본 개념은 매우 유사합니다. CameraX는 일반적인 카메라 기능을 사용 사례에 추상화하며, 결과적으로 Camera1에서 개발자가 처리해야 했던 많은 작업이 CameraX에서는 자동으로 처리됩니다. CameraX에는 다양한 카메라 작업에 사용할 수 있는 Preview
, ImageCapture
, VideoCapture
, ImageAnalysis
의 네 가지 UseCase
가 있습니다.
개발자를 위해 하위 수준 세부정보를 처리하는 CameraX의 한 가지 예로 활성 UseCase
간에 공유되는 ViewPort
를 들 수 있습니다. 이를 통해 모든 UseCase
의 픽셀이 정확히 동일하게 됩니다.
Camera1에서는 이러한 세부정보를 개발자가 직접 관리해야 하며 기기의 카메라 센서와 화면에서 가로세로 비율의 변동성을 고려할 때 미리보기가 캡처된 사진 및 동영상과 일치하는지 확인하기 어려울 수 있습니다.
또 다른 예로, CameraX는 개발자가 전달한 Lifecycle
콜백을 Lifecycle
인스턴스에서 자동으로 처리합니다. 즉, CameraX는 전체 Android 활동 수명 주기 동안 앱의 카메라 연결을 처리합니다. 여기에는 앱이 백그라운드로 전환될 때 카메라 닫기, 화면에 더 이상 표시할 필요가 없을 때 카메라 미리보기 삭제, 다른 활동(예: 수신되는 영상 통화)이 우선적으로 포그라운드에 표시될 때 카메라 미리보기 일시중지 등과 같은 사례가 포함됩니다.
마지막으로, 개발자가 추가 코드를 작성하지 않고도 CameraX는 회전 및 크기 조정을 처리합니다. 방향이 잠금 해제된 Activity
의 경우 방향 변경 시 시스템이 Activity
를 삭제하고 다시 생성하기 때문에 기기가 회전할 때마다 UseCase
설정이 실행됩니다. 그 결과 UseCases
에서 그때마다 디스플레이 방향과 기본적으로 일치하도록 타겟 회전이 설정됩니다.
CameraX의 회전 자세히 알아보기
자세히 알아보기 전에 CameraX의 UseCase
및 Camera1 앱과의 연관성을 간략하게 살펴보겠습니다. (CameraX 개념은 파란색, Camera1 개념은 녹색입니다.)
CameraX |
|||
CameraController/CameraProvider 구성 | |||
↓ | ↓ | ↓ | ↓ |
미리보기 | ImageCapture | VideoCapture | ImageAnalysis |
⁞ | ⁞ | ⁞ | ⁞ |
미리보기 노출 영역 관리 및 카메라에서 설정 | 카메라에서 PictureCallback 설정 및 takePicture() 호출 | 특정 순서로 카메라 및 MediaRecorder 구성 관리 | 미리보기 노출 영역 위에 빌드된 맞춤 분석 코드 |
↑ | ↑ | ↑ | ↑ |
기기별 코드 | |||
↑ | |||
기기 회전 및 크기 조정 관리 | |||
↑ | |||
카메라 세션 관리(카메라 선택, 수명 주기 관리) | |||
Camera1 |
CameraX의 호환성 및 성능
CameraX는 Android 5.0(API 수준 21) 이상을 실행하는 기기를 지원합니다. 이는 기존 Android 기기의 98%가 넘습니다. CameraX는 기기 간 차이를 자동으로 처리하도록 빌드되어 있어, 앱에 기기별 코드가 필요하지 않습니다. 또한 Google은 CameraX Test Lab에서 5.0 이후 모든 Android 버전을 사용하는 실제 기기를 150대 넘게 테스트합니다. 현재 Test Lab에 있는 기기의 전체 목록을 검토할 수 있습니다.
CameraX는 Executor
를 사용하여 카메라 스택을 구동합니다. 앱에 특정 스레딩 요구사항이 있는 경우 CameraX에 자체 실행자를 설정할 수 있습니다. 설정하지 않으면 CameraX는 최적화된 기본 내부 Executor
를 만들고 사용합니다. CameraX 빌드에 사용되는 대부분의 플랫폼 API는 하드웨어와의 프로세스 간 통신(IPC)을 차단할 것을 요구합니다. 응답하는 데 수백 밀리초가 걸릴 수 있기 때문입니다. 이런 이유로 CameraX는 백그라운드 스레드에서만 이러한 API를 호출하므로 기본 스레드가 차단되지 않고 UI가 유동적으로 유지됩니다.
스레드 자세히 알아보기
앱의 타겟 시장에 저사양 기기가 포함된 경우 CameraX를 사용하면 카메라 리미터로 설정 시간을 줄일 수 있습니다. 하드웨어 구성요소에 연결하는 프로세스는 특히 저사양 기기에서 상당한 시간이 걸릴 수 있으므로, 앱에 필요한 카메라 집합을 지정할 수 있습니다. CameraX는 설정 중에 이러한 카메라에만 연결됩니다. 예를 들어 애플리케이션이 후면 카메라만 사용하는 경우 DEFAULT_BACK_CAMERA
로 이 구성을 설정하면 CameraX가 전면 카메라 초기화를 방지하여 지연 시간이 줄어듭니다.
Android 개발 개념
이 가이드에서는 Android 개발에 관한 일반적인 지식을 가정합니다. 기본사항 외에 본격적으로 코드를 알아보기 전에 다음과 같은 개념을 알면 이해하는 데 도움이 됩니다.
- 뷰 결합은 XML 레이아웃 파일의 결합 클래스를 생성하므로, 개발자는 아래의 코드 스니펫에서 하는 것처럼 활동에서 뷰를 쉽게 참조할 수 있습니다. 뷰 결합과
findViewById()
(이전의 뷰 참조 방식) 간에 일부 차이점이 있지만 아래의 코드에서 뷰 결합 줄을 유사한findViewById()
호출로 바꿀 수 있어야 합니다. - 비동기 코루틴은 Kotlin 1.3에서 추가된 동시 실행 설계 패턴으로,
ListenableFuture
를 반환하는 CameraX 메서드를 처리하는 데 사용할 수 있습니다. 버전 1.1.0부터 Jetpack Concurrent 라이브러리를 사용하면 이 작업이 더 쉬워집니다. 앱에 비동기 코루틴을 추가하려면 다음 단계를 따르세요.
일반적인 시나리오 이전
이 섹션에서는 일반적인 시나리오를 Camera1에서 CameraX로 이전하는 방법을 설명합니다.
각 시나리오에서는 Camera1 구현, CameraX CameraProvider
구현, CameraX CameraController
구현을 다룹니다.
카메라 선택
카메라 애플리케이션에서 가장 먼저 제공해야 할 것은 다른 카메라를 선택하는 방법입니다.
Camera1
Camera1에서는 매개변수 없이 Camera.open()
을 호출하여 첫 번째 후면 카메라를 열거나 열려는 카메라의 정수 ID를 전달할 수 있습니다. 예를 들면 다음과 같습니다.
// Camera1: select a camera from id. // Note: opening the camera is a non-trivial task, and it shouldn't be // called from the main thread, unlike CameraX calls, which can be // on the main thread since CameraX kicks off background threads // internally as needed. private fun safeCameraOpen(id: Int): Boolean { return try { releaseCameraAndPreview() camera = Camera.open(id) true } catch (e: Exception) { Log.e(TAG, "failed to open camera", e) false } } private fun releaseCameraAndPreview() { preview?.setCamera(null) camera?.release() camera = null }
CameraX: CameraController
CameraX에서 카메라 선택은 CameraSelector
클래스에 의해 처리됩니다. CameraX를 사용하면 기본 카메라를 사용하는 일반적인 사례가 쉬워집니다. 기본 전면 카메라와 기본 후면 카메라 중 무엇을 사용할지 지정할 수 있습니다. 또한 CameraX의 CameraControl
객체를 사용하면 손쉽게 앱의 확대/축소 수준을 설정할 수 있으므로 논리 카메라를 지원하는 기기에서 앱이 실행 중이라면 적절한 렌즈로 전환됩니다.
다음은 CameraController
로 기본 후면 카메라를 사용하는 CameraX 코드입니다.
// CameraX: select a camera with CameraController var cameraController = LifecycleCameraController(baseContext) val selector = CameraSelector.Builder() .requireLensFacing(CameraSelector.LENS_FACING_BACK).build() cameraController.cameraSelector = selector
CameraX: CameraProvider
다음은 CameraProvider
로 기본 전면 카메라를 선택하는 예입니다. 전면 또는 후면 카메라를 CameraController
또는 CameraProvider
와 함께 사용할 수 있습니다.
// CameraX: select a camera with CameraProvider. // Use await() within a suspend function to get CameraProvider instance. // For more details on await(), see the "Android development concepts" // section above. private suspend fun startCamera() { val cameraProvider = ProcessCameraProvider.getInstance(this).await() // Set up UseCases (more on UseCases in later scenarios) var useCases:Array= ... // Set the cameraSelector to use the default front-facing (selfie) // camera. val cameraSelector = CameraSelector.DEFAULT_FRONT_CAMERA try { // Unbind UseCases before rebinding. cameraProvider.unbindAll() // Bind UseCases to camera. This function returns a camera // object which can be used to perform operations like zoom, // flash, and focus. var camera = cameraProvider.bindToLifecycle( this, cameraSelector, useCases) } catch(exc: Exception) { Log.e(TAG, "UseCase binding failed", exc) } }) ... // Call startCamera in the setup flow of your app, such as in onViewCreated. override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) ... lifecycleScope.launch { startCamera() } }
어느 카메라가 선택될지 제어하고 싶다면 CameraX에서 가능합니다. CameraProvider
를 사용하는 경우 getAvailableCameraInfos()
를 호출하면 isFocusMeteringSupported()
와 같은 특정 카메라 속성을 확인하는 CameraInfo
객체를 얻을 수 있습니다.
그런 다음 위의 예와 같이 CameraInfo.getCameraSelector()
메서드를 사용하여 CameraSelector
로 변환하여 사용할 수 있습니다.
Camera2CameraInfo
클래스를 사용하여 각 카메라에 관한 세부정보를 가져올 수 있습니다. 원하는 카메라 데이터의 키로 getCameraCharacteristic()
을 호출합니다. 쿼리할 수 있는 모든 키 목록은 CameraCharacteristics
클래스를 확인하세요.
다음은 직접 정의할 수 있는 맞춤 checkFocalLength()
함수를 사용하는 예입니다.
// CameraX: get a cameraSelector for first camera that matches the criteria // defined in checkFocalLength(). val cameraInfo = cameraProvider.getAvailableCameraInfos() .first { cameraInfo -> val focalLengths = Camera2CameraInfo.from(cameraInfo) .getCameraCharacteristic( CameraCharacteristics.LENS_INFO_AVAILABLE_FOCAL_LENGTHS ) return checkFocalLength(focalLengths) } val cameraSelector = cameraInfo.getCameraSelector()
미리보기 표시
대부분의 카메라 애플리케이션은 특정 시점에 카메라 피드를 화면에 표시해야 합니다. Camera1에서는 수명 주기 콜백을 올바르게 관리해야 하며 미리보기의 회전 및 크기 조정도 결정해야 합니다.
또한 Camera1에서는 미리보기 노출 영역으로 TextureView
를 사용할지 아니면 SurfaceView
를 사용할지 결정해야 합니다.
두 옵션 모두 단점은 있지만 어느 경우든 Camera1을 사용할 때는 개발자가 회전과 크기 조정을 올바르게 처리해야 합니다. 반면 CameraX의 PreviewView
에는 TextureView
및 SurfaceView
의 기본 구현이 있습니다.
CameraX는 기기 유형 및 앱을 실행하는 Android 버전과 같은 요소에 따라 가장 적합한 구현을 결정합니다. 두 구현 중 하나가 호환되는 경우 PreviewView.ImplementationMode
를 사용하여 기본 설정을 선언할 수 있습니다.
COMPATIBLE
옵션은 미리보기에 TextureView
를 사용하고, PERFORMANCE
값은 가능한 경우 SurfaceView
를 사용합니다.
Camera1
미리보기를 표시하려면 카메라 하드웨어에서 애플리케이션으로 이미지 데이터를 전달하는 데 사용되는 android.view.SurfaceHolder.Callback
인터페이스의 구현을 사용하여 Preview
클래스를 직접 작성해야 합니다. 그런 다음, 라이브 이미지 미리보기를 시작하려면 먼저 Preview
클래스를 Camera
객체에 전달해야 합니다.
// Camera1: set up a camera preview. class Preview( context: Context, private val camera: Camera ) : SurfaceView(context), SurfaceHolder.Callback { private val holder: SurfaceHolder = holder.apply { addCallback(this@Preview) setType(SurfaceHolder.SURFACE_TYPE_PUSH_BUFFERS) } override fun surfaceCreated(holder: SurfaceHolder) { // The Surface has been created, now tell the camera // where to draw the preview. camera.apply { try { setPreviewDisplay(holder) startPreview() } catch (e: IOException) { Log.d(TAG, "error setting camera preview", e) } } } override fun surfaceDestroyed(holder: SurfaceHolder) { // Take care of releasing the Camera preview in your activity. } override fun surfaceChanged(holder: SurfaceHolder, format: Int, w: Int, h: Int) { // If your preview can change or rotate, take care of those // events here. Make sure to stop the preview before resizing // or reformatting it. if (holder.surface == null) { return // The preview surface does not exist. } // Stop preview before making changes. try { camera.stopPreview() } catch (e: Exception) { // Tried to stop a non-existent preview; nothing to do. } // Set preview size and make any resize, rotate or // reformatting changes here. // Start preview with new settings. camera.apply { try { setPreviewDisplay(holder) startPreview() } catch (e: Exception) { Log.d(TAG, "error starting camera preview", e) } } } } class CameraActivity : AppCompatActivity() { private lateinit var viewBinding: ActivityMainBinding private var camera: Camera? = null private var preview: Preview? = null override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) viewBinding = ActivityMainBinding.inflate(layoutInflater) setContentView(viewBinding.root) // Create an instance of Camera. camera = getCameraInstance() preview = camera?.let { // Create the Preview view. Preview(this, it) } // Set the Preview view as the content of the activity. val cameraPreview: FrameLayout = viewBinding.cameraPreview cameraPreview.addView(preview) } }
CameraX: CameraController
CameraX에서는 개발자가 관리할 사항이 훨씬 적습니다. CameraController
를 사용하는 경우 PreviewView
도 사용해야 합니다. 즉, Preview
UseCase
가 함축되어 있으므로 설정 작업이 훨씬 줄어듭니다.
// CameraX: set up a camera preview with a CameraController. class MainActivity : AppCompatActivity() { private lateinit var viewBinding: ActivityMainBinding override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) viewBinding = ActivityMainBinding.inflate(layoutInflater) setContentView(viewBinding.root) // Create the CameraController and set it on the previewView. var cameraController = LifecycleCameraController(baseContext) cameraController.bindToLifecycle(this) val previewView: PreviewView = viewBinding.cameraPreview previewView.controller = cameraController } }
CameraX: CameraProvider
CameraX의 CameraProvider
를 사용하면 PreviewView
를 사용할 필요가 없지만 Camera1보다 미리보기 설정이 크게 간소화됩니다. 시연을 위해 이 예에서는 PreviewView
를 사용하지만, 더 복잡한 요구사항이 있는 경우 맞춤 SurfaceProvider
를 작성하여 setSurfaceProvider()
에 전달할 수 있습니다.
여기서는 Preview
UseCase
가 CameraController
의 경우처럼 함축되어 있지 않으므로 설정해야 합니다.
// CameraX: set up a camera preview with a CameraProvider. // Use await() within a suspend function to get CameraProvider instance. // For more details on await(), see the "Android development concepts" // section above. private suspend fun startCamera() { val cameraProvider = ProcessCameraProvider.getInstance(this).await() // Create Preview UseCase. val preview = Preview.Builder() .build() .also { it.setSurfaceProvider( viewBinding.viewFinder.surfaceProvider ) } // Select default back camera. val cameraSelector = CameraSelector.DEFAULT_BACK_CAMERA try { // Unbind UseCases before rebinding. cameraProvider.unbindAll() // Bind UseCases to camera. This function returns a camera // object which can be used to perform operations like zoom, // flash, and focus. var camera = cameraProvider.bindToLifecycle( this, cameraSelector, useCases) } catch(exc: Exception) { Log.e(TAG, "UseCase binding failed", exc) } }) ... // Call startCamera() in the setup flow of your app, such as in onViewCreated. override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) ... lifecycleScope.launch { startCamera() } }
탭하여 초점 맞추기
카메라 미리보기가 화면에 표시될 때 일반적인 컨트롤은 사용자가 미리보기를 탭하는 경우의 포커스 포인트를 설정하는 것입니다.
Camera1
Camera1에서 탭하여 초점 맞추기를 구현하려면 최적의 포커스 Area
을 계산하여 Camera
가 포커스를 두어야 하는 위치를 나타내야 합니다. 이 Area
는 setFocusAreas()
에 전달됩니다. 또한 Camera
에서 호환되는 포커스 모드를 설정해야 합니다. 현재 포커스 모드가 FOCUS_MODE_AUTO
, FOCUS_MODE_MACRO
, FOCUS_MODE_CONTINUOUS_VIDEO
또는 FOCUS_MODE_CONTINUOUS_PICTURE
인 경우에만 포커스 영역이 효과가 있습니다.
각 Area
는 지정된 가중치가 있는 직사각형입니다. 가중치는 1과 1,000 사이의 값이며 포커스 Areas
가 여러 개 설정된 경우 우선순위를 지정하는 데 사용됩니다. 이 예에서는 하나의 Area
만 사용하므로 가중치 값은 중요하지 않습니다. 직사각형의 좌표 범위는 -1000부터 1000까지입니다. 왼쪽 상단 지점은 (-1000, -1000)이고
오른쪽 하단 지점은 (1000, 1000)입니다. 방향은 센서 방향, 즉 센서에 감지되는 방향을 기준으로 합니다. 방향은 Camera.setDisplayOrientation()
의 회전 또는 미러링에 영향을 받지 않으므로, 터치 이벤트 좌표를 센서 좌표로 변환해야 합니다.
// Camera1: implement tap-to-focus. class TapToFocusHandler : Camera.AutoFocusCallback { private fun handleFocus(event: MotionEvent) { val camera = camera ?: return val parameters = try { camera.getParameters() } catch (e: RuntimeException) { return } // Cancel previous auto-focus function, if one was in progress. camera.cancelAutoFocus() // Create focus Area. val rect = calculateFocusAreaCoordinates(event.x, event.y) val weight = 1 // This value's not important since there's only 1 Area. val focusArea = Camera.Area(rect, weight) // Set the focus parameters. parameters.setFocusMode(Parameters.FOCUS_MODE_AUTO) parameters.setFocusAreas(listOf(focusArea)) // Set the parameters back on the camera and initiate auto-focus. camera.setParameters(parameters) camera.autoFocus(this) } private fun calculateFocusAreaCoordinates(x: Int, y: Int) { // Define the size of the Area to be returned. This value // should be optimized for your app. val focusAreaSize = 100 // You must define functions to rotate and scale the x and y values to // be values between 0 and 1, where (0, 0) is the upper left-hand side // of the preview, and (1, 1) is the lower right-hand side. val normalizedX = (rotateAndScaleX(x) - 0.5) * 2000 val normalizedY = (rotateAndScaleY(y) - 0.5) * 2000 // Calculate the values for left, top, right, and bottom of the Rect to // be returned. If the Rect would extend beyond the allowed values of // (-1000, -1000, 1000, 1000), then crop the values to fit inside of // that boundary. val left = max(normalizedX - (focusAreaSize / 2), -1000) val top = max(normalizedY - (focusAreaSize / 2), -1000) val right = min(left + focusAreaSize, 1000) val bottom = min(top + focusAreaSize, 1000) return Rect(left, top, left + focusAreaSize, top + focusAreaSize) } override fun onAutoFocus(focused: Boolean, camera: Camera) { if (!focused) { Log.d(TAG, "tap-to-focus failed") } } }
CameraX: CameraController
CameraController
는 PreviewView
의 터치 이벤트를 수신 대기하여 '탭하여 초점 맞추기'를 자동으로 처리합니다. setTapToFocusEnabled()
로 탭하여 초점 맞추기를 사용 설정하거나 사용 중지하고 해당하는 getter isTapToFocusEnabled()
로 값을 확인합니다.
getTapToFocusState()
메서드는 CameraController
의 포커스 상태 변경사항을 추적하기 위한 LiveData
객체를 반환합니다.
// CameraX: track the state of tap-to-focus over the Lifecycle of a PreviewView, // with handlers you can define for focused, not focused, and failed states. val tapToFocusStateObserver = Observer{ state -> when (state) { CameraController.TAP_TO_FOCUS_NOT_STARTED -> Log.d(TAG, "tap-to-focus init") CameraController.TAP_TO_FOCUS_STARTED -> Log.d(TAG, "tap-to-focus started") CameraController.TAP_TO_FOCUS_FOCUSED -> Log.d(TAG, "tap-to-focus finished (focus successful)") CameraController.TAP_TO_FOCUS_NOT_FOCUSED -> Log.d(TAG, "tap-to-focus finished (focused unsuccessful)") CameraController.TAP_TO_FOCUS_FAILED -> Log.d(TAG, "tap-to-focus failed") } } cameraController.getTapToFocusState().observe(this, tapToFocusStateObserver)
CameraX: CameraProvider
CameraProvider
를 사용할 때는 탭하여 초점 맞추기를 작동시키기 위해 몇 가지 설정이 필요합니다. 이 예에서는 PreviewView
를 사용한다고 가정합니다. 그렇지 않으면 맞춤 Surface
에 적용할 로직을 조정해야 합니다.
PreviewView
를 사용하는 단계는 다음과 같습니다.
- 탭 이벤트를 처리하도록 동작 감지기를 설정합니다.
- 탭 이벤트에 관해
MeteringPointFactory.createPoint()
를 사용하여MeteringPoint
를 만듭니다. MeteringPoint
를 사용하여FocusMeteringAction
을 만듭니다.Camera
에서CameraControl
객체(bindToLifecycle()
에서 반환됨)를 사용하여startFocusAndMetering()
을 호출하고FocusMeteringAction
을 전달합니다.- (선택사항)
FocusMeteringResult
에 응답합니다. PreviewView.setOnTouchListener()
에서 터치 이벤트에 응답하도록 동작 감지기를 설정합니다.
// CameraX: implement tap-to-focus with CameraProvider. // Define a gesture detector to respond to tap events and call // startFocusAndMetering on CameraControl. If you want to use a // coroutine with await() to check the result of focusing, see the // "Android development concepts" section above. val gestureDetector = GestureDetectorCompat(context, object : SimpleOnGestureListener() { override fun onSingleTapUp(e: MotionEvent): Boolean { val previewView = previewView ?: return val camera = camera ?: return val meteringPointFactory = previewView.meteringPointFactory val focusPoint = meteringPointFactory.createPoint(e.x, e.y) val meteringAction = FocusMeteringAction .Builder(meteringPoint).build() lifecycleScope.launch { val focusResult = camera.cameraControl .startFocusAndMetering(meteringAction).await() if (!result.isFocusSuccessful()) { Log.d(TAG, "tap-to-focus failed") } } } } ) ... // Set the gestureDetector in a touch listener on the PreviewView. previewView.setOnTouchListener { _, event -> // See pinch-to-zooom scenario for scaleGestureDetector definition. var didConsume = scaleGestureDetector.onTouchEvent(event) if (!scaleGestureDetector.isInProgress) { didConsume = gestureDetector.onTouchEvent(event) } didConsume }
손가락을 모으거나 펼쳐 확대/축소
미리보기 확대/축소는 카메라 미리보기의 또 다른 일반적인 직접 조작입니다. 기기의 카메라 수가 증가함에 따라 사용자는 확대/축소의 결과로 초점 거리가 최적인 렌즈가 자동으로 선택될 것을 기대합니다.
Camera1
Camera1을 사용하여 확대/축소하는 데는 두 가지 방법이 있습니다. Camera.startSmoothZoom()
메서드는 현재 확대/축소 수준에서 개발자가 전달한 확대/축소 수준으로 애니메이션 처리합니다. Camera.Parameters.setZoom()
메서드는 개발자가 전달한 확대/축소 수준으로 바로 이동합니다. 두 방법 중 하나를 사용하기 전에 각각 isSmoothZoomSupported()
또는 isZoomSupported()
를 호출하여 필요한 관련 확대/축소 메서드를 카메라에서 사용할 수 있도록 합니다.
손가락을 모으거나 펼쳐 확대/축소하기를 구현하기 위해 이 예에서는 setZoom()
을 사용합니다. 미리보기 노출 영역의 터치 리스너가 손가락 모으기 동작이 발생할 때 이벤트를 계속 발생시키므로 확대/축소 수준이 매번 즉시 업데이트되기 때문입니다. ZoomTouchListener
클래스는 아래에 정의되어 있으며 미리보기 노출 영역의 터치 리스너에 대한 콜백으로 설정해야 합니다.
// Camera1: implement pinch-to-zoom. // Define a scale gesture detector to respond to pinch events and call // setZoom on Camera.Parameters. val scaleGestureDetector = ScaleGestureDetector(context, object : ScaleGestureDetector.OnScaleGestureListener { override fun onScale(detector: ScaleGestureDetector): Boolean { val camera = camera ?: return false val parameters = try { camera.parameters } catch (e: RuntimeException) { return false } // In case there is any focus happening, stop it. camera.cancelAutoFocus() // Set the zoom level on the Camera.Parameters, and set // the Parameters back onto the Camera. val currentZoom = parameters.zoom parameters.setZoom(detector.scaleFactor * currentZoom) camera.setParameters(parameters) return true } } ) // Define a View.OnTouchListener to attach to your preview view. class ZoomTouchListener : View.OnTouchListener { override fun onTouch(v: View, event: MotionEvent): Boolean = scaleGestureDetector.onTouchEvent(event) } // Set a ZoomTouchListener to handle touch events on your preview view // if zoom is supported by the current camera. if (camera.getParameters().isZoomSupported()) { view.setOnTouchListener(ZoomTouchListener()) }
CameraX: CameraController
탭하여 초점 맞추기와 마찬가지로 CameraController
는 PreviewView의 터치 이벤트를 수신 대기하여 '손가락을 모으거나 펼쳐 확대/축소'를 자동으로 처리합니다. setPinchToZoomEnabled()
로 탭하여 손가락을 모으거나 펼쳐 확대/축소를 사용 설정하거나 사용 중지하고 해당하는 getter isPinchToZoomEnabled()
로 값을 확인합니다.
getZoomState()
메서드는 CameraController
에서 ZoomState
변경사항을 추적하기 위해 LiveData
객체를 반환합니다.
// CameraX: track the state of pinch-to-zoom over the Lifecycle of // a PreviewView, logging the linear zoom ratio. val pinchToZoomStateObserver = Observer{ state -> val zoomRatio = state.getZoomRatio() Log.d(TAG, "ptz-zoom-ratio $zoomRatio") } cameraController.getZoomState().observe(this, pinchToZoomStateObserver)
CameraX: CameraProvider
CameraProvider
에서 손가락을 모으거나 펼쳐 확대/축소하려면 일부 설정이 필요합니다. PreviewView
를 사용하지 않는 경우에는 맞춤 Surface
에 적용할 로직을 조정해야 합니다.
PreviewView
를 사용하는 단계는 다음과 같습니다.
- 손가락 모으기 이벤트를 처리하도록 크기 조정 동작 감지기를 설정합니다.
bindToLifecycle()
을 호출할 때Camera
인스턴스가 반환되는Camera.CameraInfo
객체에서ZoomState
를 가져옵니다.ZoomState
에zoomRatio
값이 있으면 이 값을 현재 확대/축소 비율로 저장합니다.ZoomState
에zoomRatio
가 없으면 카메라의 기본 확대/축소 비율(1.0)을 사용합니다.- 현재 확대/축소 비율과
scaleFactor
의 곱을 구하여 새 확대/축소 비율을 결정하고 이 값을CameraControl.setZoomRatio()
에 전달합니다. PreviewView.setOnTouchListener()
에서 터치 이벤트에 응답하도록 동작 감지기를 설정합니다.
// CameraX: implement pinch-to-zoom with CameraProvider. // Define a scale gesture detector to respond to pinch events and call // setZoomRatio on CameraControl. val scaleGestureDetector = ScaleGestureDetector(context, object : SimpleOnGestureListener() { override fun onScale(detector: ScaleGestureDetector): Boolean { val camera = camera ?: return val zoomState = camera.cameraInfo.zoomState val currentZoomRatio: Float = zoomState.value?.zoomRatio ?: 1f camera.cameraControl.setZoomRatio( detector.scaleFactor * currentZoomRatio ) } } ) ... // Set the scaleGestureDetector in a touch listener on the PreviewView. previewView.setOnTouchListener { _, event -> var didConsume = scaleGestureDetector.onTouchEvent(event) if (!scaleGestureDetector.isInProgress) { // See pinch-to-zooom scenario for gestureDetector definition. didConsume = gestureDetector.onTouchEvent(event) } didConsume }
사진 촬영
이 섹션에서는 촬영 버튼을 눌렀을 때 타이머가 경과한 후 또는 선택한 기타 이벤트 발생 시 필요에 따라 사진 캡처를 트리거하는 방법을 설명합니다.
Camera1
Camera1에서는 먼저, 요청 시 사진 데이터를 관리하도록 Camera.PictureCallback
을 정의합니다. 다음은 JPEG 이미지 데이터를 처리하는 PictureCallback
의 간단한 예입니다.
// Camera1: define a Camera.PictureCallback to handle JPEG data. private val picture = Camera.PictureCallback { data, _ -> val pictureFile: File = getOutputMediaFile(MEDIA_TYPE_IMAGE) ?: run { Log.d(TAG, "error creating media file, check storage permissions") return@PictureCallback } try { val fos = FileOutputStream(pictureFile) fos.write(data) fos.close() } catch (e: FileNotFoundException) { Log.d(TAG, "file not found", e) } catch (e: IOException) { Log.d(TAG, "error accessing file", e) } }
그런 다음, 사진을 촬영할 때마다 Camera
인스턴스에서 takePicture()
메서드를 호출합니다. 이 takePicture()
메서드에는 데이터 유형에 따라 세 가지 매개변수가 있습니다. 첫 번째 매개변수는 이 예에서는 정의되지 않은 ShutterCallback
용입니다. 두 번째 매개변수는 PictureCallback
이 원시(비압축) 카메라 데이터를 처리하기 위한 것입니다. 세 번째 매개변수는 이 예에서 사용하는 매개변수로, JPEG 이미지 데이터를 처리하기 위한 PictureCallback
입니다.
// Camera1: call takePicture on Camera instance, passing our PictureCallback. camera?.takePicture(null, null, picture)
CameraX: CameraController
CameraX의 CameraController
는 자체 takePicture()
메서드를 구현하여 이미지 캡처를 위한 Camera1의 단순성을 유지합니다. 여기서 MediaStore
항목을 구성하고 여기에 저장할 사진을 촬영하는 함수를 정의합니다.
// CameraX: define a function that uses CameraController to take a photo. private val FILENAME_FORMAT = "yyyy-MM-dd-HH-mm-ss-SSS" private fun takePhoto() { // Create time stamped name and MediaStore entry. val name = SimpleDateFormat(FILENAME_FORMAT, Locale.US) .format(System.currentTimeMillis()) val contentValues = ContentValues().apply { put(MediaStore.MediaColumns.DISPLAY_NAME, name) put(MediaStore.MediaColumns.MIME_TYPE, "image/jpeg") if(Build.VERSION.SDK_INT > Build.VERSION_CODES.P) { put(MediaStore.Images.Media.RELATIVE_PATH, "Pictures/CameraX-Image") } } // Create output options object which contains file + metadata. val outputOptions = ImageCapture.OutputFileOptions .Builder(context.getContentResolver(), MediaStore.Images.Media.EXTERNAL_CONTENT_URI, contentValues) .build() // Set up image capture listener, which is triggered after photo has // been taken. cameraController.takePicture( outputOptions, ContextCompat.getMainExecutor(this), object : ImageCapture.OnImageSavedCallback { override fun onError(e: ImageCaptureException) { Log.e(TAG, "photo capture failed", e) } override fun onImageSaved( output: ImageCapture.OutputFileResults ) { val msg = "Photo capture succeeded: ${output.savedUri}" Toast.makeText(baseContext, msg, Toast.LENGTH_SHORT).show() Log.d(TAG, msg) } } ) }
CameraX: CameraProvider
CameraProvider
를 사용한 사진 촬영은 CameraController
와 거의 같은 방식으로 작동하지만, 먼저 takePicture()
를 호출할 객체가 있도록 ImageCapture
UseCase
를 만들고 결합해야 합니다.
// CameraX: create and bind an ImageCapture UseCase. // Make a reference to the ImageCapture UseCase at a scope that can be accessed // throughout the camera logic in your app. private var imageCapture: ImageCapture? = null ... // Create an ImageCapture instance (can be added with other // UseCase definitions). imageCapture = ImageCapture.Builder().build() ... // Bind UseCases to camera (adding imageCapture along with preview here, but // preview is not required to use imageCapture). This function returns a camera // object which can be used to perform operations like zoom, flash, and focus. var camera = cameraProvider.bindToLifecycle( this, cameraSelector, preview, imageCapture)
그런 다음, 사진을 캡처할 때마다 ImageCapture.takePicture()
를 호출할 수 있습니다. takePhoto()
함수의 전체 예는 이 섹션의 CameraController
코드를 참고하세요.
// CameraX: define a function that uses CameraController to take a photo. private fun takePhoto() { // Get a stable reference of the modifiable ImageCapture UseCase. val imageCapture = imageCapture ?: return ... // Call takePicture on imageCapture instance. imageCapture.takePicture( ... ) }
동영상 녹화
동영상 녹화는 지금까지 살펴본 시나리오보다 훨씬 더 복잡합니다. 프로세스의 각 부분은 일반적으로 특정 순서로 올바르게 설정되어야 합니다. 또한 동영상 및 오디오가 동기화되어 있는지 확인하거나 추가 기기 불일치를 처리해야 할 수도 있습니다.
앞으로 살펴보겠지만, CameraX는 이러한 다양한 복잡성을 자동으로 처리합니다.
Camera1
Camera1을 사용하여 동영상을 캡처하려면 Camera
및 MediaRecorder
를 신중하게 관리해야 하며 메서드를 특정 순서로 호출해야 합니다. 이러한 순서를 반드시 따라야 애플리케이션이 제대로 작동합니다.
- 카메라를 엽니다.
- 미리보기를 준비하고 시작합니다(앱에서 녹화 중인 동영상이 표시되는 경우).
Camera.unlock()
을 호출하여MediaRecorder
에서 사용하도록 카메라를 잠금 해제합니다.MediaRecorder
에서 다음 메서드를 호출하여 녹화를 구성합니다.Camera
인스턴스를setCamera(camera)
와 연결합니다.setAudioSource(MediaRecorder.AudioSource.CAMCORDER)
를 호출합니다.setVideoSource(MediaRecorder.VideoSource.CAMERA)
를 호출합니다.setProfile(CamcorderProfile.get(CamcorderProfile.QUALITY_1080P))
를 호출하여 화질을 설정합니다. 모든 화질 옵션은CamcorderProfile
을 참고하세요.setOutputFile(getOutputMediaFile(MEDIA_TYPE_VIDEO).toString())
을 호출합니다.- 앱에 동영상 미리보기가 있으면
setPreviewDisplay(preview?.holder?.surface)
를 호출합니다. setOutputFormat(MediaRecorder.OutputFormat.MPEG_4)
을 호출합니다.setAudioEncoder(MediaRecorder.AudioEncoder.DEFAULT)
를 호출합니다.setVideoEncoder(MediaRecorder.VideoEncoder.DEFAULT)
를 호출합니다.prepare()
를 호출하여MediaRecorder
구성을 마무리합니다.
- 녹화를 시작하려면
MediaRecorder.start()
를 호출합니다. - 녹화를 중지하려면 이러한 메서드를 호출합니다. 다시 한 번 이 순서를 정확히 따릅니다.
MediaRecorder.stop()
을 호출합니다.- 선택적으로,
MediaRecorder.reset()
을 호출하여 현재MediaRecorder
구성을 삭제합니다. MediaRecorder.release()
를 호출합니다.- 향후
MediaRecorder
세션에서Camera.lock()
을 호출하여 카메라를 사용할 수 있도록 카메라를 잠급니다.
- 미리보기를 중지하려면
Camera.stopPreview()
를 호출합니다. - 마지막으로, 다른 프로세스에서 사용할 수 있도록
Camera
를 해제하려면Camera.release()
를 호출합니다.
다음은 이러한 모든 단계를 결합한 것입니다.
// Camera1: set up a MediaRecorder and a function to start and stop video // recording. // Make a reference to the MediaRecorder at a scope that can be accessed // throughout the camera logic in your app. private var mediaRecorder: MediaRecorder? = null private var isRecording = false ... private fun prepareMediaRecorder(): Boolean { mediaRecorder = MediaRecorder() // Unlock and set camera to MediaRecorder. camera?.unlock() mediaRecorder?.run { setCamera(camera) // Set the audio and video sources. setAudioSource(MediaRecorder.AudioSource.CAMCORDER) setVideoSource(MediaRecorder.VideoSource.CAMERA) // Set a CamcorderProfile (requires API Level 8 or higher). setProfile(CamcorderProfile.get(CamcorderProfile.QUALITY_HIGH)) // Set the output file. setOutputFile(getOutputMediaFile(MEDIA_TYPE_VIDEO).toString()) // Set the preview output. setPreviewDisplay(preview?.holder?.surface) setOutputFormat(MediaRecorder.OutputFormat.MPEG_4) setAudioEncoder(MediaRecorder.AudioEncoder.DEFAULT) setVideoEncoder(MediaRecorder.VideoEncoder.DEFAULT) // Prepare configured MediaRecorder. return try { prepare() true } catch (e: IllegalStateException) { Log.d(TAG, "preparing MediaRecorder failed", e) releaseMediaRecorder() false } catch (e: IOException) { Log.d(TAG, "setting MediaRecorder file failed", e) releaseMediaRecorder() false } } return false } private fun releaseMediaRecorder() { mediaRecorder?.reset() mediaRecorder?.release() mediaRecorder = null camera?.lock() } private fun startStopVideo() { if (isRecording) { // Stop recording and release camera. mediaRecorder?.stop() releaseMediaRecorder() camera?.lock() isRecording = false // This is a good place to inform user that video recording has stopped. } else { // Initialize video camera. if (prepareVideoRecorder()) { // Camera is available and unlocked, MediaRecorder is prepared, now // you can start recording. mediaRecorder?.start() isRecording = true // This is a good place to inform the user that recording has // started. } else { // Prepare didn't work, release the camera. releaseMediaRecorder() // Inform user here. } } }
CameraX: CameraController
CameraX의 CameraController
를 사용하는 경우에는 UseCase 목록을 동시에 사용할 수 있다면 ImageCapture
, VideoCapture
, ImageAnalysis
UseCase
를 독립적으로 전환할 수 있습니다.
ImageCapture
및 ImageAnalysis
UseCase
는 기본적으로 사용 설정되어 있으므로 사진 촬영을 위해 setEnabledUseCases()
를 호출하지 않아도 되었습니다.
동영상 녹화에 CameraController
를 사용하려면 먼저 setEnabledUseCases()
를 사용하여 VideoCapture
UseCase
를 허용해야 합니다.
// CameraX: Enable VideoCapture UseCase on CameraController. cameraController.setEnabledUseCases(VIDEO_CAPTURE);
동영상 녹화를 시작하려면 CameraController.startRecording()
함수를 호출하면 됩니다. 이 함수는 아래 예에서 볼 수 있듯이 녹화된 동영상을 File
에 저장할 수 있습니다. 또한 Executor
및 OnVideoSavedCallback
을 구현하는 클래스를 전달하여 성공 및 오류 콜백을 처리해야 합니다. 녹화를 종료해야 할 때 CameraController.stopRecording()
을 호출합니다.
참고: CameraX 1.3.0-alpha02 이상을 사용하는 경우 동영상의 오디오 녹음을 사용 설정하거나 사용 중지할 수 있는 추가 AudioConfig
매개변수가 있습니다. 오디오 녹음을 사용 설정하려면 마이크 권한이 있는지 확인해야 합니다.
또한 stopRecording()
메서드가 1.3.0-alpha02에서 삭제되며, startRecording()
은 동영상 녹화를 일시중지, 다시 시작, 중지하는 데 사용할 수 있는 Recording
객체를 반환합니다.
// CameraX: implement video capture with CameraController. private val FILENAME_FORMAT = "yyyy-MM-dd-HH-mm-ss-SSS" // Define a VideoSaveCallback class for handling success and error states. class VideoSaveCallback : OnVideoSavedCallback { override fun onVideoSaved(outputFileResults: OutputFileResults) { val msg = "Video capture succeeded: ${outputFileResults.savedUri}" Toast.makeText(baseContext, msg, Toast.LENGTH_SHORT).show() Log.d(TAG, msg) } override fun onError(videoCaptureError: Int, message: String, cause: Throwable?) { Log.d(TAG, "error saving video: $message", cause) } } private fun startStopVideo() { if (cameraController.isRecording()) { // Stop the current recording session. cameraController.stopRecording() return } // Define the File options for saving the video. val name = SimpleDateFormat(FILENAME_FORMAT, Locale.US) .format(System.currentTimeMillis()) val outputFileOptions = OutputFileOptions .Builder(File(this.filesDir, name)) .build() // Call startRecording on the CameraController. cameraController.startRecording( outputFileOptions, ContextCompat.getMainExecutor(this), VideoSaveCallback() ) }
CameraX: CameraProvider
CameraProvider
를 사용 중인 경우 VideoCapture
UseCase
를 만들고 Recorder
객체를 전달해야 합니다. Recorder.Builder
에서 동영상 화질을 설정할 수 있으며 선택적으로 원하는 품질 사양을 충족하지 못하는 사례를 처리하는 FallbackStrategy
를 설정할 수 있습니다. 그런 다음, VideoCapture
인스턴스를 다른 UseCase
와 함께 CameraProvider
에 결합합니다.
// CameraX: create and bind a VideoCapture UseCase with CameraProvider. // Make a reference to the VideoCapture UseCase and Recording at a // scope that can be accessed throughout the camera logic in your app. private lateinit var videoCapture: VideoCaptureprivate var recording: Recording? = null ... // Create a Recorder instance to set on a VideoCapture instance (can be // added with other UseCase definitions). val recorder = Recorder.Builder() .setQualitySelector(QualitySelector.from(Quality.FHD)) .build() videoCapture = VideoCapture.withOutput(recorder) ... // Bind UseCases to camera (adding videoCapture along with preview here, but // preview is not required to use videoCapture). This function returns a camera // object which can be used to perform operations like zoom, flash, and focus. var camera = cameraProvider.bindToLifecycle( this, cameraSelector, preview, videoCapture)
이 시점에서 videoCapture.output
속성에서 Recorder
에 액세스할 수 있습니다. Recorder
는 File
, ParcelFileDescriptor
또는 MediaStore
에 저장되는 동영상 녹화를 시작할 수 있습니다. 이 예에서는 MediaStore
를 사용합니다.
Recorder
에서는 준비에 필요한 몇 가지 메서드가 호출됩니다. prepareRecording()
을 호출하여 MediaStore
출력 옵션을 설정합니다. 앱에 기기의 마이크를 사용할 권한이 있다면 withAudioEnabled()
도 호출합니다.
그런 다음, start()
를 호출하여 녹화를 시작하고 동영상 녹화 이벤트를 처리하도록 컨텍스트 및 Consumer<VideoRecordEvent>
이벤트 리스너를 전달합니다. 성공하면 반환된 Recording
을 사용하여 녹화를 일시중지, 다시 시작 또는 중지할 수 있습니다.
// CameraX: implement video capture with CameraProvider. private val FILENAME_FORMAT = "yyyy-MM-dd-HH-mm-ss-SSS" private fun startStopVideo() { val videoCapture = this.videoCapture ?: return if (recording != null) { // Stop the current recording session. recording.stop() recording = null return } // Create and start a new recording session. val name = SimpleDateFormat(FILENAME_FORMAT, Locale.US) .format(System.currentTimeMillis()) val contentValues = ContentValues().apply { put(MediaStore.MediaColumns.DISPLAY_NAME, name) put(MediaStore.MediaColumns.MIME_TYPE, "video/mp4") if (Build.VERSION.SDK_INT > Build.VERSION_CODES.P) { put(MediaStore.Video.Media.RELATIVE_PATH, "Movies/CameraX-Video") } } val mediaStoreOutputOptions = MediaStoreOutputOptions .Builder(contentResolver, MediaStore.Video.Media.EXTERNAL_CONTENT_URI) .setContentValues(contentValues) .build() recording = videoCapture.output .prepareRecording(this, mediaStoreOutputOptions) .withAudioEnabled() .start(ContextCompat.getMainExecutor(this)) { recordEvent -> when(recordEvent) { is VideoRecordEvent.Start -> { viewBinding.videoCaptureButton.apply { text = getString(R.string.stop_capture) isEnabled = true } } is VideoRecordEvent.Finalize -> { if (!recordEvent.hasError()) { val msg = "Video capture succeeded: " + "${recordEvent.outputResults.outputUri}" Toast.makeText( baseContext, msg, Toast.LENGTH_SHORT ).show() Log.d(TAG, msg) } else { recording?.close() recording = null Log.e(TAG, "video capture ends with error", recordEvent.error) } viewBinding.videoCaptureButton.apply { text = getString(R.string.start_capture) isEnabled = true } } } } }
추가 리소스
카메라 샘플 GitHub 저장소에 완전한 CameraX 앱이 몇 가지 있습니다. 이 샘플은 이 가이드의 시나리오가 완전한 Android 앱에 어떻게 적용되는지 보여줍니다.
CameraX로의 이전과 관련한 추가 지원이 필요하거나 Android Camera API 제품군에 관해 궁금한 점이 있으면 CameraX 토론방에서 문의하세요.