Orientacje aparatu

Jeśli aplikacja na Androida korzysta z aparatów, podczas obsługi orientacji należy wziąć pod uwagę kilka kwestii. Zakładamy, że znasz podstawowe pojęcia związane z interfejsem camera2 API Androida. Przeczytaj nasz post na blogu lub podsumowanie, aby dowiedzieć się więcej o camera2. Zanim zaczniesz czytać ten dokument, zalecamy najpierw spróbować napisać aplikację aparatu.

Tło

Obsługa orientacji w aplikacjach aparatu na Androida jest skomplikowana i wymaga uwzględnienia tych czynników:

  • Orientacja naturalna: orientacja wyświetlacza, gdy urządzenie jest w „normalnej” pozycji dla jego konstrukcji – zwykle pionowej w przypadku telefonów komórkowych i poziomej w przypadku laptopów.
  • Orientacja czujnika: orientacja czujnika zamontowanego fizycznie na urządzeniu.
  • Obrót wyświetlacza: o ile urządzenie jest fizycznie obrócone względem naturalnej orientacji.
  • Rozmiar wizjera: rozmiar wizjera używanego do wyświetlania podglądu z aparatu.
  • Rozmiar obrazu wyjściowego z aparatu.

Połączenie tych czynników daje dużą liczbę możliwych konfiguracji interfejsu i podglądu w aplikacjach aparatu. Ten dokument ma na celu pokazanie programistom, jak poruszać się po tych kwestiach i prawidłowo obsługiwać orientację kamery w aplikacjach na Androida.

Aby uprościć sprawę, przyjmij, że wszystkie przykłady dotyczą tylnego aparatu, chyba że zaznaczono inaczej. Dodatkowo wszystkie poniższe zdjęcia są symulowane, aby ilustracje były bardziej przejrzyste.

Wszystko o orientacjach

Orientacja naturalna

Naturalna orientacja to orientacja wyświetlacza, gdy urządzenie znajduje się w pozycji, w której zwykle się go używa. W przypadku telefonów naturalną orientacją jest często orientacja pionowa. Innymi słowy, telefony są węższe i dłuższe. W przypadku laptopów naturalna orientacja to pozioma, co oznacza, że mają one większą szerokość i mniejszą wysokość. W przypadku tabletów jest to nieco bardziej skomplikowane – mogą one być używane w orientacji pionowej lub poziomej.

Ilustracja przedstawiająca orientację naturalną z telefonem, laptopem i obiektem z perspektywy obserwatora

Orientacja czujnika

Formalnie orientację czujnika mierzy się w stopniach, o które obraz wyjściowy z czujnika musi zostać obrócony w prawo, aby pasował do naturalnej orientacji urządzenia. Inaczej mówiąc, orientacja czujnika to liczba stopni, o którą czujnik jest obrócony w kierunku przeciwnym do ruchu wskazówek zegara przed zamontowaniem na urządzeniu. Gdy patrzysz na ekran, obrót wydaje się następować w kierunku zgodnym z ruchem wskazówek zegara, ponieważ czujnik tylnego aparatu jest zainstalowany z tyłu urządzenia.

Zgodnie z definicją zgodności Androida 10 w sekcji 7.5.5 Orientacja aparatu aparaty przednie i tylne „MUSZĄ być zorientowane tak, aby dłuższy bok aparatu był równoległy do dłuższego boku ekranu”.

Bufory wyjściowe z kamer są w orientacji poziomej. Naturalną orientacją telefonów jest zwykle orientacja pionowa, więc orientacja czujnika jest zwykle o 90 lub 270 stopni inna niż naturalna, aby dłuższy bok bufora wyjściowego pasował do dłuższego boku ekranu. Orientacja czujnika jest inna w przypadku urządzeń, których naturalna orientacja jest pozioma, np. Chromebooków. W tych urządzeniach czujniki obrazu są ponownie umieszczone tak, aby dłuższy bok bufora wyjściowego pasował do dłuższego boku ekranu. Oba te urządzenia mają rozmiar poziomy, więc orientacje są zgodne, a orientacja czujnika wynosi 0 lub 180 stopni.

