Obsługa różnych gęstości pikseli

Urządzenia z Androidem mają nie tylko ekrany w różnych rozmiarach – słuchawki, tablety, telewizory itp., ale też ekrany o różnej wielkości pikseli. Jedno urządzenie może mieć 160 pikseli na cal, a inne mieści się w tej samej przestrzeni 480 pikseli. Jeśli nie weźmiesz pod uwagę tych zmian gęstości pikseli, system może przeskalować obrazy, co spowoduje ich rozmazanie, lub obrazy mogą mieć nieprawidłowy rozmiar.

Na tej stronie dowiesz się, jak zaprojektować aplikację tak, aby obsługiwała różne gęstości pikseli, stosując jednostki miary niezależne od rozdzielczości i dostarczając alternatywne zasoby bitmapy dla poszczególnych gęstości pikseli.

Aby zapoznać się z tymi technikami, obejrzyj film poniżej.

Więcej informacji o projektowaniu zasobów ikon znajdziesz w wytycznych dotyczących ikon w stylu Material Design.

Użyj pikseli niezależnych od gęstości

Unikaj używania pikseli do definiowania odległości i rozmiarów. Definiowanie wymiarów za pomocą pikseli bywa problem, ponieważ różne ekrany mają różną gęstość pikseli, więc ta sama liczba pikseli odpowiada różnym rozmiarom fizycznym na różnych urządzeniach.

Obraz przedstawiający 2 przykładowe urządzenia o różnej gęstości
Rysunek 1. Różne ekrany o tym samym rozmiarze mogą mieć różną liczbę pikseli.

Aby zachować widoczny rozmiar interfejsu na ekranach o różnej gęstości, zaprojektuj go, używając pikseli niezależnych od gęstości (dp) jako jednostki miary. 1 dp to wirtualna jednostka pikseli równa 1 pikselowi na ekranie o średniej gęstości (160 dpi, czyli gęstość „odwzorowana”). Android przekłada tę wartość na odpowiednią liczbę rzeczywistych pikseli dla danej gęstości.

Przeanalizujmy oba urządzenia na ilustracji 1. Widok o szerokości 100 pikseli jest na urządzeniu po lewej stronie znacznie większy. Widok o szerokości 100 dp ma taki sam rozmiar na obu ekranach.

Podczas definiowania rozmiarów tekstu możesz zamiast tego używać skalowalnych pikseli (sp) jako jednostek. Domyślnie jednostka sp ma taki sam rozmiar jak dp, ale zmienia się w zależności od preferowanego rozmiaru tekstu przez użytkownika. Nigdy nie używaj sp dla rozmiarów układu.

Aby na przykład określić odstępy między dwoma widokami, użyj parametru dp:

<Button android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="@string/clickme"
    android:layout_marginTop="20dp" />

Określając rozmiar tekstu, użyj sp:

<TextView android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:textSize="20sp" />

Przelicz jednostki dp na jednostki pikseli

W niektórych przypadkach musisz wyrazić wymiary w dp, a potem przekonwertować je na piksele. Konwertowanie jednostek dp na piksele ekranu wygląda tak:

px = dp * (dpi / 160)

Uwaga: nigdy nie koduj na stałe tego równania do obliczania pikseli. Zamiast niego możesz użyć funkcji TypedValue.applyDimension(), która konwertuje wiele rodzajów wymiarów (dp, sp itp.) na piksele.

Wyobraź sobie aplikację, w której gest przewijania lub przesuwania jest rozpoznawany po przesunięciu palca użytkownika o co najmniej 16 pikseli. Na ekranie bazowym palec użytkownika musi przesunąć 16 pixels / 160 dpi, co odpowiada 1/10 cala (2, 5 mm), zanim zostanie rozpoznany.

Na urządzeniu z wyświetlaczem o dużej gęstości (240 dpi) palec użytkownika musi przesuwać się 16 pixels / 240 dpi, co oznacza 1/15 cala (czyli 1, 7 mm). Odległość jest znacznie mniejsza, przez co aplikacja wydaje się bardziej czuła na użytkownika.

Aby rozwiązać ten problem, podaj próg gestów w kodzie w dp, a potem przekonwertuj go na rzeczywiste piksele. Na przykład:

