이 주제에서는 ImageAnalysis
사용 사례든 ImageCapture
사용 사례든 앱에서 CameraX 사용 사례를 설정하여 올바른 회전 정보가 포함된 이미지를 가져오는 방법을 설명합니다. 따라서 다음과 같은 전략을 구사하세요.
ImageAnalysis
사용 사례의Analyzer
는 올바른 회전이 적용된 프레임을 수신해야 합니다.ImageCapture
사용 사례에서는 올바른 회전으로 사진을 촬영해야 합니다.
용어
이 주제에서는 다음과 같은 용어를 사용하므로 각 용어의 의미를 이해하는 것이 중요합니다.
- 디스플레이 방향
- 기기의 어느 측면이 위를 향하는지를 나타냅니다. 값은 4가지 즉, 세로, 가로, 세로 반전, 가로 반전 중 하나일 수 있습니다.
- 디스플레이 회전
Display.getRotation()
에서 반환된 값으로, 기기가 자연스러운 방향에서 시계 반대 방향으로 회전한 각도를 나타냅니다.- 타겟 회전
- 자연스러운 방향에 이르도록 기기를 시계 방향으로 회전시킬 각도를 나타냅니다.
타겟 회전을 결정하는 방법
다음 예는 자연스러운 방향을 기준으로 기기의 타겟 회전을 결정하는 방법을 보여줍니다.
예 1: 자연스러운 세로 방향
기기 예: Pixel 3 XL | |
---|---|
자연스러운 방향 = 세로 디스플레이 회전 = 0 |
|
자연스러운 방향 = 세로 디스플레이 회전 = 90 |
예 2: 자연스러운 가로 방향
기기 예: Pixel C | |
---|---|
자연스러운 방향 = 가로 디스플레이 회전 = 0 |
|
자연스러운 방향 = 가로 디스플레이 회전 = 270 |
이미지 회전
어느 쪽이 위를 향하나요? 센서 방향은 Android에서 상수 값으로 정의됩니다. 이 값은 기기가 자연스러운 위치에 있을 때 센서가 기기 상단에서 회전한 각도(0, 90, 180, 270)를 나타냅니다. 도표의 모든 사례에 나와 있는 이미지 회전에서는 데이터를 똑바로 표시하기 위해 시계 방향으로 어떻게 회전해야 하는지를 설명합니다.
다음 예에서는 카메라 센서 방향에 따라 이미지 회전이 어떻게 이루어져야 하는지를 보여줍니다. 또한 타겟 회전이 디스플레이 회전으로 설정되어 있다고 가정합니다.
예 1: 센서가 90도 회전함
기기 예: Pixel 3 XL | |
---|---|
디스플레이 회전 = 0 |
|
디스플레이 회전 = 90 |
예 2: 센서가 270도 회전함
기기 예: Nexus 5X | |
---|---|
디스플레이 회전 = 0 |
|
디스플레이 회전 = 90 |
예 3: 센서가 0도 회전함
기기 예: Pixel C(태블릿) | |
---|---|
디스플레이 회전 = 0 |
|
디스플레이 회전 = 270 |
이미지 회전 계산
ImageAnalysis
ImageAnalysis
의 Analyzer
는 카메라의 이미지를 ImageProxy
형태로 수신합니다. 각 이미지에는 다음을 통해 액세스할 수 있는 회전 정보가 포함되어 있습니다.
val rotation = imageProxy.imageInfo.rotationDegrees
이 값은 ImageAnalysis
의 타겟 회전과 일치하도록 이미지를 시계 방향으로 회전시켜야 하는 각도를 나타냅니다. Android 앱의 컨텍스트에서 ImageAnalysis
의 타겟 회전은 일반적으로 화면의 방향과 일치합니다.
ImageCapture
콜백은 ImageCapture
인스턴스에 연결되어 캡처 결과가 준비되면 신호를 보냅니다. 결과는 캡처된 이미지이거나 오류일 수 있습니다.
사진을 찍을 때 제공되는 콜백은 다음 유형 중 하나일 수 있습니다.
OnImageCapturedCallback
: 메모리 내 액세스 권한이 있는 이미지를ImageProxy
형태로 수신합니다.OnImageSavedCallback
: 캡처된 이미지가ImageCapture.OutputFileOptions
로 지정된 위치에 성공적으로 저장되면 호출됩니다. 옵션은File
,OutputStream
을 지정할 수도 있고MediaStore
로 위치를 지정할 수 있습니다.
어떤 형식이든(ImageProxy
, File
, OutputStream
, MediaStore Uri
) 캡처된 이미지의 회전은 캡처된 이미지를 ImageCapture
의 타겟 회전(일반적으로 Android 앱의 컨텍스트에서는 화면의 방향과 일치)과 일치시키기 위해 시계 방향으로 회전시켜야 하는 각도를 나타냅니다.
다음 방법 중 하나로 캡처된 이미지의 회전을 가져올 수 있습니다.
ImageProxy
val rotation = imageProxy.imageInfo.rotationDegrees
File
val exif = Exif.createFromFile(file) val rotation = exif.rotation
OutputStream
val byteArray = outputStream.toByteArray() val exif = Exif.createFromInputStream(ByteArrayInputStream(byteArray)) val rotation = exif.rotation
MediaStore uri
val inputStream = contentResolver.openInputStream(outputFileResults.savedUri) val exif = Exif.createFromInputStream(inputStream) val rotation = exif.rotation
이미지 회전 확인
ImageAnalysis
및 ImageCapture
사용 사례에서는 캡처 요청이 성공하면 카메라에서 ImageProxy
를 수신합니다. ImageProxy
는 회전 정보를 포함하여 이미지와 이미지 정보를 래핑합니다. 이 회전 정보는 사용 사례의 타겟 회전과 일치하도록 이미지를 회전시켜야 하는 각도를 나타냅니다.
ImageCapture/ImageAnalysis 타겟 회전 가이드라인
대부분의 기기가 기본적으로 세로 반전 또는 가로 반전으로 회전되지 않기 때문에 일부 Android 앱에서는 이러한 방향을 지원하지 않습니다. 앱의 지원 여부에 따라 사용 사례의 타겟 회전 업데이트 방식이 달라집니다.
아래 두 가지 표에는 사용 사례의 타겟 회전을 디스플레이 회전과 동기화 상태로 유지하는 방법이 나와 있습니다. 첫 번째 표에는 네 가지 방향을 모두 지원하면서 동기화 상태를 유지하는 방법이 나와 있습니다. 두 번째 표에는 기기가 기본적으로 회전하는 방향만 처리하는 경우가 나와 있습니다.
앱에서 어떤 가이드라인을 따를지 선택하려면 다음 단계를 수행하세요.
앱의 카메라
Activity
방향이 잠겨 있는지, 잠금 해제되어 있는지 아니면 방향 구성 변경을 재정의하는지 확인합니다.앱의 카메라
Activity
에서 네 가지의 모든 기기 방향(세로, 세로 반전, 가로, 가로 반전)을 모두 처리하도록 할지 아니면 앱이 실행되는 기기가 기본적으로 지원하는 방향만 처리하도록 할지 결정합니다.
네 가지 방향 모두 지원
이 표에는 기기가 세로 반전으로 회전되지 않는 경우에 따라야 할 특정 가이드라인이 나와 있습니다. 가로 반전으로 회전되지 않는 기기에도 동일하게 적용될 수 있습니다.
시나리오 | 가이드라인 | 단일 창 모드 | 멀티 윈도우 화면 분할 모드 |
---|---|---|---|
방향이 잠금 해제됨 | Activity 의 onCreate() 콜백 등에서 Activity 가 생성될 때마다 사용 사례를 설정합니다. |
||
OrientationEventListener 의 onOrientationChanged() 를 사용합니다.
콜백 내에서 사용 사례의 타겟 회전을 업데이트합니다. 이는 기기가 180도 회전하는 등 방향이 변경된 후에도 시스템에서 Activity 를 다시 생성하지 않는 사례를 처리합니다.
|
디스플레이가 세로 반전 방향일 때 기기가 기본적으로 세로 반전으로 회전되지 않는 경우도 처리됩니다. |
기기가 회전(예: 90도)할 때 Activity 가 다시 생성되지 않는 사례도 처리됩니다. 이 같은 경우는 앱이 화면의 절반까지 차지하는 소형 폼 팩터 기기와 앱이 화면의 2/3까지 차지하는 대형 기기에서 발생합니다.
|
|
선택사항: AndroidManifest 파일에서 Activity 의 screenOrientation 속성을 fullSensor 로 설정합니다. |
기기가 세로 반전 방향일 때 UI가 똑바로 표시되며 기기가 90도 회전할 때마다 시스템에서 Activity 가 다시 생성됩니다. |
기본적으로 세로 반전으로 회전하지 않는 기기에는 아무런 영향을 미치지 않습니다. 디스플레이가 세로 반전 방향일 때는 멀티 윈도우 모드가 지원되지 않습니다. | |
방향이 잠김 | Activity 의 onCreate() 콜백 등에서 Activity 가 처음 생성될 때 사용 사례를 한 번만 설정합니다. |
||
OrientationEventListener 의 onOrientationChanged() 를 사용합니다.
콜백 내에서 미리보기를 제외한 사용 사례의 타겟 회전을 업데이트합니다.
|
기기가 회전(예: 90도)할 때 Activity 가 다시 생성되지 않는 사례도 처리됩니다. 이 같은 경우는 앱이 화면의 절반까지 차지하는 소형 폼 팩터 기기와 앱이 화면의 2/3까지 차지하는 대형 기기에서 발생합니다.
|
||
방향 구성 변경이 재정의됨 | Activity 의 onCreate() 콜백 등에서 Activity 가 처음 생성될 때 사용 사례를 한 번만 설정합니다. |
||
OrientationEventListener 의 onOrientationChanged() 를 사용합니다.
콜백 내에서 사용 사례의 타겟 회전을 업데이트합니다.
|
기기가 회전(예: 90도)할 때 Activity 가 다시 생성되지 않는 사례도 처리됩니다. 이 같은 경우는 앱이 화면의 절반까지 차지하는 소형 폼 팩터 기기와 앱이 화면의 2/3까지 차지하는 대형 기기에서 발생합니다.
|
||
선택사항: AndroidManifest 파일에서 Activity의 screenOrientation 속성을 fullSensor로 설정합니다. | 기기가 세로 반전일 때 UI가 똑바로 표시됩니다. | 기본적으로 세로 반전으로 회전하지 않는 기기에는 아무런 영향을 미치지 않습니다. 디스플레이가 세로 반전 방향일 때는 멀티 윈도우 모드가 지원되지 않습니다. |
기기에서 가능한 방향만 지원
기기에서 기본적으로 지원되는 방향만 지원합니다(세로 반전/가로 반전이 포함되거나 포함되지 않을 수 있음).
시나리오 | 가이드라인 | 멀티 윈도우 화면 분할 모드 |
---|---|---|
방향이 잠금 해제됨 | Activity 의 onCreate() 콜백 등에서 Activity 가 생성될 때마다 사용 사례를 설정합니다. |
|
DisplayListener 의 onDisplayChanged() 를 사용합니다. 콜백 내에서 사용 사례의 타겟 회전(예: 기기가 180도 회전할 때)을 업데이트합니다.
|
기기가 회전(예: 90도)할 때 Activity 가 다시 생성되지 않는 사례도 처리됩니다. 이 같은 경우는 앱이 화면의 절반까지 차지하는 소형 폼 팩터 기기와 앱이 화면의 2/3까지 차지하는 대형 기기에서 발생합니다.
|
|
방향이 잠김 | Activity 의 onCreate() 콜백 등에서 Activity 가 처음 생성될 때 사용 사례를 한 번만 설정합니다. |
|
OrientationEventListener 의 onOrientationChanged() 를 사용합니다.
콜백 내에서 사용 사례의 타겟 회전을 업데이트합니다.
|
기기가 회전(예: 90도)할 때 Activity 가 다시 생성되지 않는 사례도 처리됩니다. 이 같은 경우는 앱이 화면의 절반까지 차지하는 소형 폼 팩터 기기와 앱이 화면의 2/3까지 차지하는 대형 기기에서 발생합니다.
|
|
방향 구성 변경이 재정의됨 | Activity 의 onCreate() 콜백 등에서 Activity 가 처음 생성될 때 사용 사례를 한 번만 설정합니다. |
|
DisplayListener 의 onDisplayChanged() 를 사용합니다. 콜백 내에서 사용 사례의 타겟 회전(예: 기기가 180도 회전할 때)을 업데이트합니다.
|
기기가 회전(예: 90도)할 때 Activity 가 다시 생성되지 않는 사례도 처리됩니다. 이 같은 경우는 앱이 화면의 절반까지 차지하는 소형 폼 팩터 기기와 앱이 화면의 2/3까지 차지하는 대형 기기에서 발생합니다.
|
방향이 잠금 해제됨
Activity
는 세로 또는 가로 같은 디스플레이 방향이 기기의 물리적 방향과 일치할 때 방향이 잠금 해제됩니다. 단, 일부 기기에서 기본적으로 지원하지 않는 세로 반전/가로 반전은 제외됩니다. 기기가 4개의 모든 방향으로 회전하도록 강제하려면 Activity
의 screenOrientation
속성을 fullSensor
로 설정합니다.
멀티 윈도우 모드에서 기본적으로 세로 반전/가로 반전을 지원하지 않는 기기는 screenOrientation
속성이 fullSensor
로 설정되더라도 세로 반전/가로 반전으로 회전하지 않습니다.
<!-- The Activity has an unlocked orientation, but might not rotate to reverse portrait/landscape in single-window mode if the device doesn't support it by default. --> <activity android:name=".UnlockedOrientationActivity" /> <!-- The Activity has an unlocked orientation, and will rotate to all four orientations in single-window mode. --> <activity android:name=".UnlockedOrientationActivity" android:screenOrientation="fullSensor" />
방향이 잠김
디스플레이는 기기의 물리적 방향과 관계없이 동일한 디스플레이 방향(세로나 가로 등)에 계속 놓이면 방향이 잠깁니다. 이렇게 하려면 AndroidManifest.xml
파일의 선언 내에 Activity
의 screenOrientation
속성을 지정하면 됩니다.
디스플레이 방향이 잠기면 기기가 회전할 때 시스템은 Activity
를 삭제하거나 다시 생성하지 않습니다.
<!-- The Activity keeps a portrait orientation even as the device rotates. --> <activity android:name=".LockedOrientationActivity" android:screenOrientation="portrait" />
방향 구성 변경이 재정의됨
Activity
가 방향 구성 변경을 재정의하면 기기의 물리적 방향이 변경될 때 시스템은 이 요소를 삭제하거나 다시 생성하지 않습니다.
하지만 시스템은 기기의 물리적 방향과 일치하도록 UI를 업데이트합니다.
<!-- The Activity's UI might not rotate in reverse portrait/landscape if the device doesn't support it by default. --> <activity android:name=".OrientationConfigChangesOverriddenActivity" android:configChanges="orientation|screenSize" /> <!-- The Activity's UI will rotate to all 4 orientations in single-window mode. --> <activity android:name=".OrientationConfigChangesOverriddenActivity" android:configChanges="orientation|screenSize" android:screenOrientation="fullSensor" />
카메라 사용 사례 설정
위에 설명된 시나리오에서는 Activity
가 처음 생성될 때 카메라 사용 사례를 설정할 수 있습니다.
방향이 잠금 해제된 Activity
의 경우 방향 변경 시 시스템이 Activity
를 삭제하고 다시 생성하기 때문에 기기가 회전할 때마다 이 설정이 수행됩니다. 그 결과 사용 사례에서 그때마다 디스플레이 방향과 기본적으로 일치하도록 타겟 회전이 설정됩니다.
방향이 잠겨 있거나 방향 구성 변경을 재정의하는 Activity
의 경우 Activity
가 처음 생성될 때 이 설정이 한 번 실행됩니다.
class CameraActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) val cameraProcessFuture = ProcessCameraProvider.getInstance(this) cameraProcessFuture.addListener(Runnable { val cameraProvider = cameraProcessFuture.get() // By default, the use cases set their target rotation to match the // display’s rotation. val preview = buildPreview() val imageAnalysis = buildImageAnalysis() val imageCapture = buildImageCapture() cameraProvider.bindToLifecycle( this, cameraSelector, preview, imageAnalysis, imageCapture) }, mainExecutor) } }
OrientationEventListener 설정
OrientationEventListener
를 사용하면 기기 방향이 변경될 때 카메라 사용 사례의 타겟 회전을 지속적으로 업데이트할 수 있습니다.
class CameraActivity : AppCompatActivity() { private val orientationEventListener by lazy { object : OrientationEventListener(this) { override fun onOrientationChanged(orientation: Int) { if (orientation == ORIENTATION_UNKNOWN) { return } val rotation = when (orientation) { in 45 until 135 -> Surface.ROTATION_270 in 135 until 225 -> Surface.ROTATION_180 in 225 until 315 -> Surface.ROTATION_90 else -> Surface.ROTATION_0 } imageAnalysis.targetRotation = rotation imageCapture.targetRotation = rotation } } } override fun onStart() { super.onStart() orientationEventListener.enable() } override fun onStop() { super.onStop() orientationEventListener.disable() } }
DisplayListener 설정
DisplayListener
를 사용하면, 기기가 180도 회전한 다음 시스템에서 Activity
를 삭제하거나 다시 생성하지 않는 특정 상황 등에서 카메라 사용 사례의 타겟 회전을 업데이트할 수 있습니다.
class CameraActivity : AppCompatActivity() { private val displayListener = object : DisplayManager.DisplayListener { override fun onDisplayChanged(displayId: Int) { if (rootView.display.displayId == displayId) { val rotation = rootView.display.rotation imageAnalysis.targetRotation = rotation imageCapture.targetRotation = rotation } } override fun onDisplayAdded(displayId: Int) { } override fun onDisplayRemoved(displayId: Int) { } } override fun onStart() { super.onStart() val displayManager = getSystemService(Context.DISPLAY_SERVICE) as DisplayManager displayManager.registerDisplayListener(displayListener, null) } override fun onStop() { super.onStop() val displayManager = getSystemService(Context.DISPLAY_SERVICE) as DisplayManager displayManager.unregisterDisplayListener(displayListener) } }