Rotacja przypadków użycia w aplikacji CameraX

W tym artykule pokażemy, jak skonfigurować przypadki użycia w aplikacji CameraX, aby uzyskać obrazy z prawidłowymi informacjami o obrócie niezależnie od tego, czy pochodzą z zastosowania ImageAnalysis czy ImageCapture. A więc:

  • Element Analyzer przypadku użycia ImageAnalysis powinien otrzymywać ramki z prawidłowym obrotem.
  • Funkcja ImageCapture powinna robić zdjęcia z prawidłowym obrotem.

Terminologia

W tym temacie wykorzystano podaną niżej terminologię, dlatego ważne jest, aby zrozumieć, co oznaczają poszczególne terminy:

Orientacja wyświetlacza
Określa ono, po której stronie urządzenia jest góra i może mieć jedną z 4 wartości: pionowa, pozioma, odwrotna pionowa lub odwrócona poziomo.
Obrót wyświetlacza
Jest to wartość zwracana przez Display.getRotation(), która reprezentuje stopnie, o jakie urządzenie jest obrócone w lewo względem naturalnej orientacji.
Rotacja docelowa
Jest to liczba stopni, o którą należy obrócić urządzenie w prawo, aby uzyskać orientację.

Jak określić rotację docelową

W przykładach poniżej pokazujemy, jak określić docelowy obrót urządzenia na podstawie jego naturalnej orientacji.

Przykład 1. Naturalna orientacja pionowa

Przykład urządzenia: Pixel 3 XL

Orientacja naturalna = Orientacja pionowa
Bieżąca orientacja = Pionowa

Rotacja reklam = 0
Rotacja docelowa = 0

Orientacja naturalna = Orientacja pionowa
Bieżąca orientacja = Pozioma

Rotacja reklam = 90
Rotacja docelowa = 90

Przykład 2: naturalna orientacja pozioma

Przykład urządzenia: Pixel C

Orientacja naturalna = Orientacja pozioma
Obecna orientacja = Orientacja pozioma

Rotacja reklam = 0
Rotacja docelowa = 0

Orientacja naturalna = Orientacja pozioma
Bieżąca orientacja = Pionowa

Rotacja reklam displayowych = 270
Rotacja docelowa = 270

Obrót obrazu

Który koniec? Orientacja czujnika w Androidzie jest definiowana jako wartość stała, która reprezentuje stopnie (0, 90, 180, 270), o których czujnik jest obrócony od górnej krawędzi urządzenia, gdy znajduje się ono w naturalnym położeniu. We wszystkich przypadkach na diagramach rotacja obrazu określa, jak należy obracać dane w prawo, aby były wyświetlane pionowo.

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

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

Przykład urządzenia: Pixel 3 XL

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

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

Przykład 2. Czujnik został obrócony o 270 stopni

Przykład urządzenia: Nexus 5X

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

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

Przykład 3. Czujnik został obrócony o 0 stopni

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

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

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

Obliczanie obrotu obrazu

Analiza obrazu

Urządzenie Analyzer urządzenia ImageAnalysis otrzymuje z aparatu obrazy w postaci ImageProxy s. Każdy obraz zawiera informacje o obrotie dostępne przez:

val rotation = imageProxy.imageInfo.rotationDegrees

Ta wartość określa stopnie, o jakie trzeba obracać obraz w prawo, by zapewnić zgodność z docelowym obrotem (ImageAnalysis). W kontekście aplikacji na Androida docelowa rotacja w ImageAnalysis zwykle odpowiada orientacji ekranu.

Robienie zdjęć

Wywołanie zwrotne jest dołączone do instancji ImageCapture, aby zasygnalizować, że wynik przechwytywania jest gotowy. Przyczyną może być zrobienie zdjęcia lub błąd.

Podczas robienia zdjęcia możesz użyć jednego z tych wywołań zwrotnych:

  • OnImageCapturedCallback: odbiera obraz z dostępem w pamięci w postaci ImageProxy.
  • OnImageSavedCallback: wywoływany po zapisaniu przechwyconego obrazu w lokalizacji podanej w polu ImageCapture.OutputFileOptions. Opcje mogą zawierać pole File, OutputStream lub lokalizację w polu MediaStore.

Obrót przechwyconego obrazu, niezależnie od jego formatu (ImageProxy, File, OutputStream, MediaStore Uri) reprezentuje stopień obrotu, o który trzeba obrócić zdjęcie w prawo, aby dopasować je do docelowego obrotu w trybie ImageCapture. Trzeba to powtarzać w kontekście aplikacji na Androida i dopasować ją do orientacji ekranu.

Informacje o obrócie obrazu można pobrać w 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

Przypadki użycia ImageAnalysis i ImageCapture otrzymują zdarzenia ImageProxy z kamery po pomyślnym żądaniu zapisu. Element ImageProxy zawija obraz i informacje o nim, w tym jego obrót. Ta informacja o obrócie reprezentuje stopnie, o jakie należy obrócić obraz, aby dopasować go do docelowej rotacji w danym przypadku użycia.

