Rotacja przypadków użycia w aplikacji CameraX

W tym temacie pokazujemy, jak skonfigurować przypadki użycia CameraX w aplikacji, aby uzyskać obrazy z poprawnymi informacjami o obrocie, niezależnie od tego, czy pochodzą one z przypadku użycia ImageAnalysis czy ImageCapture. A więc:

  • W przypadku użycia ImageAnalysis Analyzer powinny otrzymywać ramki z prawidłowym obracaniem.
  • W przypadku ImageCapture zdjęcia powinny być robione z właściwym obracaniem.

Terminologia

W tym temacie używamy następującej terminologii:

Orientacja wyświetlacza
Określa, która strona urządzenia jest skierowana ku górze. Może to być jedna z 4 wartości: pion, poziom, odwrócony pion lub odwrócony poziom.
Obrót wyświetlacza
To wartość zwracana przez Display.getRotation(), która wskazuje, o ile stopni urządzenie zostało obrócone w przeciwnym kierunku od ruchu wskazówek zegara od swojej naturalnej orientacji.
Docelowa rotacja
Pozwala określić, o ile stopni należy obrócić urządzenie zgodnie z kierunkiem wskazówek zegara, aby uzyskać jego naturalną orientację.

Jak określić docelową rotację

Poniższe przykłady pokazują, jak określić docelową orientację urządzenia na podstawie jego naturalnej orientacji.

Przykład 1. Pionowa orientacja naturalna

Przykład urządzenia: Pixel 3 XL

Naturalna orientacja = Pionowa
Obecna orientacja = Pionowa

Obrót wyświetlacza = 0
Obrót docelowy = 0

Naturalna orientacja = pionowa
Obecna orientacja = pozioma

Obrót wyświetlacza = 90
Docelowy obrót = 90

Przykład 2. Naturalna orientacja pozioma

Przykład urządzenia: Pixel C

Naturalna orientacja = pozioma
Obecna orientacja = pozioma

Obrót wyświetlacza = 0
Obrót docelowy = 0

Naturalna orientacja = pozioma
Obecna orientacja = pionowa

Obrót wyświetlacza = 270
Obrót docelowy = 270

Obrót obrazu

Który koniec jest górą? Kierunek działania czujnika jest zdefiniowany w Androidzie jako wartość statyczna, która reprezentuje kąt (0, 90, 180, 270) odchylenia czujnika od górnej części urządzenia, gdy urządzenie jest w naturalnej pozycji. W przypadku wszystkich przypadków na diagramach obrót obrazu pokazuje, jak należy obrócić dane zgodnie z kierunkiem wskazówek zegara, aby były widoczne w poziomie.

Poniższe przykłady pokazują, jaką obrót powinien mieć obraz w zależności od orientacji czujnika aparatu. Zakładają też, że rotacja docelowa jest ustawiona na rotację wyświetlania.

Przykład 1. Czujnik obrócony o 90 stopni

Przykład urządzenia: Pixel 3 XL

Obrót wyświetlacza = 0
Orientacja wyświetlacza = Pion
Obrót obrazu = 90

Obrót wyświetlacza = 90
Orientacja wyświetlacza = pozioma
Obrót obrazu = 0

Przykład 2. Czujnik obrócony o 270°

Przykład urządzenia: Nexus 5X

Obrót wyświetlacza = 0
Orientacja wyświetlacza = Pion
Obrót obrazu = 270

Obrót wyświetlacza = 90
Orientacja wyświetlacza = Pionowa
Obrót obrazu = 180

Przykład 3. Czujnik obrócony o 0°

Przykład urządzenia: Pixel C (tablet)

Obrót wyświetlacza = 0
Orientacja wyświetlacza = Pion
Obrót obrazu = 0

Obrót wyświetlacza = 270
Orientacja wyświetlacza = Pion
Obrót obrazu = 90

Obliczanie obrotu obrazu

ImageAnalysis

Urządzenie ImageAnalysis Analyzer otrzymuje obrazy z aparatu w formie ImageProxy. Każdy obraz zawiera informacje o obrocie, które są dostępne:

val rotation = imageProxy.imageInfo.rotationDegrees

