Android 앱에서 카메라를 사용하는 경우 방향을 처리할 때 몇 가지 특별한 고려사항이 있습니다. 이 문서에서는 독자가 Android camera2 API의 기본 개념을 이해하고 있다고 가정합니다. 블로그 게시물 또는 요약에서 camera2 개요를 확인할 수 있습니다. 이 문서를 살펴보기 전에 먼저 카메라 앱을 작성해 보는 것이 좋습니다.
배경
Android 카메라 앱에서 방향을 처리하는 것은 까다로우며 다음 요소를 고려해야 합니다.
- 자연스러운 방향: 기기 디자인의 '일반' 위치에 있을 때의 디스플레이 방향입니다. 일반적으로 휴대전화의 경우 세로 방향, 노트북의 경우 가로 방향입니다.
- 센서 방향: 기기에 물리적으로 장착된 센서의 방향입니다.
- 디스플레이 회전: 기기가 자연스러운 방향에서 물리적으로 어느 정도 회전하는지입니다.
- 뷰파인더 크기: 카메라 미리보기를 표시하는 데 사용되는 뷰파인더의 크기입니다.
- 카메라에서 출력하는 이미지 크기입니다.
이러한 요소가 결합되어 카메라 앱에 가능한 UI 및 미리보기 구성이 많이 도입됩니다. 이 문서는 개발자가 이러한 문제를 탐색하고 Android 앱에서 카메라 방향을 올바르게 처리하는 방법을 보여주기 위한 것입니다.
간단하게 하기 위해 달리 언급하지 않는 한 모든 예시에는 후면 카메라가 포함된다고 가정합니다. 또한 다음 사진은 시각적으로 더 명확하게 설명하기 위해 시뮬레이션되었습니다.
방향에 관한 모든 것
자연스러운 방향
자연스러운 방향은 기기가 일반적으로 있어야 하는 위치에 있을 때의 디스플레이 방향으로 정의됩니다. 휴대전화의 경우 자연스러운 방향이 세로인 경우가 많습니다. 즉, 휴대전화는 너비가 짧고 높이가 깁니다. 노트북의 경우 자연스러운 방향이 가로 모드이므로 너비가 길고 높이가 짧습니다. 태블릿은 이보다 약간 더 복잡합니다. 세로 모드 또는 가로 모드일 수 있습니다.
센서 방향
공식적으로 센서 방향은 센서의 출력 이미지를 기기의 자연스러운 방향과 일치시키기 위해 시계 방향으로 회전해야 하는 각도로 측정됩니다. 다시 말해 센서 방향은 센서가 기기에 장착되기 전에 시계 반대 방향으로 회전한 각도입니다. 화면을 볼 때 회전이 시계 방향으로 보이는 이유는 후면 카메라 센서가 기기의 '뒷면'에 설치되어 있기 때문입니다.
Android 10 호환성 정의 7.5.5 카메라 방향에 따르면 전면 및 후면 카메라는 '카메라의 긴 쪽이 화면의 긴 쪽과 정렬되도록 방향을 설정해야 합니다(MUST)'.
카메라의 출력 버퍼는 가로 모드 크기입니다. 휴대전화의 자연스러운 방향은 보통 세로 모드이므로 센서 방향은 일반적으로 출력 버퍼의 긴 쪽이 화면의 긴 쪽과 일치하도록 자연스러운 방향에서 90도 또는 270도입니다. 센서 방향은 Chromebook과 같이 자연스러운 방향이 가로 모드인 기기와 다릅니다. 이러한 기기에서는 출력 버퍼의 긴 쪽이 화면의 긴 쪽과 일치하도록 이미지 센서가 다시 배치됩니다. 두 이미지가 모두 가로 크기이므로 방향이 일치하고 센서 방향은 0도 또는 180도입니다.
다음 그림은 기기 화면을 보는 관찰자의 관점에서 어떻게 보이는지 보여줍니다.
다음 장면을 고려해 보세요.
| 전화 | 노트북 |
|---|---|
![]() |
![]() |
센서 방향은 휴대전화에서 보통 90도 또는 270도이므로 센서 방향을 고려하지 않으면 이미지가 다음과 같이 표시됩니다.
| 전화 | 노트북 |
|---|---|
![]() |
![]() |
반시계 방향 센서 방향이 sensorOrientation 변수에 저장된다고 가정해 보겠습니다. 센서 방향을 보정하려면 출력 버퍼를 `sensorOrientation`만큼 시계 방향으로 회전하여 방향을 기기의 자연스러운 방향과 다시 정렬해야 합니다.
Android에서 앱은 TextureView 또는 SurfaceView를 사용하여 카메라 미리보기를 표시할 수 있습니다. 앱에서 올바르게 사용하는 경우 둘 다 센서 방향을 처리할 수 있습니다. 다음 섹션에서는 센서 방향을 고려해야 하는 방법을 자세히 설명합니다.
디스플레이 회전
디스플레이 회전은 화면에 그려진 그래픽의 회전으로 공식적으로 정의되며, 이는 기기가 자연스러운 방향에서 물리적으로 회전하는 방향과 반대입니다. 다음 섹션에서는 디스플레이 회전이 모두 90의 배수라고 가정합니다. 절대 각도로 디스플레이 회전을 가져오는 경우 {0, 90, 180, 270} 중 가장 가까운 값으로 올림합니다.
다음 섹션의 '디스플레이 방향'은 기기가 가로 또는 세로 위치로 물리적으로 유지되는지 여부를 나타내며 '디스플레이 회전'과는 다릅니다.
다음 그림과 같이 이전 위치에서 시계 반대 방향으로 90도 회전한다고 가정해 보겠습니다.
센서 방향에 따라 출력 버퍼가 이미 회전되었다고 가정하면 다음과 같은 출력 버퍼가 있습니다.
| 전화 | 노트북 |
|---|---|
![]() |
![]() |
디스플레이 회전이 displayRotation 변수에 저장된 경우 올바른 이미지를 얻으려면 출력 버퍼를 displayRotation만큼 시계 반대 방향으로 회전해야 합니다.
전면 카메라의 경우 디스플레이 회전이 화면과 반대 방향으로 이미지 버퍼에 작용합니다. 전면 카메라를 다루는 경우 displayRotatation만큼 버퍼를 시계 방향으로 회전해야 합니다.
주의사항
디스플레이 회전은 기기의 시계 반대 방향 회전을 측정합니다. 모든 방향/회전 API에 적용되는 것은 아닙니다.
예를 들면 다음과 같습니다.
-
Display#getRotation()를 사용하면 이 문서에 언급된 대로 시계 반대 방향 회전이 적용됩니다. - OrientationEventListener#onOrientationChanged(int)를 사용하면 대신 시계 방향 회전이 표시됩니다.
여기서 중요한 점은 디스플레이 회전이 자연스러운 방향을 기준으로 한다는 것입니다. 예를 들어 휴대전화를 90도 또는 270도 회전하면 가로 모양의 화면이 표시됩니다. 반면 노트북을 같은 각도로 회전하면 세로 모양의 화면이 표시됩니다. 앱은 항상 이를 염두에 두고 기기의 자연스러운 방향에 관해 가정해서는 안 됩니다.
예
이전 수치를 사용하여 방향과 회전이 무엇인지 설명해 보겠습니다.
| 전화 | 노트북 |
|---|---|
| 자연스러운 방향 = 세로 | 자연스러운 방향 = 가로 |
| 센서 방향 = 90 | 센서 방향 = 0 |
| 디스플레이 회전 = 0 | 디스플레이 회전 = 0 |
| 디스플레이 방향 = 세로 | 디스플레이 방향 = 가로 |
| 전화 | 노트북 |
|---|---|
| 자연스러운 방향 = 세로 | 자연스러운 방향 = 가로 |
| 센서 방향 = 90 | 센서 방향 = 0 |
| 디스플레이 회전 = 90 | 디스플레이 회전 = 90 |
| 디스플레이 방향 = 가로 | 디스플레이 방향 = 세로 |
뷰파인더 크기
앱은 항상 방향, 회전, 화면 해상도에 따라 뷰파인더의 크기를 조정해야 합니다. 일반적으로 앱은 뷰파인더의 방향을 현재 디스플레이 방향과 동일하게 만들어야 합니다. 즉, 앱은 뷰파인더의 긴 모서리를 화면의 긴 모서리와 정렬해야 합니다.
카메라별 이미지 출력 크기
미리보기의 이미지 출력 크기를 선택할 때는 가능하면 뷰파인더 크기와 같거나 약간 큰 크기를 선택해야 합니다. 일반적으로 출력 버퍼가 스케일 업되어 픽셀화가 발생하지 않도록 하는 것이 좋습니다. 성능을 저하시키고 배터리를 더 많이 사용할 수 있는 너무 큰 크기를 선택하지 않는 것이 좋습니다.
JPEG 방향
일반적인 상황인 JPEG 사진 촬영부터 시작해 보겠습니다. camera2 API에서는 캡처 요청에 JPEG_ORIENTATION를 전달하여 출력 JPEG를 시계 방향으로 회전할 정도를 지정할 수 있습니다.
앞서 언급한 내용을 간략하게 요약하면 다음과 같습니다.
-
센서 방향을 처리하려면 이미지 버퍼를 시계 방향으로
sensorOrientation도 회전해야 합니다. -
디스플레이 회전을 처리하려면 후면 카메라의 경우 버퍼를 시계 반대 방향으로
displayRotation만큼 회전하고 전면 카메라의 경우 시계 방향으로 회전해야 합니다.
두 요소를 더하면 시계 방향으로 회전할 양은
-
후면 카메라용
sensorOrientation - displayRotation -
전면 카메라용
sensorOrientation + displayRotation
이 로직의 샘플 코드는 JPEG_ORIENTATION 문서에서 확인할 수 있습니다. 문서의 샘플 코드에 있는 deviceOrientation은 기기의 시계 방향 회전을 사용합니다. 따라서 디스플레이 회전의 부호가 반대로 표시됩니다.
미리보기
카메라 미리보기는 어떤가요? 앱이 카메라 미리보기를 표시하는 방법에는 SurfaceView와 TextureView라는 두 가지 주요 방법이 있습니다. 각각 방향을 올바르게 처리하려면 다른 접근 방식이 필요합니다.
SurfaceView
미리보기 버퍼를 처리하거나 애니메이션 처리할 필요가 없는 경우 일반적으로 카메라 미리보기에 SurfaceView를 사용하는 것이 좋습니다. TextureView보다 성능이 우수하고 리소스 요구사항이 적습니다.
SurfaceView는 레이아웃도 비교적 쉽게 지정할 수 있습니다. 카메라 미리보기를 표시하는 SurfaceView의 가로세로 비율만 고려하면 됩니다.
소스
SurfaceView 아래에서 Android 플랫폼은 기기의 디스플레이 방향과 일치하도록 출력 버퍼를 회전합니다. 즉, 센서 방향과 디스플레이 회전을 모두 고려합니다. 더 간단히 말하면 디스플레이가 가로 모드일 때는 미리보기도 가로 모드로 표시되고, 세로 모드일 때는 세로 모드로 표시됩니다.
다음 표를 참고하세요. 여기서 중요한 점은 디스플레이 회전만으로는 소스의 방향을 결정할 수 없다는 것입니다.
| 디스플레이 회전 | 휴대전화 (자연스러운 방향 = 세로) | 노트북 (자연스러운 방향 = 가로) |
|---|---|---|
| 0 | ![]() |
![]() |
| 90 | ![]() |
![]() |
| 180 | ![]() |
![]() |
| 270 | ![]() |
![]() |
레이아웃
보시다시피 SurfaceView는 이미 까다로운 작업을 일부 처리해 줍니다. 하지만 이제 뷰파인더의 크기 또는 화면에 표시할 미리보기의 크기를 고려해야 합니다. SurfaceView는 소스 버퍼를 크기에 맞게 자동으로 조정합니다. 뷰파인더의 가로세로 비율이 sourcebuffer의 가로세로 비율과 동일해야 합니다. 예를 들어 세로 모양 미리보기를 가로 모양 SurfaceView에 맞추려고 하면 다음과 같이 왜곡된 결과가 표시됩니다.
일반적으로 뷰파인더의 가로세로 비율 (즉, 너비/높이)이 소스의 가로세로 비율과 동일해야 합니다. 뷰파인더에서 이미지를 잘라내어 디스플레이를 수정하지 않으려면 aspectRatioActivity이 aspectRatioSource보다 큰 경우와 aspectRatioActivity이 aspectRatioSource보다 작거나 같은 경우의 두 가지 사례를 고려해야 합니다.
aspectRatioActivity > aspectRatioSource
케이스는 활동이 '더 넓은' 것으로 생각할 수 있습니다. 아래에서는 16:9 활동과 4:3 소스가 있는 예를 살펴보겠습니다.
aspectRatioActivity = 16/9 ≈ 1.78 aspectRatioSource = 4/3 ≈ 1.33
먼저 뷰파인더도 4:3이어야 합니다. 그런 다음 다음과 같이 소스와 뷰파인더를 활동에 맞춥니다.
이 경우 뷰파인더의 높이를 활동의 높이와 일치시키고 뷰파인더의 가로세로 비율을 소스의 가로세로 비율과 동일하게 만들어야 합니다. 의사 코드는 다음과 같습니다.
viewfinderHeight = activityHeight; viewfinderWidth = activityHeight * aspectRatioSource;
aspectRatioActivity ≤ aspectRatioSource
다른 경우는 활동이 '좁거나' '긴' 경우입니다. 다음 예시에서 기기를 90도 회전하여 활동을 9:16으로 만들고 소스를 3:4로 만드는 것을 제외하고 이전 예시를 재사용할 수 있습니다.
aspectRatioActivity = 9/16 = 0.5625 aspectRatioSource = 3/4 = 0.75
이 경우 소스와 뷰파인더를 다음과 같이 활동에 맞추면 됩니다.
뷰파인더의 가로세로 비율을 소스의 가로세로 비율과 동일하게 유지하면서 뷰파인더의 너비를 활동의 너비와 일치시켜야 합니다 (이전 사례의 높이와 반대). 의사 코드는 다음과 같습니다.
viewfinderWidth = activityWidth; viewfinderHeight = activityWidth / aspectRatioSource;
잘림
Camera2 샘플의 AutoFitSurfaceView.kt (GitHub)는 SurfaceView를 재정의하고 양쪽 방향에서 활동과 같거나 활동보다 '약간 큰' 이미지를 사용하여 가로세로 비율 불일치를 처리한 다음 오버플로되는 콘텐츠를 클리핑합니다. 이는 미리보기가 전체 활동을 포함하거나 이미지를 왜곡하지 않고 고정된 크기의 뷰를 완전히 채우려는 앱에 유용합니다.
주의
위의 샘플에서는 미리보기가 활동보다 약간 커서 채워지지 않은 공간이 남지 않도록 하여 화면 공간을 최대화하려고 합니다. 이는 오버플로 부분이 기본적으로 상위 레이아웃 (또는 ViewGroup)에 의해 클리핑된다는 사실에 기반합니다. 이 동작은 RelativeLayout 및 LinearLayout과 일치하지만 ConstraintLayout과는 일치하지 않습니다. ConstraintLayout은 레이아웃 내에 맞도록 하위 뷰의 크기를 조절할 수 있으며, 이 경우 의도한 '중앙 자르기' 효과가 깨지고 미리보기가 늘어납니다. 이 커밋을 참고하세요.
TextureView
TextureView는 카메라 미리보기 콘텐츠를 최대한 제어할 수 있지만 성능 비용이 발생합니다. 또한 카메라 미리보기를 올바르게 표시하려면 더 많은 작업이 필요합니다.
소스
TextureView 아래에서 Android 플랫폼은 센서 방향에 따라 출력 버퍼를 회전시켜 기기의 자연스러운 방향과 일치시킵니다. TextureView는 센서 방향을 처리하지만 디스플레이 회전은 처리하지 않습니다. 출력 버퍼를 기기의 자연스러운 방향에 맞추므로 디스플레이 회전을 직접 처리해야 합니다.
다음 표를 참고하세요. 해당 디스플레이 회전으로 수치를 회전하면 SurfaceView에서 실제로 동일한 수치가 표시됩니다.
| 디스플레이 회전 | 휴대전화 (자연스러운 방향 = 세로) | 노트북 (자연스러운 방향 = 가로) |
|---|---|---|
| 0 | ![]() |
![]() |
| 90 | ![]() |
![]() |
| 180 | ![]() |
![]() |
| 270 | ![]() |
![]() |
레이아웃
TextureView의 경우 레이아웃이 약간 까다롭습니다. 이전에는 TextureView에 변환 매트릭스를 사용하도록 제안했지만 이 방법은 모든 기기에서 작동하지 않습니다. 대신 여기에 설명된 단계를 따르세요.
TextureView에서 미리보기를 올바르게 배치하는 3단계 프로세스는 다음과 같습니다.
- 선택한 미리보기 크기와 동일하도록 TextureView의 크기를 설정합니다.
- 늘어났을 수 있는 TextureView를 미리보기의 원래 크기로 다시 조정합니다.
-
TextureView를 시계 반대 방향으로
displayRotation만큼 회전합니다.
디스플레이 회전이 90도인 휴대전화가 있다고 가정해 보겠습니다.
1. 선택한 미리보기 크기와 동일한 크기로 TextureView를 설정합니다.
선택한 미리보기 크기가 previewWidth × previewHeight이라고 가정해 보겠습니다. 여기서 previewWidth > previewHeight은 센서 출력이 기본적으로 가로 모양입니다. 캡처 세션을 구성할 때는 SurfaceTexture#setDefaultBufferSize(int width, height)을 호출하여 미리보기 크기 (previewWidth × previewHeight)를 지정해야 합니다.
setDefaultBufferSize를 호출하기 전에 View#setLayoutParams(android.view.ViewGroup.LayoutParams)를 사용하여 TextureView의 크기를 `previewWidth × previewHeight`로 설정해야 합니다. 이는 TextureView가 측정된 너비와 높이로 SurfaceTexture#setDefaultBufferSize(int width, height)를 호출하기 때문입니다. TextureView의 크기가 사전에 명시적으로 설정되지 않으면 경합 조건이 발생할 수 있습니다. 이 문제는 먼저 TextureView의 크기를 명시적으로 설정하여 완화할 수 있습니다.
이제 TextureView가 소스의 크기와 일치하지 않을 수 있습니다. 휴대전화의 경우 소스는 세로 모양이지만 방금 설정한 layoutParams 때문에 TextureView는 가로 모양입니다. 이로 인해 다음과 같이 미리보기가 늘어납니다.
2. 늘어날 수 있는 TextureView를 미리보기의 원래 크기로 다시 조정합니다.
늘어난 미리보기를 소스 크기로 다시 조정하려면 다음을 고려하세요.
소스의 측정기준 (sourceWidth × sourceHeight)은 다음과 같습니다.
-
previewHeight × previewWidth(자연스러운 방향이 세로 또는 세로 반전인 경우(센서 방향이 90도 또는 270도)) -
previewWidth × previewHeight: 자연스러운 방향이 가로 또는 역가로인 경우 (센서 방향이 0 또는 180도)
View#setScaleX(float) 및 View#setScaleY(float)를 활용하여 스트레칭 수정
-
setScaleX(
sourceWidth / previewWidth) -
setScaleY(
sourceHeight / previewHeight)
3. `displayRotation`만큼 시계 반대 방향으로 미리보기를 회전합니다.
앞서 언급한 대로 디스플레이 회전을 보정하기 위해 미리보기를 시계 반대 방향으로 displayRotation만큼 회전해야 합니다.
View#setRotation(float)을 사용하여 이를 수행할 수 있습니다.
-
시계 방향으로 회전하므로 setRotation(
-displayRotation)을 사용합니다.
샘플
-
Jetpack의 camerax에 있는
PreviewView는 앞에서 설명한 대로 TextureView 레이아웃을 처리합니다. PreviewCorrector로 변환을 구성합니다.
참고: 코드에서 TextureView에 변환 행렬을 이전에 사용한 경우 Chromebook과 같은 기본 가로 모드 기기에서 미리보기가 올바르게 표시되지 않을 수 있습니다. 변환 행렬이 센서 방향을 90도 또는 270도로 잘못 가정했을 수 있습니다. 해결 방법은 GitHub의 이 커밋을 참고하세요. 하지만 앱을 이전하여 여기에 설명된 방법을 사용하는 것이 좋습니다.





