Ilustracja przedstawiająca orientację naturalną z telefonem, laptopem i obiektem z perspektywy obserwatora

Ilustracje poniżej pokazują, jak wygląda to z punktu widzenia obserwatora patrzącego na ekran urządzenia:

Ilustracja przedstawiająca orientację czujnika z telefonem, laptopem i obiektem z perspektywy obserwatora

Rozważmy taką sytuację:

Scena z uroczą figurką Androida (bugdroidem)

Telefon Laptop
Ilustracja przedstawiająca widok z tylnego aparatu telefonu Ilustracja przedstawiająca widok z tylnego aparatu laptopa

Orientacja czujnika w telefonach wynosi zwykle 90 lub 270 stopni. Bez uwzględnienia orientacji czujnika zdjęcia wyglądałyby tak:

Telefon Laptop
Ilustracja przedstawiająca widok z tylnego aparatu telefonu Ilustracja przedstawiająca widok z tylnego aparatu laptopa

Załóżmy, że orientacja czujnika przeciwna do ruchu wskazówek zegara jest przechowywana w zmiennej sensorOrientation. Aby skompensować orientację czujnika, musisz obrócić bufory wyjściowe o `sensorOrientation` zgodnie z ruchem wskazówek zegara, aby przywrócić orientację zgodną z naturalną orientacją urządzenia.

W Androidzie aplikacje mogą wyświetlać podgląd z aparatu za pomocą TextureView lub SurfaceView. Obie te metody mogą obsługiwać orientację czujnika, jeśli aplikacje prawidłowo z nich korzystają. W kolejnych sekcjach wyjaśnimy, jak uwzględnić orientację czujnika.

Obrót wyświetlacza

Obrót wyświetlacza jest formalnie definiowany przez obrót rysowanej grafiki na ekranie, który jest przeciwny do fizycznego obrotu urządzenia z jego naturalnej orientacji. W sekcjach poniżej zakładamy, że wszystkie rotacje wyświetlacza są wielokrotnościami 90. Jeśli pobierasz rotację wyświetlacza w stopniach bezwzględnych, zaokrąglij ją do najbliższej wartości z tego zbioru: {0, 90, 180, 270}.

„Orientacja wyświetlacza” w kolejnych sekcjach odnosi się do tego, czy urządzenie jest fizycznie trzymane w pozycji poziomej czy pionowej, i różni się od „obrotu wyświetlacza”.

Załóżmy, że obrócisz urządzenia o 90 stopni w lewo względem ich poprzednich pozycji, jak pokazano na ilustracji poniżej:

Ilustracja obrotu wyświetlacza o 90 stopni z telefonem, laptopem i obiektem z perspektywy obserwatora

Zakładając, że bufory wyjściowe są już obrócone zgodnie z orientacją czujnika, będziesz mieć te bufory wyjściowe:

Telefon Laptop
Ilustracja przedstawiająca widok z tylnego aparatu telefonu Ilustracja przedstawiająca widok z tylnego aparatu laptopa

Jeśli rotacja wyświetlacza jest przechowywana w zmiennej displayRotation, aby uzyskać prawidłowy obraz, należy obrócić bufory wyjściowe o wartość zmiennej displayRotation w kierunku przeciwnym do ruchu wskazówek zegara.

W przypadku przednich aparatów obrót wyświetlacza działa na bufory obrazu w kierunku przeciwnym do kierunku obrotu ekranu. Jeśli używasz przedniego aparatu, obróć bufory o wartość displayRotation w kierunku zgodnym z ruchem wskazówek zegara.

Uwagi

Obrót wyświetlacza mierzy obrót urządzenia w lewo. Nie dotyczy to wszystkich interfejsów API związanych z orientacją i obrotem.

Na przykład