Ta wartość określa, o ile stopni należy obrócić obraz w kierunku zgodnym z kierunkiem ruchu wskazówek zegara, aby pasował do docelowego obrotu ImageAnalysis. W kontekście aplikacji na Androida docelowe ustawienie ImageAnalysis zwykle odpowiada orientacji ekranu.

ImageCapture

Do instancji ImageCapture jest dołączone wywołanie zwrotne, które sygnalizuje, że wynik przechwytywania jest gotowy. Wynikiem może być zarejestrowany obraz lub błąd.

Podczas robienia zdjęcia podany wywoływany zwrotnie adres może być jednym z tych typów:

  • OnImageCapturedCallback: otrzymuje obraz z dostępem do pamięci w postaci ImageProxy.
  • OnImageSavedCallback: wywoływana, gdy przechwycony obraz został zapisany w miejscu określonym przez parametr ImageCapture.OutputFileOptions. Opcje mogą określać File, OutputStream lub lokalizację w MediaStore.

Obrót obrazu, niezależnie od jego formatu (ImageProxy, File, OutputStream, MediaStore Uri), oznacza kąt obrotu w stopniach, o ile obraz musi zostać obrócony zgodnie z kierunkiem obrotu ImageCapture, który w kontekście aplikacji na Androida zwykle odpowiada orientacji ekranu.

Obraz można obrócić na jeden z tych sposobów:

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

Sprawdzanie rotacji obrazu

W przypadku ImageAnalysisImageCapture żądanie wykonania zdjęcia jest wysyłane do kamery, a kamera wysyła ImageProxy po wykonaniu zdjęcia. ImageProxy zawiera obraz i informacje o nim, w tym jego obrót. Te informacje o obrocie wskazują, o ile stopni należy obrócić obraz, aby pasował do docelowego ustawienia.

Proces weryfikacji rotacji obrazu

Wytyczne dotyczące obracania docelowego obiektu w ImageCapture/ImageAnalysis

Wiele urządzeń nie obraca ekranu w tryb odwróconego pionu lub odwróconego poziomego domyślnie, dlatego niektóre aplikacje na Androida nie obsługują tych orientacji. To, czy aplikacja obsługuje tę funkcję, wpływa na sposób aktualizowania docelowej rotacji w przypadku użycia.

Poniżej znajdują się 2 tabele określające, jak zachować spójność docelowej rotacji przypadków użycia z rotacją wyświetlania. Pierwszy pokazuje, jak to zrobić, obsługując wszystkie 4 orientacje; drugi obsługuje tylko orientacje, do których urządzenie przełącza się domyślnie.

Aby wybrać wytyczne, których chcesz przestrzegać w aplikacji:

  1. Sprawdź, czy kamera Activity w aplikacji ma zablokowaną orientację, odblokowaną orientację czy też zastępuje zmiany konfiguracji orientacji.

  2. Zdecyduj, czy aparat w aplikacji Activity ma obsługiwać wszystkie 4 orientacje urządzenia (portret, odwrócony portret, poziom i odwrócony poziom), czy tylko te, które są domyślnie obsługiwane przez urządzenie.

Obsługa wszystkich 4 orientacji

W tej tabeli znajdziesz wskazówki dotyczące sytuacji, gdy urządzenie nie obraca się do orientacji poziomej. To samo dotyczy urządzeń, które nie obracają się do odwróconej orientacji poziomej.