Kotlin

// The gesture threshold expressed in dp
private const val GESTURE_THRESHOLD_DP = 16.0f

private var gestureThreshold: Int = 0

// Convert the dps to pixels, based on density scale
gestureThreshold = TypedValue.applyDimension(
  COMPLEX_UNIT_DIP,
  GESTURE_THRESHOLD_DP + 0.5f,
  resources.displayMetrics).toInt()

// Use gestureThreshold as a distance in pixels...

Java

// The gesture threshold expressed in dp
private final float GESTURE_THRESHOLD_DP = 16.0f;

// Convert the dps to pixels, based on density scale
int gestureThreshold = (int) TypedValue.applyDimension(
  COMPLEX_UNIT_DIP,
  GESTURE_THRESHOLD_DP + 0.5f,
  getResources().getDisplayMetrics());

// Use gestureThreshold as a distance in pixels...

Pole DisplayMetrics.density określa współczynnik skali używany do konwertowania jednostek dp na piksele zgodnie z bieżącą gęstością pikseli. Na ekranie o małej gęstości DisplayMetrics.density równa się 1,0, a na ekranie o dużej gęstości – 1,5. Na ekranie o bardzo dużej gęstości ma on wartość 2,0, a na ekranie o małej gęstości – 0, 75. Ta wartość jest używana przez TypedValue.applyDimension() do uzyskania rzeczywistej liczby pikseli na bieżącym ekranie.

Użyj wstępnie skalowanych wartości konfiguracji

Klasa ViewConfiguration zapewnia dostęp do wspólnych odległości, prędkości i czasów używanych przez system Android. Na przykład odległość w pikselach używaną przez platformę jako próg przewijania można uzyskać za pomocą funkcji getScaledTouchSlop():

Kotlin

private val GESTURE_THRESHOLD_DP = ViewConfiguration.get(myContext).scaledTouchSlop

Java

private final int GESTURE_THRESHOLD_DP = ViewConfiguration.get(myContext).getScaledTouchSlop();

Gwarantujemy, że metody w ViewConfiguration, które zaczynają się od prefiksu getScaled, zwracają wartość w pikselach, które są wyświetlane prawidłowo niezależnie od bieżącej gęstości pikseli.

Preferuj grafikę wektorową

Alternatywą dla tworzenia wielu wersji obrazu o określonej gęstości jest utworzenie tylko jednej grafiki wektorowej. Zamiast pikselowych map bitowych, grafika wektorowa tworzy obraz za pomocą kodu XML do określania ścieżek i kolorów. Dzięki temu grafika wektorowa może być skalowana do dowolnego rozmiaru bez skalowania artefaktów, ale zwykle najlepiej sprawdza się w przypadku ilustracji, takich jak ikony, a nie fotografie.

Grafiki wektorowe są często udostępniane jako pliki SVG (Scalable Vector Graphics), ale Android nie obsługuje tego formatu, dlatego pliki SVG trzeba przekonwertować na format wektorowy dostępny w Androidzie.

Możesz przekonwertować plik SVG na obiekt rysowalny wektorowo za pomocą narzędzia Vector Asset Studio w Android Studio w ten sposób:

  1. W oknie Projekt kliknij prawym przyciskiem myszy katalog res i wybierz Nowy element > Zasób wektorowy.
  2. Wybierz Plik lokalny (SVG, PSD).
  3. Odszukaj plik, który chcesz zaimportować, i wprowadź ewentualne zmiany.

    Obraz pokazujący, jak importować pliki SVG w Android Studio
    Rysunek 2. Importowanie pliku SVG w Android Studio

    W oknie Asset Studio możesz zauważyć błędy, które wskazują, że obiekty rysowalne wektorowe nie obsługują niektórych właściwości pliku. Nie uniemożliwia to importu pliku – nieobsługiwane właściwości są ignorowane.

  4. Kliknij Dalej.

  5. Na następnym ekranie potwierdź zbiór źródłowy, w którym chcesz umieścić plik w projekcie, i kliknij Zakończ.

    Pojedynczy obiekt rysowalny wektorowo może być używany do wszystkich gęstości pikseli, dlatego ten plik trafi do domyślnego katalogu obiektów rysowalnych, jak pokazano poniżej w hierarchii. Nie musisz używać katalogów przeznaczonych dla konkretnej gęstości.

    res/
      drawable/
        ic_android_launcher.xml
    