Ważne jest, aby pamiętać, że obrót wyświetlacza jest względny w stosunku do naturalnej orientacji. Jeśli na przykład obrócisz telefon o 90 lub 270 stopni, ekran będzie miał kształt poziomy. Dla porównania, jeśli obrócisz laptopa o taki sam kąt, ekran będzie miał kształt pionowy. Aplikacje powinny zawsze o tym pamiętać i nigdy nie zakładać, że urządzenie jest w określonej orientacji.

Przykłady

Na podstawie poprzednich rysunków wyjaśnimy, czym są orientacje i rotacje.

Ilustracja przedstawiająca połączoną orientację z telefonem i laptopem bez obracania oraz obiektem

Telefon Laptop
Orientacja naturalna = pionowa Naturalna orientacja = pozioma
Orientacja czujnika = 90 Orientacja czujnika = 0
Display Rotation = 0 Display Rotation = 0
Orientacja wyświetlacza = pionowa Orientacja wyświetlacza = pozioma

Ilustracja przedstawiająca połączoną orientację z telefonem i laptopem bez obracania oraz obiektem

Telefon Laptop
Orientacja naturalna = pionowa Naturalna orientacja = pozioma
Orientacja czujnika = 90 Orientacja czujnika = 0
Display Rotation = 90 Display Rotation = 90
Orientacja wyświetlacza = pozioma Orientacja wyświetlacza = pionowa

Rozmiar wizjera

Aplikacje powinny zawsze zmieniać rozmiar wizjera na podstawie orientacji, obrotu i rozdzielczości ekranu. Ogólnie aplikacje powinny ustawiać orientację wizjera identyczną z bieżącą orientacją wyświetlacza. Inaczej mówiąc, aplikacje powinny wyrównywać dłuższą krawędź wizjera z dłuższą krawędzią ekranu.

Rozmiar obrazu wyjściowego z aparatu

Wybierając rozmiar obrazu wyjściowego na potrzeby podglądu, w miarę możliwości wybieraj rozmiar równy rozmiarowi wizjera lub nieco większy. Zwykle nie chcesz, aby bufory wyjściowe były powiększane, ponieważ powoduje to pikselizację. Nie wybieraj też zbyt dużego rozmiaru, ponieważ może to obniżyć wydajność i zużywać więcej baterii.

Orientacja JPEG

Zacznijmy od typowej sytuacji, czyli zrobienia zdjęcia w formacie JPEG. W interfejsie camera2 API możesz przekazać wartość JPEG_ORIENTATION w żądaniu przechwytywania, aby określić, o ile stopni chcesz obrócić wyjściowe pliki JPEG w kierunku zgodnym z ruchem wskazówek zegara.

Krótkie podsumowanie tego, o czym wspomnieliśmy:

  • Aby dostosować orientację czujnika, musisz obrócić bufor obrazu o sensorOrientation stopni w prawo.
  • Aby obsłużyć obracanie wyświetlacza, musisz obrócić bufor o displayRotation w kierunku przeciwnym do ruchu wskazówek zegara w przypadku tylnych aparatów i zgodnie z ruchem wskazówek zegara w przypadku przednich aparatów.

Po dodaniu tych 2 czynników kwota, o którą chcesz obrócić obraz zgodnie z ruchem wskazówek zegara, wynosi

  • sensorOrientation - displayRotation – tylne aparaty.
  • sensorOrientation + displayRotation w przypadku przednich aparatów.

Przykładowy kod tej logiki znajdziesz w dokumentacji JPEG_ORIENTATION. Pamiętaj, że deviceOrientation w przykładowym kodzie w dokumentacji oznacza obrót urządzenia w kierunku zgodnym z ruchem wskazówek zegara. Dlatego znaki obrotu wyświetlacza są odwrócone.

Podgląd

A co z podglądem z aparatu? Aplikacja może wyświetlać podgląd z kamery na 2 główne sposoby: SurfaceView i TextureView. Każdy z nich wymaga innego podejścia, aby prawidłowo obsługiwać orientację.

SurfaceView

W przypadku podglądu z kamery zalecamy używanie SurfaceView, o ile nie musisz przetwarzać ani animować buforów podglądu. Jest wydajniejszy i mniej wymagający pod względem zasobów niż TextureView.