Scenariusz Wskazówki Tryb pojedynczego okna Tryb wielu okien w podzielonym ekranie
Orientacja nieblokowana Konfiguruj przypadki użycia za każdym razem, gdy Activity jest tworzony, np. w funkcji onCreate() wywołania zwrotnego Activity.
Użyj konta OrientationEventListener: onOrientationChanged(). W wywołaniu zwrotnym zaktualizuj docelową rotację przypadków użycia. Rozwiązanie to sprawdza się w przypadkach, gdy system nie odtwarza Activity nawet po zmianie orientacji, np. gdy urządzenie zostanie obrócone o 180 stopni. Obsługuje też wyświetlacz w orientacji pionowej odwróconej o 180°, gdy urządzenie nie obraca się do odwróconej orientacji pionowej domyślnie. Funkcja ta obsługuje też przypadki, w których Activity nie jest ponownie tworzona po obróceniu urządzenia (np. o 90 stopni). Dzieje się tak na urządzeniach o małym formacie, gdy aplikacja zajmuje połowę ekranu, oraz na większych urządzeniach, gdy aplikacja zajmuje 2/3 ekranu.
Opcjonalnie: w pliku AndroidManifest ustaw właściwość screenOrientation elementu Activity na fullSensor. Dzięki temu interfejs będzie wyświetlany w poziomie, gdy urządzenie będzie w orientacji poziomej, a system będzie mógł odtworzyć Activity za każdym razem, gdy urządzenie zostanie obrócone o 90 stopni. Nie ma wpływu na urządzenia, które nie obracają się do orientacji poziomej. Tryb wielu okien nie jest obsługiwany, gdy wyświetlacz jest w orientacji poziomej.
Zablokowana orientacja Konfigurację przypadków użycia należy przeprowadzić tylko raz, podczas tworzenia Activity, np. w przypadku wywołania zwrotnego onCreate()Activity.
Użyj konta OrientationEventListener: onOrientationChanged(). W funkcji wywołania zwrotnego zaktualizuj docelową rotację przypadków użycia z wyjątkiem podglądu. Funkcja ta obsługuje też przypadki, w których Activity nie jest ponownie tworzona po obróceniu urządzenia (np. o 90 stopni). Dzieje się tak na urządzeniach o małym formacie, gdy aplikacja zajmuje połowę ekranu, oraz na większych urządzeniach, gdy aplikacja zajmuje 2/3 ekranu.
Zastąpiono zmiany konfiguracji orientacji Konfigurację przypadków użycia należy przeprowadzić tylko raz, podczas tworzenia Activity, np. w przypadku wywołania zwrotnego onCreate()Activity.
Użyj konta OrientationEventListener: onOrientationChanged(). W funkcji callback zaktualizuj docelową rotację przypadków użycia. Obsługuje też przypadki, w których Activity nie jest ponownie tworzony po obróceniu urządzenia (np. o 90 stopni). Dzieje się tak na urządzeniach o małym formacie, gdy aplikacja zajmuje połowę ekranu, oraz na większych urządzeniach, gdy aplikacja zajmuje 2/3 ekranu.
Opcjonalnie: w pliku AndroidManifest ustaw właściwość screenOrientation aktywności na fullSensor. Umożliwia wyświetlanie interfejsu w poziomie, gdy urządzenie jest w orientacji pionowej odwróconej. Nie ma wpływu na urządzenia, które nie obracają się do orientacji poziomej. Tryb wielu okien nie jest obsługiwany, gdy wyświetlacz jest w orientacji poziomej.

Obsługuj tylko orientacje obsługiwane przez urządzenie.

Obsługa tylko orientacji obsługiwanych domyślnie przez urządzenie (może, ale nie musi, obejmować orientację pionową i poziomą odwróconą).