Więcej informacji o tworzeniu grafiki wektorowej znajdziesz w dokumentacji elementu rysowalnego wektorowego.

Podaj alternatywne mapy bitowe

Aby zapewnić dobrą jakość grafiki na urządzeniach o różnej gęstości pikseli, udostępnij kilka wersji każdej bitmapy w aplikacji – po jednej na każdy zasobnik gęstości w odpowiedniej rozdzielczości. W przeciwnym razie Android musi skalować bitmapę tak, aby zajmowała tę samą widoczną przestrzeń na każdym ekranie, co skutkuje skalowaniem artefaktów takich jak rozmycie.

Obraz przedstawiający względne rozmiary map bitowych o różnych gęstościach
Rysunek 3. Względne rozmiary map bitowych w zasobnikach o różnej gęstości.

W swoich aplikacjach możesz wykorzystać kilka grup gęstości. W tabeli 1 opisujemy różne dostępne kwalifikatory konfiguracji i typy ekranów, do których się one odnoszą.

Tabela 1. Kwalifikatory konfiguracji dla różnych gęstości pikseli.

Kwalifikator gęstości Opis
ldpi Zasoby dla ekranów o niskiej gęstości (ldpi) (ok. 120 dpi).
mdpi Zasoby dla ekranów o średniej gęstości (mdpi) (ok. 160 dpi). To jest gęstość bazowa.
hdpi Zasoby dla ekranów o dużej gęstości (hdpi) (ok. 240 dpi).
xhdpi Zasoby dla ekranów o bardzo dużej gęstości (xhdpi) (ok. 320 dpi).
xxhdpi Zasoby dla ekranów o bardzo dużej gęstości (xxhdpi) (ok. 480 dpi).
xxxhdpi Zasoby związane z wyjątkowo dużą gęstością (xxxhdpi) (ok. 640 dpi).
nodpi Zasoby dla wszystkich gęstości. Są to zasoby niezależne od gęstości. System nie skaluje zasobów oznaczonych tym kwalifikatorem niezależnie od bieżącej gęstości ekranu.
tvdpi Zasoby dla ekranów o rozdzielczości od mdpi do hdpi; około 213 dpi. Nie jest to „główna” grupa gęstości. Służy on głównie do wyświetlania na telewizorach i większość aplikacji go nie potrzebuje. W większości aplikacji wystarczy udostępnienie zasobów mdpi i HDPI, a system odpowiednio je skaluje. Jeśli musisz dostarczyć zasoby tvdpi, ustaw ich rozmiar na 1,33 * mdpi. Na przykład obraz o wymiarach 100 x 100 pikseli w przypadku ekranów mdpi to 133 x 133 piksele dla rozdzielczości tvdpi.

Aby utworzyć alternatywne elementy rysowalne do bitmap o różnych gęstościach, użyj współczynnika skalowania 3:4:6:8:12:16 dla 6 gęstości podstawowych. Jeśli np. masz rysowalną mapę bitową o wymiarach 48 x 48 pikseli na ekranach o średniej gęstości, rozmiary te są następujące:

  • 36 x 36 (0,75x) dla małej gęstości (ldpi)
  • 48 x 48 (1,0 x wartość bazowa) dla średniej gęstości (mdpi)
  • 72 x 72 (1,5x) dla dużej gęstości (HDpi)
  • 96 x 96 (2,0x) dla bardzo dużej gęstości (xhdpi)
  • 144 x 144 (3,0x) – wyjątkowo duża gęstość (xxhdpi)
  • 192 x 192 (4,0x) – wyjątkowo duża gęstość (xxxhdpi)

Umieść wygenerowane pliki graficzne w odpowiednim podkatalogu w sekcji res/:

res/
  drawable-xxxhdpi/
    awesome_image.png
  drawable-xxhdpi/
    awesome_image.png
  drawable-xhdpi/
    awesome_image.png
  drawable-hdpi/
    awesome_image.png
  drawable-mdpi/
    awesome_image.png