Element SurfaceView jest też stosunkowo łatwiejszy do rozmieszczenia. Musisz tylko zadbać o współczynnik proporcji elementu SurfaceView, na którym wyświetlasz podgląd z kamery.

Źródło

Platforma Android obraca bufory wyjściowe pod elementem SurfaceView, aby dopasować je do orientacji wyświetlacza urządzenia. Innymi słowy, uwzględnia zarówno orientację czujnika, jak i obrót wyświetlacza. Mówiąc prościej, gdy wyświetlacz jest w orientacji poziomej, podgląd też jest w orientacji poziomej, a gdy wyświetlacz jest w orientacji pionowej, podgląd też jest w orientacji pionowej.

Ilustruje to tabela poniżej. Ważne jest, aby pamiętać, że sama rotacja wyświetlacza nie określa orientacji źródła.

Obrót wyświetlacza Telefon (naturalna orientacja = pionowa) Laptop (orientacja naturalna = pozioma)
0 Obraz w formacie pionowym przedstawiający głowę bugdroida skierowaną do góry Obraz w orientacji poziomej z głową bugdroida skierowaną do góry
90 Obraz w orientacji poziomej z głową bugdroida skierowaną do góry Obraz w formacie pionowym przedstawiający głowę bugdroida skierowaną do góry
180 Obraz w formacie pionowym przedstawiający głowę bugdroida skierowaną do góry Obraz w orientacji poziomej z głową bugdroida skierowaną do góry
270 Obraz w orientacji poziomej z głową bugdroida skierowaną do góry Obraz w formacie pionowym przedstawiający głowę bugdroida skierowaną do góry

Układ

Jak widać, SurfaceView już obsługuje niektóre z trudnych kwestii. Musisz jednak wziąć pod uwagę rozmiar wizjera lub to, jak duży ma być podgląd na ekranie. SurfaceView automatycznie skaluje bufor źródłowy, aby dopasować go do swoich wymiarów. Musisz zadbać o to, aby współczynnik proporcji wizjera był identyczny ze współczynnikiem proporcji bufora źródłowego. Jeśli na przykład spróbujesz dopasować podgląd w formacie pionowym do obiektu SurfaceView w formacie poziomym, obraz będzie zniekształcony, jak na tym przykładzie:

Ilustracja przedstawiająca rozciągniętego bugdroida w wyniku dopasowania podglądu w formacie pionowym do wizjera w formacie poziomym

Zazwyczaj współczynnik proporcji (czyli szerokość/wysokość) wizjera powinien być identyczny ze współczynnikiem proporcji źródła. Jeśli nie chcesz przycinać obrazu w wizjerze (odcinać niektórych pikseli, aby poprawić wyświetlanie), rozważ 2 przypadki: gdy aspectRatioActivity jest większe niż aspectRatioSource i gdy jest mniejsze lub równe aspectRatioSource.

aspectRatioActivity > aspectRatioSource

Możesz traktować zgłoszenie jako „szerszą” aktywność. Poniżej rozważymy przykład, w którym masz aktywność w formacie 16:9 i źródło w formacie 4:3.

aspectRatioActivity = 16/9 ≈ 1.78
aspectRatioSource = 4/3 ≈ 1.33

Najpierw ustaw wizjer w formacie 4:3. Następnie dopasuj źródło i wizjer do aktywności w ten sposób:

Ilustracja przedstawiająca aktywność, której współczynnik proporcji jest większy niż współczynnik proporcji wizjera

W takim przypadku wysokość wizjera powinna być równa wysokości aktywności, a współczynnik proporcji wizjera powinien być taki sam jak współczynnik proporcji źródła. Pseudokod wygląda tak:

viewfinderHeight = activityHeight;
viewfinderWidth = activityHeight * aspectRatioSource;
aspectRatioActivity ≤ aspectRatioSource

Drugi przypadek to sytuacja, gdy aktywność jest „węższa” lub „wyższa”. Możemy użyć poprzedniego przykładu, z tym że w tym przypadku obracasz urządzenie o 90 stopni, co sprawia, że aktywność ma format 9:16, a źródło – 3:4.