Scenariusz Wskazówki Tryb wielu okien w podzielonym ekranie
Orientacja nieblokowana Konfiguruj przypadki użycia za każdym razem, gdy Activity jest tworzony, np. w funkcji onCreate() wywołania zwrotnego Activity.
Użyj konta DisplayListener. onDisplayChanged(). W wywołaniu zwrotnym zaktualizuj docelową rotację przypadków użycia, np. gdy urządzenie jest obracane o 180 stopni. Funkcja ta obsługuje też przypadki, w których Activity nie jest ponownie tworzona po obróceniu urządzenia (np. o 90 stopni). Dzieje się tak na urządzeniach o małym formacie, gdy aplikacja zajmuje połowę ekranu, oraz na większych urządzeniach, gdy aplikacja zajmuje 2/3 ekranu.
Zablokowana orientacja Konfigurację przypadków użycia należy przeprowadzić tylko raz, podczas tworzenia Activity, np. w przypadku wywołania zwrotnego onCreate()Activity.
Użyj konta OrientationEventListener: onOrientationChanged(). W wywołaniu zwrotnym zaktualizuj docelową rotację przypadków użycia. Funkcja ta obsługuje też przypadki, w których Activity nie jest ponownie tworzona po obróceniu urządzenia (np. o 90 stopni). Dzieje się tak na urządzeniach o małym formacie, gdy aplikacja zajmuje połowę ekranu, oraz na większych urządzeniach, gdy aplikacja zajmuje 2/3 ekranu.
Zastąpiono zmiany konfiguracji orientacji Konfigurację przypadków użycia należy przeprowadzić tylko raz, podczas tworzenia Activity, np. w przypadku wywołania zwrotnego onCreate()Activity.
Użyj konta DisplayListener. onDisplayChanged(). W wywołaniu zwrotnym zaktualizuj docelową rotację przypadków użycia, np. gdy urządzenie jest obracane o 180 stopni. Funkcja ta obsługuje też przypadki, w których Activity nie jest ponownie tworzona po obróceniu urządzenia (np. o 90 stopni). Dzieje się tak na urządzeniach o małym formacie, gdy aplikacja zajmuje połowę ekranu, oraz na większych urządzeniach, gdy aplikacja zajmuje 2/3 ekranu.

Odblokowana orientacja

Activity ma odblokowaną orientację, gdy orientacja wyświetlacza (np. pionowa lub pozioma) odpowiada fizycznej orientacji urządzenia, z wyjątkiem orientacji pionowej w drugą stronę lub poziomej, których niektóre urządzenia nie obsługują domyślnie. Aby wymusić obrót urządzenia w wszystkie 4 kierunki, ustaw właściwość Activity screenOrientation na fullSensor.

W trybie wielu okien urządzenie, które nie obsługuje odwróconej orientacji poziomej lub pionowej, nie będzie domyślnie obracać się w odwróconą orientację poziomą lub pionową, nawet jeśli właściwość screenOrientation ma wartość 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" />

Zablokowana orientacja

Wyświetlacz ma zablokowaną orientację, gdy pozostaje w tej samej orientacji (np. pionowej lub poziomej) niezależnie od fizycznej orientacji urządzenia. Aby to zrobić, w deklaracji obiektu Activity w pliku AndroidManifest.xml należy określić właściwość screenOrientation.

Gdy wyświetlacz ma zablokowaną orientację, system nie usuwa ani nie tworzy ponownie Activity podczas obracania urządzenia.

<!-- The Activity keeps a portrait orientation even as the device rotates. -->
<activity
   android:name=".LockedOrientationActivity"
   android:screenOrientation="portrait" />

Zmiany konfiguracji orientacji zastąpione

Gdy Activity zastąpi zmiany konfiguracji orientacji, system nie zniszczy go ani nie utworzy ponownie, gdy zmieni się fizyczna orientacja urządzenia. System aktualizuje interfejs, aby dopasować go do fizycznej orientacji urządzenia.

<!-- 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" />

Konfigurowanie przypadków użycia aparatu

W opisanych powyżej scenariuszach przypadki użycia kamery można skonfigurować podczas tworzenia Activity.

W przypadku Activity z odblokowaną orientacją ta konfiguracja jest wykonywana za każdym razem, gdy urządzenie zostanie obrócone, ponieważ system niszczy i tworzy ponownie Activity po zmianie orientacji. W efekcie w przypadku każdego zastosowania domyślnie ustawiana jest rotacja dopasowana do orientacji wyświetlacza.

W przypadku Activity z zablokowaną orientacją lub takiego, który zastępuje zmiany konfiguracji orientacji, ta konfiguracja jest wykonywana raz, gdy Activity jest tworzony po raz pierwszy.

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)
   }
}

Konfiguracja OrientationEventListener

Korzystanie z OrientationEventListener umożliwia ciągłe aktualizowanie docelowego ustawienia obrotu kamery w zależności od orientacji urządzenia.

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()
    }
}

Konfiguracja DisplayListener

Korzystanie z DisplayListener umożliwia aktualizowanie docelowego obrotu kamery w pewnych sytuacjach, np. gdy system nie zniszczy i nie utworzy ponownie Activity po obróbieniu urządzenia o 180 stopni.

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)
    }
}