Następnie za każdym razem, gdy odwołujesz się do @drawable/awesomeimage, system wybierze odpowiednią bitmapę na podstawie dpi ekranu. Jeśli nie podasz zasobu przeznaczonego do określonej gęstości, system znajdzie następne najlepsze dopasowanie i przeskaluje go, aby pasował do ekranu.

Wskazówka: jeśli masz rysowalne zasoby, których nie chcesz skalować przez system, np. gdy samodzielnie przeprowadzasz pewne korekty obrazu w czasie działania, umieść je w katalogu z kwalifikatorem konfiguracji nodpi. Zasoby z tym kwalifikatorem są uznawane za niezależne od gęstości, a system ich nie skaluje.

Więcej informacji o innych kwalifikatorach konfiguracji i o tym, jak Android wybiera odpowiednie zasoby na potrzeby bieżącej konfiguracji ekranu, znajdziesz w omówieniu zasobów aplikacji.

Umieść ikony aplikacji w katalogach mipmap

Podobnie jak w przypadku innych zasobów map bitowych, musisz udostępnić wersje ikony aplikacji dostosowane do gęstości. Jednak niektóre menu z aplikacjami wyświetlają ikonę aplikacji nawet o 25% większą od tej, na którą zezwala zasobnik gęstości urządzenia.

Jeśli na przykład zasobnik urządzenia o gęstości ma rozmiar xxhdpi, a największa udostępniona ikona aplikacji to drawable-xxhdpi, menu z aplikacjami skaluje tę ikonę w górę, przez co wydaje się mniej wyraźna.

Aby tego uniknąć, umieść wszystkie ikony aplikacji w katalogach mipmap, a nie w katalogach drawable. W przeciwieństwie do katalogów drawable wszystkie katalogi mipmap są przechowywane w pliku APK nawet wtedy, gdy tworzysz pliki APK o konkretnej gęstości. Dzięki temu aplikacje uruchamiające będą mogły wybrać ikonę rozdzielczości o najlepszej rozdzielczości do wyświetlenia na ekranie głównym.

res/
  mipmap-xxxhdpi/
    launcher_icon.png
  mipmap-xxhdpi/
    launcher_icon.png
  mipmap-xhdpi/
    launcher_icon.png
  mipmap-hdpi/
    launcher_icon.png
  mipmap-mdpi/
    launcher_icon.png

W poprzednim przykładzie urządzenia xxhdpi ikonę programu uruchamiającego o większej gęstości możesz podać w katalogu mipmap-xxxhdpi.

Wskazówki dotyczące projektowania ikon znajdziesz w artykule Ikony systemowe.

Więcej informacji o tworzeniu ikon aplikacji znajdziesz w artykule Tworzenie ikon aplikacji w Image Asset Studio.

Porady dotyczące rzadkich problemów z gęstością

W tej sekcji opisujemy, jak Android przeprowadza skalowanie map bitowych o różnej gęstości pikseli oraz jak można kontrolować sposób rysowania map bitowych o różnej gęstości. Jeśli Twoja aplikacja nie obsługuje grafiki lub nie działa na różnych gęstości pikseli, możesz zignorować tę sekcję.