Proces weryfikacji rotacji obrazu

Wskazówki dotyczące rotacji celów przechwytywania obrazu/analizy obrazów

Ponieważ wiele urządzeń domyślnie nie obraca się do pozycji pionowej lub poziomej, niektóre aplikacje na Androida ich nie obsługują. Określa, czy aplikacja ją obsługuje czy nie zmienia sposób aktualizowania rotacji docelowej w przypadku użycia.

Poniżej znajdziesz 2 tabele określające, jak zachować synchronizację rotacji docelowej w przypadku poszczególnych przypadków użycia z rotacją reklam displayowych. Pierwszy z nich pokazuje, jak to zrobić przy obsłudze wszystkich 4 orientacji, a drugi obsługuje tylko orientacje, do których urządzenie jest domyślnie obracane.

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

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

  2. Zdecyduj, czy kamera Activity w Twojej aplikacji ma obsługiwać wszystkie 4 orientacje urządzenia (pionowa, pionowa, pionowa, pozioma i odwrotna), czy też domyślnie powinna obsługiwać tylko orientacje obsługiwane przez urządzenie, na którym działa.

Obsługuj wszystkie 4 orientacje

W tej tabeli znajdziesz pewne wskazówki, których należy przestrzegać, gdy urządzenie nie może obrócić się do pozycji pionowej. To samo dotyczy urządzeń, które nie obracają ekranu w orientacji poziomej.

Scenariusz Wskazówki Tryb jednego okna Tryb podzielonego ekranu w trybie wielu okien
Odblokowana orientacja Skonfiguruj przypadki użycia za każdym razem, gdy tworzony jest Activity, na przykład w wywołaniu zwrotnym onCreate() dla Activity.
Użyj usługi onOrientationChanged() usługi OrientationEventListener. W wywołaniu zwrotnym zaktualizuj rotację docelową przypadków użycia. Dotyczy to sytuacji, w których system nie odtwarza Activity nawet po zmianie orientacji, np. gdy urządzenie jest obrócone o 180 stopni. Obsługuje też tryb, gdy wyświetlacz jest w odwrotnej orientacji pionowej, a urządzenie domyślnie nie obraca się do tej orientacji. Uwzględnia też sytuacje, gdy Activity nie jest odtwarzany, gdy urządzenie obróci się (np. o 90 stopni). Dotyczy to małych urządzeń, gdy aplikacja zajmuje połowę ekranu, i większych, gdy zajmuje dwie trzecie ekranu.
Opcjonalnie: ustaw właściwość screenOrientation elementu Activity na fullSensor w pliku AndroidManifest. Dzięki temu interfejs użytkownika jest ustawiony pionowo, gdy urządzenie jest ustawione w odwrotnej orientacji pionowej, a system może odtworzyć element Activity za każdym razem, gdy urządzenie zostanie obrócone o 90 stopni. Nie ma wpływu na urządzenia, które domyślnie nie obracają ekranu do orientacji pionowej. Tryb wielu okien nie jest obsługiwany, gdy wyświetlacz jest w odwrotnej orientacji pionowej.
Zablokowana orientacja Przypadki użycia skonfiguruj tylko raz, podczas pierwszego tworzenia zasobu Activity, na przykład w wywołaniu zwrotnym onCreate() interfejsu Activity.
Użyj usługi onOrientationChanged() usługi OrientationEventListener. W wywołaniu zwrotnym zaktualizuj rotację docelową przypadków użycia oprócz podglądu. Uwzględnia też sytuacje, gdy Activity nie jest odtwarzany, gdy urządzenie obróci się (np. o 90 stopni). Dotyczy to małych urządzeń, gdy aplikacja zajmuje połowę ekranu, i większych, gdy zajmuje dwie trzecie ekranu.
Zastąpienie konfiguracji configChanges Przypadki użycia skonfiguruj tylko raz, podczas pierwszego tworzenia zasobu Activity, na przykład w wywołaniu zwrotnym onCreate() interfejsu Activity.
Użyj usługi onOrientationChanged() usługi OrientationEventListener. W wywołaniu zwrotnym zaktualizuj rotację docelową przypadków użycia. Uwzględnia też sytuacje, gdy Activity nie jest odtwarzany, gdy urządzenie obróci się (np. o 90 stopni). Dotyczy to małych urządzeń, gdy aplikacja zajmuje połowę ekranu, i większych, gdy zajmuje dwie trzecie ekranu.
Opcjonalnie: w pliku AndroidManifest ustaw właściwość screenOrientation aktywności na fullSensor. Pozwala ustawić interfejs pionowo, gdy urządzenie jest ustawione w odwrotnej orientacji. Nie ma wpływu na urządzenia, które domyślnie nie obracają ekranu do orientacji pionowej. Tryb wielu okien nie jest obsługiwany, gdy wyświetlacz jest w odwrotnej orientacji pionowej.

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