aspectRatioActivity = 9/16 = 0.5625
aspectRatioSource = 3/4 = 0.75

W tym przypadku chcesz dopasować źródło i wizjer do działania w ten sposób:

Ilustracja aktywności, której współczynnik proporcji jest mniejszy niż współczynnik proporcji wizjera

Szerokość wizjera powinna być zgodna z szerokością aktywności (w przeciwieństwie do wysokości w poprzednim przypadku), a współczynnik proporcji wizjera powinien być identyczny ze współczynnikiem proporcji źródła. Pseudokod:

viewfinderWidth = activityWidth;
viewfinderHeight = activityWidth / aspectRatioSource;
Obcinanie

AutoFitSurfaceView.kt (github) z przykładowych kodów Camera2 zastępuje SurfaceView i obsługuje niedopasowane proporcje, używając obrazu, który jest równy lub „nieco większy” niż aktywność w obu wymiarach, a następnie przycina zawartość, która się nie mieści. Jest to przydatne w przypadku aplikacji, w których podgląd ma obejmować całą aktywność lub całkowicie wypełniać widok o stałych wymiarach bez zniekształcania obrazu.

Caveat

W poprzednim przykładzie staramy się zmaksymalizować wykorzystanie przestrzeni ekranu, powiększając podgląd tak, aby był nieco większy od aktywności i nie pozostawiał wolnego miejsca. Działa to dzięki temu, że domyślnie przepełnione części są przycinane przez układ nadrzędny (lub ViewGroup). Działa to tak samo jak w przypadku elementów RelativeLayout i LinearLayout, ale NIE w przypadku elementu ConstraintLayout. Element ConstraintLayout może zmienić rozmiar elementów podrzędnych, aby dopasować je do układu, co zniweczy zamierzony efekt „przycinania do środka” i spowoduje rozciągnięcie podglądów. Możesz się odwołać do tego zatwierdzenia.

TextureView

TextureView zapewnia maksymalną kontrolę nad zawartością podglądu z kamery, ale wiąże się to z kosztem wydajności. Wymaga to też więcej pracy, aby podgląd z kamery był wyświetlany prawidłowo.

Źródło

Pod elementem TextureView platforma Androida obraca bufory wyjściowe zgodnie z orientacją czujnika, aby dopasować je do naturalnej orientacji urządzenia. Chociaż TextureView obsługuje orientację czujnika, nie obsługuje obrotów wyświetlacza. Wyrównuje bufory wyjściowe z naturalną orientacją urządzenia, co oznacza, że musisz samodzielnie obsługiwać obroty wyświetlacza.

Ilustruje to tabela poniżej. Jeśli obrócisz figury zgodnie z odpowiednim obrotem wyświetlacza, w SurfaceView uzyskasz te same figury.

Obrót wyświetlacza Telefon (naturalna orientacja = pionowa) Laptop (orientacja naturalna = pozioma)
0 Obraz w formacie pionowym przedstawiający głowę bugdroida skierowaną do góry Obraz w orientacji poziomej z głową bugdroida skierowaną do góry
90 Obraz w formacie pionowym przedstawiający głowę bugdroida skierowaną w prawo Obraz w orientacji poziomej z głową bugdroida skierowaną w prawo.
180 Obraz w orientacji poziomej z głową bugdroida skierowaną do góry Obraz w formacie pionowym przedstawiający głowę bugdroida skierowaną do góry
270 Obraz w orientacji poziomej z głową bugdroida skierowaną do góry Obraz w formacie pionowym przedstawiający głowę bugdroida skierowaną do góry

Układ

W przypadku TextureView układ jest nieco skomplikowany. Wcześniej sugerowano użycie macierzy przekształceń w przypadku TextureView, ale ta metoda nie działa na wszystkich urządzeniach. Zamiast tego wykonaj czynności opisane tutaj.