Aby lepiej zrozumieć, jak obsługiwać wiele gęstości podczas manipulacji grafiką w czasie działania, musisz wiedzieć, jak system pomaga zapewnić właściwe skalowanie map bitowych. Można to zrobić na następujące sposoby:

  1. Wstępne skalowanie zasobów, takich jak obiekty rysowalne do bitmap

    Na podstawie gęstości bieżącego ekranu system wykorzystuje wszystkie zasoby z aplikacji związane z gęstością. Jeśli zasoby są niedostępne w odpowiedniej gęstości, system wczytuje zasoby domyślne i odpowiednio skaluje je w górę lub w dół. System zakłada, że zasoby domyślne (te z katalogu bez kwalifikatorów konfiguracji) są zaprojektowane pod kątem bazowej gęstości pikseli (mdpi) i zmienia ich rozmiar do rozmiaru odpowiadającego bieżącej gęstości pikseli.

    Jeśli poprosisz o wymiary wstępnie skalowanego zasobu, system zwróci wartości reprezentujące wymiary po przeskalowaniu. Na przykład mapa bitowa zaprojektowana w wymiarach 50 x 50 pikseli dla ekranów o rozdzielczości mdpi jest skalowana do 75 x 75 pikseli na ekranie hdpi (jeśli nie ma innego zasobu dla standardu hdpi) i system raportuje ten rozmiar w ten sposób.

    W pewnych sytuacjach Android może nie chcieć wstępnie przeskalowywać zasobów. Najprostszym sposobem uniknięcia wstępnego skalowania jest umieszczenie zasobu w katalogu zasobów z kwalifikatorem konfiguracji nodpi. Na przykład:

    res/drawable-nodpi/icon.png

    Gdy system używa mapy bitowej icon.png z tego folderu, nie skaluje jej zgodnie z bieżącą gęstością urządzeń.

  2. Autoskalowanie wymiarów i współrzędnych pikseli

    Aby wyłączyć wymiary i obrazy przed skalowaniem, ustaw dla elementu android:anyDensity wartość "false" w pliku manifestu lub automatycznie dla elementu Bitmap, ustawiając wartość inScaled na "false". W takim przypadku system automatycznie skaluje wszystkie bezwzględne współrzędne pikseli i wartości wymiarów pikseli w czasie rysowania. Dzięki temu elementy na ekranie zdefiniowane za pomocą pikseli są nadal wyświetlane w przybliżeniu w tym samym rozmiarze fizycznym, w którym mogą być wyświetlane przy podstawowej gęstości pikseli (mdpi). System obsługuje to skalowanie w sposób przejrzysty dla aplikacji i raportuje jej skalowane wymiary w pikselach, a nie rzeczywiste wymiary w pikselach.

    Załóżmy na przykład, że urządzenie ma ekran o dużej gęstości obrazu WVGA o rozdzielczości 480 x 800 i mniej więcej tego samego rozmiaru co tradycyjny ekran HVGA. Zostało na nim jednak uruchomione aplikację z wyłączoną preskalacją. W tym przypadku system „odpowiada” aplikacji za zapytanie o wymiary ekranu i zgłasza przybliżone tłumaczenie w mdpi dla gęstości pikseli w rozmiarze 320 x 533.

    Następnie, gdy aplikacja wykonuje operacje rysowania, np. unieważnianie prostokąta z zakresu (10,10) do (100, 100), system przekształca współrzędne, skalując je do odpowiedniej wartości i faktycznie unieważnia obszar (15,15) do (150, 150). Ta rozbieżność może spowodować nieoczekiwane zachowanie, jeśli aplikacja bezpośrednio modyfikuje skalowaną bitmapę. Jest to jednak uznawane za rozsądną zmianę, która pozwala uzyskać najlepszą możliwą wydajność aplikacji. W takim przypadku przeczytaj artykuł Przekonwertuj jednostki dp na jednostki pikseli.

    Zwykle nie wyłączasz wstępnego skalowania. Najlepszym sposobem na obsługę wielu ekranów jest stosowanie podstawowych technik omówionych na tej stronie.

Jeśli Twoja aplikacja obsługuje mapy bitowe lub bezpośrednio wchodzi w interakcje z pikselami na ekranie w inny sposób, konieczne może być wykonanie dodatkowych czynności w celu obsługi różnego gęstości pikseli. Jeśli np. reagujesz na gesty dotykowe, zliczając liczbę pikseli, które przecina palec, zamiast wartości rzeczywistych pikseli musisz użyć odpowiednich wartości pikseli niezależnych od gęstości. Możesz jednak przeliczyć wartości między dp i px.

Przetestuj gęstości wszystkich pikseli

Przetestuj aplikację na wielu urządzeniach o różnej gęstości pikseli, by mieć pewność, że interfejs będzie prawidłowo skalowany. W miarę możliwości testy na urządzeniu fizycznym. Jeśli nie masz dostępu do urządzeń fizycznych o różnej gęstości pikseli, użyj Emulatora Androida.

Jeśli chcesz przeprowadzać testy na urządzeniach fizycznych, ale nie chcesz kupować urządzeń, możesz skorzystać z Laboratorium Firebase, aby uzyskać dostęp do urządzeń w centrum danych Google.