Obsługuje tylko orientacje, które urządzenie domyślnie obsługuje (może nie obejmować odwrotnej orientacji poziomej/pionowej).

Scenariusz Wskazówki Tryb podzielonego ekranu w trybie wielu okien
Odblokowana orientacja Skonfiguruj przypadki użycia za każdym razem, gdy tworzony jest Activity, na przykład w wywołaniu zwrotnym onCreate() dla Activity.
Użyj usługi onDisplayChanged() usługi DisplayListener. W wywołaniu zwrotnym zaktualizuj docelową rotację przypadków użycia, np. przy obróceniu urządzenia o 180 stopni. Uwzględnia też sytuacje, gdy Activity nie jest odtwarzany, gdy urządzenie obróci się (np. o 90 stopni). Dotyczy to małych urządzeń, gdy aplikacja zajmuje połowę ekranu, i większych, gdy zajmuje dwie trzecie ekranu.
Zablokowana orientacja Przypadki użycia skonfiguruj tylko raz, podczas pierwszego tworzenia zasobu Activity, na przykład w wywołaniu zwrotnym onCreate() interfejsu Activity.
Użyj usługi onOrientationChanged() usługi OrientationEventListener. W wywołaniu zwrotnym zaktualizuj rotację docelową przypadków użycia. Uwzględnia też sytuacje, gdy Activity nie jest odtwarzany, gdy urządzenie obróci się (np. o 90 stopni). Dotyczy to małych urządzeń, gdy aplikacja zajmuje połowę ekranu, i większych, gdy zajmuje dwie trzecie ekranu.
Zastąpienie konfiguracji configChanges Przypadki użycia skonfiguruj tylko raz, podczas pierwszego tworzenia zasobu Activity, na przykład w wywołaniu zwrotnym onCreate() interfejsu Activity.
Użyj usługi onDisplayChanged() usługi DisplayListener. W wywołaniu zwrotnym zaktualizuj docelową rotację przypadków użycia, np. przy obróceniu urządzenia o 180 stopni. Uwzględnia też sytuacje, gdy Activity nie jest odtwarzany, gdy urządzenie obróci się (np. o 90 stopni). Dotyczy to małych urządzeń, gdy aplikacja zajmuje połowę ekranu, i większych, gdy zajmuje dwie trzecie ekranu.

Orientacja odblokowana

Urządzenie Activity ma odblokowaną orientację, gdy orientacja wyświetlacza (np. pionowa lub pozioma) odpowiada fizycznej orientacji urządzenia. Wyjątkiem są odwrotne orientacja pozioma/pionowa, której niektóre urządzenia nie obsługują domyślnie. Aby wymusić obrócenie urządzenia do wszystkich 4 orientacji, ustaw właściwość screenOrientation elementu Activity na fullSensor.

W trybie wielu okien urządzenie, które domyślnie nie obsługuje orientacji poziomej/pionowej, nie zostanie obrócone do orientacji poziomej/pionowej, 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" />

Zablokowanie orientacji

Wyświetlacz ma zablokowaną orientację, gdy pozostaje w tej samej orientacji (np. pionowa lub pozioma) niezależnie od fizycznej orientacji urządzenia. Można to zrobić, określając właściwość screenOrientation elementu Activity w deklaracji w pliku AndroidManifest.xml.

Jeśli wyświetlacz ma zablokowaną orientację, system nie zniszczy ani nie utworzy elementu Activity przy obracaniu urządzenia.

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

Zastąpienie zmian w konfiguracji orientacji

Gdy Activity zastępuje zmianę konfiguracji orientacji, system nie zniszczy go ani nie odtworzy po zmianie fizycznej orientacji urządzenia. System aktualizuje jednak 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" />

Konfiguracja przypadków użycia kamery

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

W przypadku obiektu Activity z odblokowaną orientacją konfiguracja jest wykonywana przy każdym obróceniu urządzenia, ponieważ system niszczy i odtwarza Activity po zmianie orientacji. Powoduje to, że za każdym razem rotacja docelowa jest domyślnie zgodna z orientacją ekranu.

W przypadku obiektu Activity z zablokowaną orientacją lub takim, który zastępuje zmiany konfiguracji orientacji, konfiguracja jest wykonywana raz przy pierwszym utworzeniu obiektu 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)
   }
}

Konfiguracja OrientationEventListener

Za pomocą OrientationEventListener można stale aktualizować docelowy obrót przypadków użycia kamery wraz ze zmianą 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

Użycie obiektu DisplayListener pozwala zaktualizować docelowy obrót kamery w określonych sytuacjach, np. gdy system nie niszczy, a potem odtwarza Activity po obróceniu 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)
    }
}