참고: 이 페이지에서는 Camera2 패키지에 관해 다룹니다. 앱에 Camera2의 특정 하위 수준 기능이 필요한 경우가 아니라면 CameraX를 사용하는 것이 좋습니다. CameraX와 Camera2는 모두 Android 5.0(API 수준 21) 이상을 지원합니다.
Android 기기에서 카메라 미리보기와 카메라 미리보기가 항상 동일한 방향은 아닙니다.
카메라는 기기가 스마트폰, 태블릿, 컴퓨터인지 여부와 관계없이 기기에서 고정된 위치에 있습니다. 기기 방향이 변경되면 카메라 방향도 변경됩니다.
따라서 카메라 앱은 일반적으로 기기의 방향과 카메라 미리보기의 가로세로 비율 사이에 고정된 관계를 가정합니다. 휴대전화가 세로 모드 방향인 경우 카메라 미리보기가 너비보다 높이가 더 크다고 가정합니다. 휴대전화 (및 카메라)가 가로 모드로 회전하면 카메라 미리보기는 높이보다 더 넓을 것으로 예상됩니다.
그러나 이러한 가정은 폴더블 기기와 같은 새로운 폼 팩터와 멀티 윈도우 및 다중 디스플레이와 같은 디스플레이 모드로 인해 어려움을 겪습니다. 폴더블 기기는 방향 변경 없이 디스플레이 크기와 가로세로 비율을 변경합니다. 멀티 윈도우 모드는 카메라 앱을 화면의 일부로 제한하여 기기 방향과 관계없이 카메라 미리보기를 확장합니다. 다중 디스플레이 모드를 사용하면 기본 디스플레이와 방향이 동일하지 않을 수 있는 보조 디스플레이를 사용할 수 있습니다.
카메라 방향
Android 호환성 정의는 카메라 이미지 센서가 '카메라의 긴 쪽이 화면의 긴 쪽과 정렬되도록 방향을 설정해야 한다(MUST)'고 명시합니다. 즉, 기기를 가로 방향으로 쥐면 카메라가 가로 방향에서 이미지를 캡처해야 합니다(MUST). 이는 기기의 자연스러운 방향과 상관없이 적용됩니다. 즉, 가로 모드가 기본인 기기는 물론 세로 모드가 기본인 기기에도 적용됩니다.
카메라 대 화면 정렬은 카메라 앱에서 카메라 뷰파인더의 디스플레이 영역을 최대화합니다. 또한 이미지 센서는 일반적으로 데이터를 가로 모드 가로세로 비율(가장 일반적인 4:3)로 출력합니다.
카메라 센서의 자연스러운 방향은 가로 모드입니다. 그림 1에서 전면 카메라의 센서 (디스플레이와 같은 방향을 가리키는 카메라)는 Android 호환성 정의에 따라 휴대전화를 기준으로 270도 회전합니다.
센서 회전을 앱에 노출하기 위해 camera2 API에는 SENSOR_ORIENTATION
상수가 포함되어 있습니다. 대부분의 스마트폰 및 태블릿의 경우 기기는 전면 카메라의 경우 270도, 후면 카메라의 경우 90도 (기기 뒷면에서의 시점)의 센서 방향을 보고하며, 이를 통해 센서의 긴 가장자리가 기기의 긴 가장자리에 정렬됩니다. 노트북 카메라는 일반적으로 0 또는 180도의 센서 방향을 보고합니다.
카메라 이미지 센서가 센서의 자연스러운 방향 (가로 모드)으로 데이터 (이미지 버퍼)를 출력하기 때문에, 카메라 미리보기가 기기의 자연스러운 방향에서 수직으로 표시되도록 하려면 이미지 버퍼를 SENSOR_ORIENTATION
로 지정된 각도로 회전해야 합니다. 전면 카메라의 경우 시계 반대 방향으로 회전하며, 후면 카메라의 경우 시계 방향으로 회전합니다.
예를 들어, 그림 1 전면 카메라의 경우 카메라 센서에 의해 생성된 이미지 버퍼는 다음과 같습니다.
미리보기의 방향이 기기 방향과 일치하도록 이미지는 시계 반대 방향으로 270도 회전해야 합니다.
후면 카메라는 위 버퍼와 방향이 같은 이미지 버퍼를 생성하지만 SENSOR_ORIENTATION
는 90도입니다. 따라서 버퍼는 시계 방향으로 90도 회전합니다.
기기 회전
기기 회전은 기기가 자연스러운 방향에서 회전되는 각도입니다. 예를 들어 가로 모드 방향의 휴대전화는 회전 방향에 따라 기기 회전이 90도 또는 270도입니다.
카메라 미리보기가 수직으로 표시되려면 카메라 센서 이미지 버퍼가 센서 방향의 각도 외에 기기 회전과 동일한 각도로 회전되어야 합니다.
방향 계산
카메라 미리보기의 올바른 방향은 센서 방향과 기기 회전을 고려합니다.
센서 이미지 버퍼의 전체 회전은 다음 수식을 사용하여 계산할 수 있습니다.
rotation = (sensorOrientationDegrees - deviceOrientationDegrees * sign + 360) % 360
여기서 sign
는 전면 카메라의 경우 1
이고, 후면 카메라의 경우 -1
입니다.
전면 카메라의 경우 이미지 버퍼는 센서의 자연스러운 방향에서 시계 반대 방향으로 회전합니다. 후면 카메라의 경우 센서 이미지 버퍼는 시계 방향으로 회전합니다.
deviceOrientationDegrees * sign + 360
표현식은 후면 카메라의 기기 회전을 시계 반대 방향에서 시계 방향으로 변환합니다 (예: 시계 반대 방향 270도를 시계 방향으로 90도로 변환). 모듈로 연산에서는 결과의 크기를 360도 미만으로 조정합니다 (예: 540도 회전의 크기를 180으로 조정).
API마다 기기 회전을 다르게 보고합니다.
Display#getRotation()
는 사용자 관점에서 기기의 시계 반대 방향 회전을 제공합니다. 이 값은 위 수식에 그대로 연결됩니다.OrientationEventListener#onOrientationChanged()
는 사용자 관점에서 기기의 시계 방향 회전을 반환합니다. 위 수식에서 사용할 값을 음수로 바꿉니다.
전면 카메라
다음은 그림 2의 카메라 센서에 의해 생성된 이미지 버퍼입니다.
센서 방향을 조정하려면 버퍼를 시계 반대 방향으로 270도 회전해야 합니다 (위의 카메라 방향 참고).
그런 다음 버퍼를 시계 반대 방향으로 추가로 90도 회전하여 기기 회전을 고려하여 그림 2에서 카메라 미리보기의 올바른 방향을 얻습니다.
다음은 카메라를 오른쪽으로 가로 모드 방향으로 돌린 예입니다.
다음은 이미지 버퍼입니다.
센서 방향을 조정하려면 버퍼를 시계 반대 방향으로 270도 회전해야 합니다.
그런 다음 기기 회전을 고려하여 버퍼를 시계 반대 방향으로 270도 더 회전합니다.
후면 카메라
후면 카메라는 일반적으로 센서 방향이 90도입니다 (기기 뒷면에서 볼 수 있음). 카메라 미리보기의 방향을 설정할 때 센서 이미지 버퍼는 (전면 카메라와 같은 시계 반대 방향이 아니라) 센서 회전 정도에 따라 시계 방향으로 회전한 다음, 기기 회전량에 따라 이미지 버퍼가 시계 반대 방향으로 회전합니다.
다음은 그림 4에서 카메라 센서의 이미지 버퍼입니다.
센서 방향을 조정하려면 버퍼를 시계 방향으로 90도 회전해야 합니다.
그런 다음 기기 회전을 고려하여 버퍼가 시계 반대 방향으로 270도 회전합니다.
가로세로 비율
디스플레이 가로세로 비율은 기기 방향이 변경될 때 변경되며 폴더블이 접히고 펼쳐질 때, 멀티 윈도우 환경에서 창 크기가 조절될 때, 앱이 보조 디스플레이에서 열릴 때도 변경됩니다.
기기 방향 변경 여부와 관계없이 UI가 동적으로 방향을 변경할 때 카메라 센서 이미지 버퍼는 뷰파인더 UI 요소의 방향과 가로세로 비율에 맞게 방향과 크기가 조정되어야 합니다.
새 폼 팩터나 멀티 윈도우 또는 다중 디스플레이 환경에서 앱이 카메라 미리보기가 기기와 동일한 방향(세로 또는 가로)이라고 가정하면 미리보기의 방향이 잘못되었거나 잘못 조정되거나 둘 다 발생할 수 있습니다.
그림 5에서 애플리케이션은 기기가 시계 반대 방향으로 90도 회전했다고 잘못 가정하여 같은 양만큼 미리보기를 회전했습니다.
그림 6에서 앱은 카메라 미리보기 UI 요소의 새 크기에 맞게 적절히 조정되도록 이미지 버퍼의 가로세로 비율을 조정하지 않았습니다.
고정 방향 카메라 앱은 일반적으로 폴더블 및 노트북과 같은 기타 대형 화면 기기에서 문제가 발생합니다.
그림 7에서 카메라 앱의 UI는 가로로 표시됩니다. 앱의 방향이 세로 모드로만 제한되어 있기 때문입니다. 뷰파인더 이미지의 방향이 카메라 센서를 기준으로 올바르게 지정됩니다.
세로 모드 인셋
멀티 윈도우 모드(resizeableActivity="false"
)를 지원하지 않고 방향을 제한(screenOrientation="portrait"
또는 screenOrientation="landscape"
)하는 카메라 앱은 대형 화면 기기에서 인셋 세로 모드로 배치되어 카메라 미리보기의 방향을 적절하게 설정할 수 있습니다.
디스플레이 가로세로 비율이 가로 모드일지라도 세로 모드 방향의 세로 모드 전용 앱을 인셋하여 세로 모드 레터박스 (인셋)합니다. 가로 모드 전용 앱은 디스플레이 가로세로 비율이 세로 모드인 경우에도 가로 모드 방향으로 레터박스 처리됩니다. 카메라 이미지는 앱 UI에 맞게 회전되고 카메라 미리보기의 가로세로 비율에 맞게 잘린 다음 미리보기에 맞게 크기가 조정됩니다.
인셋 인물 사진 모드는 카메라 이미지 센서의 가로세로 비율과 애플리케이션 기본 활동의 가로세로 비율이 일치하지 않을 때 트리거됩니다.
그림 8에서는 세로 모드 전용 카메라 앱이 회전하여 노트북 디스플레이에 UI를 바로 표시합니다. 세로 모드 앱과 가로 모드 디스플레이의 가로세로 비율 차이로 인해 앱이 레터박스 처리됩니다. 인셋 세로 모드로 인해 앱의 UI 회전을 보정하기 위해 카메라 미리보기 이미지가 회전되었으며 세로 모드 방향에 맞게 이미지가 잘리고 크기가 조정되어 시야가 줄었습니다.
회전, 자르기, 크기 조정
인셋 세로 모드는 가로 모드의 가로세로 비율이 있는 디스플레이의 세로 모드 전용 카메라 앱에 호출됩니다.
앱이 세로 모드 방향으로 레터박스 처리됩니다.
카메라 이미지는 90도 회전하여 앱의 방향 조정에 맞게 조정됩니다.
이미지는 카메라 미리보기의 가로세로 비율에 맞게 잘린 다음 미리보기를 채우도록 조정됩니다 (시야각 감소).
폴더블 기기에서 카메라 센서의 방향은 세로 모드일 수 있고 디스플레이의 가로세로 비율은 가로 모드일 수 있습니다.
카메라 미리보기는 센서 방향에 맞게 조정되도록 회전되므로 뷰파인더에서 이미지의 방향이 올바르게 조정되지만 세로 모드 전용 앱은 가로로 표시됩니다.
인셋 세로 모드에서는 앱을 세로 방향으로 레터박스 처리하여 앱과 카메라 미리보기의 방향을 올바르게 설정하기만 하면 됩니다.
API
Android 12 (API 수준 31)부터 앱은 CaptureRequest
클래스의 SCALER_ROTATE_AND_CROP
속성을 사용하여 인셋 세로 모드를 명시적으로 제어할 수도 있습니다.
기본값은 SCALER_ROTATE_AND_CROP_AUTO
이며, 이 값을 사용하면 시스템에서 인셋 세로 모드를 호출할 수 있습니다.
SCALER_ROTATE_AND_CROP_90
은 위에 설명된 인셋 세로 모드의 동작입니다.
일부 기기는 일부 SCALER_ROTATE_AND_CROP
값을 지원하지 않습니다. 지원 값 목록을 확인하려면 CameraCharacteristics#SCALER_AVAILABLE_ROTATE_AND_CROP_MODES
를 참고하세요.
CameraX
Jetpack CameraX 라이브러리를 사용하면 센서 방향과 기기 회전을 수용하는 카메라 뷰파인더를 간단한 작업으로 만들 수 있습니다.
PreviewView
레이아웃 요소는 카메라 미리보기를 만들어 센서 방향, 기기 회전, 크기 조정을 자동으로 조정합니다. PreviewView
는 이미지를 중앙에 배치하지만 PreviewView
의 크기와 일치하도록 자를 수 있는 FILL_CENTER
크기 조정 유형을 적용하여 카메라 이미지의 가로세로 비율을 유지합니다. 카메라 이미지를 레터박스 처리하려면 배율 유형을 FIT_CENTER
로 설정합니다.
PreviewView
로 카메라 미리보기를 만드는 기본사항을 알아보려면 미리보기 구현을 참고하세요.
전체 샘플 구현은 GitHub의 CameraXBasic
저장소를 참고하세요.
카메라 뷰파인더
Preview 사용 사례와 마찬가지로 CameraViewfinder 라이브러리는 카메라 미리보기 생성을 간소화하는 도구 모음을 제공합니다. CameraX Core에 종속되지 않으므로 기존 Camera2 코드베이스에 원활하게 통합할 수 있습니다.
Surface
를 직접 사용하는 대신 CameraViewfinder
위젯을 사용하여 Camera2의 카메라 피드를 표시할 수 있습니다.
CameraViewfinder
는 내부적으로 TextureView
또는 SurfaceView
를 사용하여 카메라 피드를 표시하고 필요한 변환을 적용하여 뷰파인더를 올바르게 표시합니다.
이때 가로세로 비율, 크기, 회전을 수정해야 합니다.
CameraViewfinder
객체에서 노출 영역을 요청하려면 ViewfinderSurfaceRequest
를 만들어야 합니다.
이 요청에는 CameraCharacteristics
의 노출 영역 해상도 및 카메라 기기 정보에 관한 요구사항이 포함되어 있습니다.
requestSurfaceAsync()
를 호출하면 노출 영역 제공자(TextureView
또는 SurfaceView
)로 요청이 전송되고 Surface
의 ListenableFuture
를 가져옵니다.
markSurfaceSafeToRelease()
를 호출하면 노출 영역 제공자에게 노출 영역이 필요하지 않고 관련 리소스를 해제할 수 있다고 알립니다.
Kotlin
fun startCamera(){ val previewResolution = Size(width, height) val viewfinderSurfaceRequest = ViewfinderSurfaceRequest(previewResolution, characteristics) val surfaceListenableFuture = cameraViewfinder.requestSurfaceAsync(viewfinderSurfaceRequest) Futures.addCallback(surfaceListenableFuture, object : FutureCallback<Surface> { override fun onSuccess(surface: Surface) { /* create a CaptureSession using this surface as usual */ } override fun onFailure(t: Throwable) { /* something went wrong */} }, ContextCompat.getMainExecutor(context)) }
Java
void startCamera(){ Size previewResolution = new Size(width, height); ViewfinderSurfaceRequest viewfinderSurfaceRequest = new ViewfinderSurfaceRequest(previewResolution, characteristics); ListenableFuture<Surface> surfaceListenableFuture = cameraViewfinder.requestSurfaceAsync(viewfinderSurfaceRequest); Futures.addCallback(surfaceListenableFuture, new FutureCallback<Surface>() { @Override public void onSuccess(Surface result) { /* create a CaptureSession using this surface as usual */ } @Override public void onFailure(Throwable t) { /* something went wrong */} }, ContextCompat.getMainExecutor(context)); }
SurfaceView
SurfaceView
는 미리보기에 처리가 필요하지 않고 애니메이션이 적용되지 않은 경우 간단하게 카메라 미리보기를 만들 수 있는 방법입니다.
SurfaceView
는 센서 방향과 기기 회전을 모두 고려하여 디스플레이 방향에 맞게 카메라 센서 이미지 버퍼를 자동으로 회전합니다. 그러나 이미지 버퍼는 가로세로 비율을 고려하지 않고 SurfaceView
크기에 맞게 조정됩니다.
이미지 버퍼의 가로세로 비율이 SurfaceView
의 가로세로 비율과 일치하도록 해야 합니다. 이는 구성요소의 onMeasure()
메서드에서 SurfaceView
콘텐츠의 크기를 조정하여 얻을 수 있습니다.
computeRelativeRotation()
소스 코드는 아래의 상대 회전에 있습니다.
Kotlin
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) { val width = MeasureSpec.getSize(widthMeasureSpec) val height = MeasureSpec.getSize(heightMeasureSpec) val relativeRotation = computeRelativeRotation(characteristics, surfaceRotationDegrees) if (previewWidth > 0f && previewHeight > 0f) { /* Scale factor required to scale the preview to its original size on the x-axis. */ val scaleX = if (relativeRotation % 180 == 0) { width.toFloat() / previewWidth } else { width.toFloat() / previewHeight } /* Scale factor required to scale the preview to its original size on the y-axis. */ val scaleY = if (relativeRotation % 180 == 0) { height.toFloat() / previewHeight } else { height.toFloat() / previewWidth } /* Scale factor required to fit the preview to the SurfaceView size. */ val finalScale = min(scaleX, scaleY) setScaleX(1 / scaleX * finalScale) setScaleY(1 / scaleY * finalScale) } setMeasuredDimension(width, height) }
Java
@Override void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { int width = MeasureSpec.getSize(widthMeasureSpec); int height = MeasureSpec.getSize(heightMeasureSpec); int relativeRotation = computeRelativeRotation(characteristics, surfaceRotationDegrees); if (previewWidth > 0f && previewHeight > 0f) { /* Scale factor required to scale the preview to its original size on the x-axis. */ float scaleX = (relativeRotation % 180 == 0) ? (float) width / previewWidth : (float) width / previewHeight; /* Scale factor required to scale the preview to its original size on the y-axis. */ float scaleY = (relativeRotation % 180 == 0) ? (float) height / previewHeight : (float) height / previewWidth; /* Scale factor required to fit the preview to the SurfaceView size. */ float finalScale = Math.min(scaleX, scaleY); setScaleX(1 / scaleX * finalScale); setScaleY(1 / scaleY * finalScale); } setMeasuredDimension(width, height); }
SurfaceView
를 카메라 미리보기로 구현하는 방법에 관한 자세한 내용은 카메라 방향을 참고하세요.
TextureView
TextureView
는 SurfaceView
보다 성능이 낮고 작업량이 많지만 TextureView
를 사용하면 카메라 미리보기를 최대한 제어할 수 있습니다.
TextureView
는 센서 방향에 따라 센서 이미지 버퍼를 회전하지만 기기 회전이나 미리보기 크기 조정을 처리하지는 않습니다.
크기 조정 및 회전은 매트릭스 변환으로 인코딩할 수 있습니다. TextureView
의 크기를 올바르게 조정하고 회전하는 방법을 알아보려면 카메라 앱에서 크기 조절 가능한 노출 영역 지원을 참고하세요.
상대 회전
카메라 센서의 상대적 회전은 카메라 센서 출력을 기기 방향에 맞추는 데 필요한 회전의 정도입니다.
상대 회전은 SurfaceView
및 TextureView
와 같은 구성요소에서 사용하여 미리보기 이미지의 x 및 y 배율을 결정합니다. 또한 센서 이미지 버퍼의 회전을 지정하는 데도 사용됩니다.
CameraCharacteristics
및 Surface
클래스를 사용하면 카메라 센서의 상대적 회전을 계산할 수 있습니다.
Kotlin
/** * Computes rotation required to transform the camera sensor output orientation to the * device's current orientation in degrees. * * @param characteristics The CameraCharacteristics to query for the sensor orientation. * @param surfaceRotationDegrees The current device orientation as a Surface constant. * @return Relative rotation of the camera sensor output. */ public fun computeRelativeRotation( characteristics: CameraCharacteristics, surfaceRotationDegrees: Int ): Int { val sensorOrientationDegrees = characteristics.get(CameraCharacteristics.SENSOR_ORIENTATION)!! // Reverse device orientation for back-facing cameras. val sign = if (characteristics.get(CameraCharacteristics.LENS_FACING) == CameraCharacteristics.LENS_FACING_FRONT ) 1 else -1 // Calculate desired orientation relative to camera orientation to make // the image upright relative to the device orientation. return (sensorOrientationDegrees - surfaceRotationDegrees * sign + 360) % 360 }
Java
/** * Computes rotation required to transform the camera sensor output orientation to the * device's current orientation in degrees. * * @param characteristics The CameraCharacteristics to query for the sensor orientation. * @param surfaceRotationDegrees The current device orientation as a Surface constant. * @return Relative rotation of the camera sensor output. */ public int computeRelativeRotation( CameraCharacteristics characteristics, int surfaceRotationDegrees ){ Integer sensorOrientationDegrees = characteristics.get(CameraCharacteristics.SENSOR_ORIENTATION); // Reverse device orientation for back-facing cameras. int sign = characteristics.get(CameraCharacteristics.LENS_FACING) == CameraCharacteristics.LENS_FACING_FRONT ? 1 : -1; // Calculate desired orientation relative to camera orientation to make // the image upright relative to the device orientation. return (sensorOrientationDegrees - surfaceRotationDegrees * sign + 360) % 360; }
창 측정항목
카메라 뷰파인더의 크기를 결정하는 데 화면 크기를 사용하면 안 됩니다. 카메라 앱이 휴대기기의 멀티 윈도우 모드 또는 ChromeOS의 Free-from 모드에서 화면의 일부에서 실행되고 있을 수 있습니다.
WindowManager#getCurrentWindowMetrics()
(API 수준 30에 추가됨)는 화면 크기가 아닌 애플리케이션 창의 크기를 반환합니다. Jetpack WindowManager 라이브러리 메서드 WindowMetricsCalculator#computeCurrentWindowMetrics()
및 WindowInfoTracker#currentWindowMetrics()
는 API 수준 14와 하위 호환성을 제공하는 유사한 지원을 제공합니다.
180도 회전
기기가 180도 회전해도 (예: 자연스러운 방향에서 자연스러운 방향이 거꾸로 향)도 onConfigurationChanged()
콜백이 트리거되지 않습니다. 따라서 카메라 미리보기가 거꾸로 표시될 수 있습니다.
180도 회전을 감지하려면 DisplayListener
를 구현하고 onDisplayChanged()
콜백에서 Display#getRotation()
를 호출하여 기기 회전을 확인합니다.
독점 리소스
Android 10 이전에는 멀티 윈도우 환경에서 맨 위에 표시되는 활동만 RESUMED
상태였습니다. 이는 사용자에게 혼란을 주었습니다. 시스템에서 재개된 활동을 나타내지 않았기 때문입니다.
Android 10 (API 수준 29)에서는 표시되는 모든 활동이 RESUMED
상태인 다중 재개를 도입했습니다. 투명한 활동이 활동 위에 있거나 활동에 포커스를 맞출 수 없는 경우(예: PIP 모드) 표시되는 활동은 여전히 PAUSED
상태가 될 수 있습니다(PIP 모드 지원 참고).
API 수준 29 이상에서 카메라, 마이크 또는 독점 또는 싱글톤 리소스를 사용하는 애플리케이션은 다중 재개를 지원해야 합니다. 예를 들어, 재개된 활동 3개가 카메라를 사용하려는 경우 하나만 이 독점 리소스에 액세스할 수 있습니다. 각 활동은 onDisconnected()
콜백을 구현하여 우선순위가 더 높은 활동으로 카메라에 관한 사전 액세스를 인지해야 합니다.
자세한 내용은 다중 재개를 참고하세요.
추가 리소스
- Camera2 샘플은 GitHub의 Camera2Basic 앱을 참고하세요.
- CameraX 미리보기 사용 사례에 관한 자세한 내용은 CameraX 미리보기 구현을 참고하세요.
- CameraX 카메라 미리보기 샘플 구현은 GitHub의 CameraXBasic 저장소를 참고하세요.
- ChromeOS의 카메라 미리보기에 관한 자세한 내용은 카메라 방향을 참고하세요.
- 폴더블 기기 개발에 관한 자세한 내용은 폴더블 알아보기를 참고하세요.