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 Obrót wyświetlacza = 0 |
|
Naturalna orientacja = pionowa Obrót wyświetlacza = 90 |
Przykład 2. Naturalna orientacja pozioma
Przykład urządzenia: Pixel C | |
---|---|
Naturalna orientacja = pozioma Obrót wyświetlacza = 0 |
|
Naturalna orientacja = pozioma Obrót wyświetlacza = 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 |
|
Obrót wyświetlacza = 90 |
Przykład 2. Czujnik obrócony o 270°
Przykład urządzenia: Nexus 5X | |
---|---|
Obrót wyświetlacza = 0 |
|
Obrót wyświetlacza = 90 |
Przykład 3. Czujnik obrócony o 0°
Przykład urządzenia: Pixel C (tablet) | |
---|---|
Obrót wyświetlacza = 0 |
|
Obrót wyświetlacza = 270 |
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 postaciImageProxy
.OnImageSavedCallback
: wywoływana, gdy przechwycony obraz został zapisany w miejscu określonym przez parametrImageCapture.OutputFileOptions
. Opcje mogą określaćFile
,OutputStream
lub lokalizację wMediaStore
.
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 ImageAnalysis
i ImageCapture
żą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.
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:
Sprawdź, czy kamera
Activity
w aplikacji ma zablokowaną orientację, odblokowaną orientację czy też zastępuje zmiany konfiguracji orientacji.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() w 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() w 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() w 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() w 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) } }