3-etapowy proces prawidłowego rozmieszczenia podglądów w obiekcie TextureView:

  1. Ustaw rozmiar elementu TextureView tak, aby był identyczny z wybranym rozmiarem podglądu.
  2. Skaluje potencjalnie rozciągnięty element TextureView z powrotem do pierwotnych wymiarów podglądu.
  3. Obróć element TextureView o displayRotation w lewo.

Załóżmy, że masz telefon z ekranem obróconym o 90 stopni.

Ilustracja telefonu z obróconym o 90 stopni wyświetlaczem i obiektem

1. Ustaw rozmiar elementu TextureView na taki sam jak wybrany rozmiar podglądu.

Załóżmy, że wybrany rozmiar podglądu to previewWidth × previewHeight, gdzie previewWidth > previewHeight (dane wyjściowe z czujnika są z natury w formacie poziomym). Podczas konfigurowania sesji przechwytywania należy wywołać funkcję SurfaceTexture#setDefaultBufferSize(int width, height), aby określić rozmiar podglądu (previewWidth × previewHeight).

Przed wywołaniem metody setDefaultBufferSize ważne jest, aby ustawić rozmiar elementu TextureView na `previewWidth × previewHeight` za pomocą View#setLayoutParams(android.view.ViewGroup.LayoutParams). Dzieje się tak, ponieważ TextureView wywołuje SurfaceTexture#setDefaultBufferSize(int width, height) z zmierzoną szerokością i wysokością. Jeśli rozmiar elementu TextureView nie zostanie wcześniej wyraźnie ustawiony, może to spowodować wyścig. Można temu zapobiec, najpierw jawnie ustawiając rozmiar elementu TextureView.

Teraz TextureView może nie pasować do wymiarów źródła. W przypadku telefonów źródło ma kształt pionowy, ale TextureView ma kształt poziomy ze względu na ustawione przez Ciebie parametry layoutParams. Spowoduje to rozciągnięcie podglądu, jak pokazano tutaj:

Ilustracja przedstawiająca podgląd w formacie pionowym rozciągnięty tak, aby pasował do elementu TextureView o rozmiarze wybranego podglądu.

2. Skalowanie potencjalnie rozciągniętego widoku TextureView z powrotem do pierwotnych wymiarów podglądu

Aby przywrócić rozciągniętą podgląd do wymiarów źródła, wykonaj te czynności:

Wymiary źródła (sourceWidth × sourceHeight) to:

  • previewHeight × previewWidth, jeśli naturalna orientacja to pionowa lub odwrócona pionowa (orientacja czujnika to 90 lub 270 stopni);
  • previewWidth × previewHeight, jeśli naturalna orientacja to pozioma lub odwrócona pozioma (orientacja czujnika to 0 lub 180 stopni);

Zapobiegaj rozciąganiu, korzystając z wartości View#setScaleX(float)View#setScaleY(float).

  • setScaleX(sourceWidth / previewWidth)
  • setScaleY(sourceHeight / previewHeight)

Ilustracja przedstawiająca procedurę skalowania rozciągniętego podglądu z powrotem do pierwotnych wymiarów

3. Obróć podgląd o `displayRotation` w lewo

Jak już wspomnieliśmy, aby skompensować obrót wyświetlacza, należy obrócić podgląd o displayRotation w lewo.

Możesz to zrobić, View#setRotation(float)

  • setRotation(-displayRotation), ponieważ obraca on element w prawo.

Ilustracja przedstawiająca procedurę obracania podglądu w celu dopasowania go do orientacji wyświetlacza urządzenia

Próbka

Uwaga: jeśli w kodzie używasz macierzy przekształceń dla elementu TextureView, podgląd może wyglądać nieprawidłowo na urządzeniu z orientacją poziomą, takim jak Chromebook. Prawdopodobnie macierz przekształcenia nieprawidłowo zakłada, że orientacja czujnika wynosi 90 lub 270 stopni. Obejście tego problemu znajdziesz w tym zatwierdzeniu w GitHubie, ale zdecydowanie zalecamy przeniesienie aplikacji na metodę opisaną tutaj.