Układy w widokach

Wypróbuj sposób tworzenia wiadomości
Jetpack Compose to zalecany zestaw narzędzi UI na Androida. Dowiedz się, jak korzystać z układów w sekcji Utwórz

Układ określa strukturę interfejsu użytkownika aplikacji, np. w aktywności. Wszystkie elementy układu są tworzone na podstawie hierarchii obiektów View i ViewGroup. View zwykle rysuje coś, co użytkownik widzi i z czym może wchodzić w interakcje. ViewGroup to niewidoczny kontener, który określa strukturę układu dla obiektu View i innych obiektów ViewGroup, jak widać na ilustracji 1.

Rysunek 1. Ilustracja hierarchii widoków, która określa układ interfejsu.

Obiekty View są często nazywane widżetami i mogą być jedną z wielu podklas, takich jak Button lub TextView. Obiekty ViewGroup są zwykle nazywane układami i mogą być jednym z wielu typów obiektów mających odrębną strukturę układu, np. LinearLayout lub ConstraintLayout.

Układ możesz zadeklarować na 2 sposoby:

  • Deklaruj elementy interfejsu w języku XML. Android zapewnia prosty słownik XML odpowiadający klasom i podklasom View, takim jak widżety i układy. Możesz też skorzystać z edytora układu w Android Studio, by utworzyć układ XML przy użyciu interfejsu typu „przeciągnij i upuść”.

  • Inicjuj elementy układu w czasie działania. Twoja aplikacja może w sposób zautomatyzowany tworzyć obiekty View i ViewGroup oraz modyfikować ich właściwości.

Zadeklarowanie interfejsu w formacie XML umożliwia oddzielenie prezentacji aplikacji od kodu, który kontroluje jej działanie. Korzystanie z plików XML ułatwia też tworzenie różnych układów w zależności od rozmiaru i orientacji ekranu. Zostało to szczegółowo opisane w artykule Obsługa różnych rozmiarów ekranów.

Platforma Androida daje Ci możliwość korzystania z jednej lub obu tych metod do tworzenia interfejsu aplikacji. Możesz na przykład zadeklarować domyślne układy aplikacji w formacie XML, a następnie zmodyfikować układ w czasie działania.

Napisz kod XML

Korzystając ze słownika XML na Androidzie, możesz szybko projektować układy interfejsu i zawarte w nich elementy ekranu, tak samo jak strony internetowe w kodzie HTML zawierają serię zagnieżdżonych elementów.

Każdy plik układu musi zawierać dokładnie 1 element główny, który musi być obiektem View lub ViewGroup. Po zdefiniowaniu elementu głównego możesz dodawać kolejne obiekty układu lub widżety jako elementy podrzędne, aby stopniowo tworzyć hierarchię View definiującą układ. Oto na przykład układ XML, w którym pionowa konstrukcja LinearLayout przechowuje TextView i Button:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
              android:layout_width="match_parent"
              android:layout_height="match_parent"
              android:orientation="vertical" >
    <TextView android:id="@+id/text"
              android:layout_width="wrap_content"
              android:layout_height="wrap_content"
              android:text="Hello, I am a TextView" />
    <Button android:id="@+id/button"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="Hello, I am a Button" />
</LinearLayout>

Gdy zadeklarujesz układ w formacie XML, zapisz plik z rozszerzeniem .xml w katalogu res/layout/ projektu na Androida, aby kompilacja przebiegła prawidłowo.

Więcej informacji o składni pliku XML układu znajdziesz w sekcji Zasób Układ.

Wczytaj zasób XML

Podczas kompilowania aplikacji każdy plik układu XML jest kompilowany w zasób View. Wczytaj zasób układu w implementacji wywołania zwrotnego Activity.onCreate() aplikacji. Aby to zrobić, wywołaj właściwość setContentView() i przekaż jej odniesienie do zasobu układu w formularzu: R.layout.layout_file_name. Jeśli np. układ XML został zapisany jako main_layout.xml, wczytaj go na urządzeniu Activity w ten sposób:

Kotlin

fun onCreate(savedInstanceState: Bundle) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.main_layout)
}

Java

public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.main_layout);
}

Platforma Androida wywołuje metodę wywołania zwrotnego onCreate() w Activity po uruchomieniu Activity. Więcej informacji o cyklach życia działań znajdziesz w artykule Wprowadzenie do aktywności.

Atrybuty

Każdy obiekt View i ViewGroup obsługuje własne różne atrybuty XML. Niektóre atrybuty są specyficzne dla obiektu View. Na przykład TextView obsługuje atrybut textSize. Te atrybuty są jednak również dziedziczone przez wszystkie obiekty View, które rozszerzają tę klasę. Niektóre z nich są wspólne dla wszystkich obiektów View, ponieważ są dziedziczone z głównej klasy View, np. w atrybucie id. Inne atrybuty są uważane za parametry układu, czyli atrybuty opisujące określone orientacje układu obiektu View określone przez nadrzędny obiekt ViewGroup tego obiektu.

ID

Z każdym obiektem View może być powiązany identyfikator w postaci liczby całkowitej, który umożliwia jednoznaczne zidentyfikowanie obiektu View w drzewie. Podczas kompilowania aplikacji identyfikator jest przywoływany w postaci liczby całkowitej, ale jest on zwykle przypisywany w pliku XML układu jako ciąg znaków w atrybucie id. To atrybut XML wspólny dla wszystkich obiektów View definiowany przez klasę View. Używasz go bardzo często. Składnia identyfikatora w tagu XML jest taka:

android:id="@+id/my_button"

Symbol at (@) na początku ciągu oznacza, że parser XML analizuje i rozwija pozostałą część ciągu identyfikatora oraz identyfikuje go jako zasób identyfikatora. Symbol plusa (+) oznacza nową nazwę zasobu, którą należy utworzyć i dodać do zasobów w pliku R.java.

Platforma Androida udostępnia wiele innych zasobów identyfikatorów. Gdy odwołujesz się do identyfikatora zasobu Androida, nie potrzebujesz symbolu plusa, ale musisz dodać przestrzeń nazw pakietu android w ten sposób:

android:id="@android:id/empty"

Przestrzeń nazw pakietu android wskazuje, że odwołujesz się do identyfikatora z klasy zasobów android.R, a nie do klasy zasobów lokalnych.

Aby tworzyć widoki danych i odwoływać się do nich z poziomu aplikacji, możesz użyć takiego wzorca:

  1. Zdefiniuj widok w pliku układu i przypisz mu unikalny identyfikator, jak w tym przykładzie:
    <Button android:id="@+id/my_button"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="@string/my_button_text"/>
    
  2. Utwórz instancję obiektu widoku i przechwyć ją z układu, zwykle za pomocą metody onCreate(), jak w tym przykładzie:

    Kotlin

    val myButton: Button = findViewById(R.id.my_button)
    

    Java

    Button myButton = (Button) findViewById(R.id.my_button);
    

Podczas tworzenia obiektu RelativeLayout ważne jest określenie identyfikatorów obiektów widoków. W układzie względnym widoki równorzędne mogą definiować układ w stosunku do innego, do którego odwołuje się unikalny identyfikator.

Identyfikator nie musi być unikalny w całym drzewie, ale musi być niepowtarzalny w obrębie przeszukiwanej części drzewa. Często może to być całe drzewo, więc w miarę możliwości zadbaj o to, aby było unikalne.

Parametry układu

Atrybuty układu XML o nazwie layout_something określają parametry układu dla elementu View, które są odpowiednie dla elementu ViewGroup, w którym się on znajduje.

Każda klasa ViewGroup implementuje zagnieżdżoną klasę rozszerzającą zakres ViewGroup.LayoutParams. Ta podklasa zawiera typy właściwości, które określają rozmiar i pozycję każdego widoku podrzędnego odpowiednio do danej grupy widoków. Jak widać na rys. 2, nadrzędna grupa widoków określa parametry układu dla każdego widoku podrzędnego, w tym dla podrzędnego widoku danych.

Rysunek 2. Wizualizacja hierarchii widoków z parametrami układu powiązanymi z każdym widokiem.

Każda podklasa LayoutParams ma własną składnię wartości ustawień. Każdy element podrzędny musi definiować element LayoutParams odpowiedni dla elementu nadrzędnego, ale może też określać inny element LayoutParams dla własnych elementów podrzędnych.

Wszystkie grupy widoków danych mogą zawierać szerokość i wysokość za pomocą elementów layout_width i layout_height, a każdy widok musi mieć ich definiowanie. Wiele LayoutParams ma opcjonalne marginesy i obramowania.

Możesz określić szerokość i wysokość za pomocą dokładnych pomiarów, ale może to być niezbyt częste. Częściej do ustawiania szerokości lub wysokości używasz jednej z tych stałych:

  • wrap_content: informuje widok, że wyświetla się do wymiarów wymaganych przez jego zawartość.
  • match_parent: informuje, że widok danych jest większy, na tyle, na ile zezwala jego nadrzędna grupa widoków.

Ogólnie nie zalecamy określania szerokości i wysokości układu za pomocą jednostek bezwzględnych takich jak piksele. Lepszym sposobem jest stosowanie pomiarów względnych, takich jak niezależne jednostki pikseli (dp), wrap_content lub match_parent, ponieważ pomagają one prawidłowo wyświetlać się na ekranach urządzeń o różnych rozmiarach. Akceptowane typy pomiarów są określone w sekcji Zasób Układ.

Pozycja układu

Widok ma prostokątną geometrię. Zawiera on lokalizację wyrażoną jako parę współrzędnych left i top oraz dwa wymiary wyrażone jako szerokość i wysokość. Jednostkami lokalizacji i wymiarów jest piksel.

Lokalizację widoku możesz pobrać, wywołując metody getLeft() i getTop(). Pierwsze zwraca współrzędną lewą (x) prostokąta reprezentującego widok. Zwraca ona współrzędną górną (y) prostokąta reprezentującego widok. Zwracają one lokalizację widoku w odniesieniu do jego elementu nadrzędnego. Gdy np. getLeft() zwraca 20, oznacza to, że widok znajduje się 20 pikseli po prawej stronie lewej krawędzi jego bezpośredniego elementu nadrzędnego.

Istnieją też wygodne metody, które pozwalają uniknąć niepotrzebnych obliczeń: getRight() i getBottom(). Zwracają one współrzędne prawej i dolnej krawędzi prostokąta reprezentującego widok. Na przykład wywołanie metody getRight() przebiega podobnie do tego obliczenia: getLeft() + getWidth().

Rozmiar, dopełnienie i marginesy

Rozmiar widoku jest wyrażany przez szerokość i wysokość. Widok ma dwie pary wartości szerokości i wysokości.

Pierwsza para jest znana jako szerokość mierzona i mierzona wysokość. Określają one, jak duży ma być dany widok w ramach swojego elementu nadrzędnego. Zmierzone wymiary możesz uzyskać, wywołując metodę getMeasuredWidth() i getMeasuredHeight().

Druga para to szerokość i wysokość lub czasami szerokość rysowania i wysokość rysowania. Określają one rzeczywisty rozmiar widoku na ekranie, w czasie rysowania i po układzie. Te wartości mogą, ale nie muszą, różnić się od zmierzonej szerokości i wysokości. Szerokość i wysokość możesz sprawdzić, wywołując metodę getWidth() i getHeight().

Podczas pomiaru wymiarów widoku uwzględnia się jego dopełnienie. Dopełnienie jest wyrażone w pikselach dla lewej, górnej, prawej i dolnej części widoku. Możesz użyć dopełnienia, aby odsunąć zawartość widoku o określoną liczbę pikseli. Na przykład 2 lewe dopełnienie przesuwa zawartość widoku o 2 piksele na prawo od lewej krawędzi. Dopełnienie możesz ustawić za pomocą metody setPadding(int, int, int, int) i zapytać je, wywołując getPaddingLeft(), getPaddingTop(), getPaddingRight() i getPaddingBottom().

Widok może definiować dopełnienie, ale nie obsługuje marginesów. ale grupy widoków obsługują marże. Więcej informacji znajdziesz w opisie ViewGroup i ViewGroup.MarginLayoutParams.

Więcej informacji o wymiarach znajdziesz w artykule Wymiar.

Poza automatycznym ustawianiem marginesów i dopełnienia możesz je też ustawiać w układach XML, jak w tym przykładzie:

  <?xml version="1.0" encoding="utf-8"?>
  <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
                android:layout_width="match_parent"
                android:layout_height="match_parent"
                android:orientation="vertical" >
      <TextView android:id="@+id/text"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_margin="16dp"
                android:padding="8dp"
                android:text="Hello, I am a TextView" />
      <Button android:id="@+id/button"
              android:layout_width="wrap_content"
              android:layout_height="wrap_content"
              android:layout_marginTop="16dp"
              android:paddingBottom="4dp"
              android:paddingEnd="8dp"
              android:paddingStart="8dp"
              android:paddingTop="4dp"
              android:text="Hello, I am a Button" />
  </LinearLayout>
  

Poprzedni przykład przedstawia stosowane marginesy i dopełnienie. Element TextView ma równomierne marginesy i dopełnienie dookoła, a Button pokazuje, jak możesz je osobno stosować na różnych krawędziach.

Typowe układy

Każda podklasa klasy ViewGroup zapewnia unikalny sposób wyświetlania zagnieżdżonych w niej widoków. Najbardziej elastycznym typem układu, który zapewnia najlepsze narzędzia do utrzymywania niewielkiej hierarchii układu, jest ConstraintLayout.

Oto kilka typowych typów układów wbudowanych w Androida.

Utwórz układ liniowy

Porządkuje elementy podrzędne w jeden poziomy lub pionowy wiersz i tworzy pasek przewijania, jeśli długość okna przekracza długość ekranu.

Tworzenie list dynamicznych

Jeśli treść układu jest dynamiczna lub nie została wstępnie określona, możesz użyć właściwości RecyclerView lub podklasy AdapterView. RecyclerView jest zasadniczo lepszą opcją, ponieważ wykorzystuje pamięć bardziej wydajnie niż AdapterView.

Typowe układy możliwe w przypadku elementów RecyclerView i AdapterView to:

Lista

Wyświetla przewijaną listę z jedną kolumną.

Siatka

Wyświetla przewijaną siatkę z kolumnami i wierszami.

RecyclerView udostępnia więcej możliwości i opcję tworzenia niestandardowego menedżera układu.

Wypełnianie widoku adaptera danymi

Możesz wypełnić AdapterView, np. ListView lub GridView, powiążąc instancję AdapterView z elementem Adapter, co spowoduje pobieranie danych ze źródła zewnętrznego i utworzenie View reprezentującego każdy wpis danych.

Android udostępnia kilka podklas funkcji Adapter, które przydają się do pobierania różnego rodzaju danych i tworzenia widoków dla interfejsu AdapterView. Dwa najpopularniejsze adaptery to:

ArrayAdapter
Używaj tego adaptera, gdy źródłem danych jest tablica. Domyślnie ArrayAdapter tworzy widok dla każdego elementu tablicy, wywołując toString() w każdym elemencie i umieszczając jego zawartość w TextView.

Jeśli np. masz tablicę ciągów znaków, które chcesz wyświetlić w elemencie ListView, zainicjuj nowy obiekt ArrayAdapter za pomocą konstruktora, aby określić układ dla każdego ciągu i tablicy ciągów:

Kotlin

    val adapter = ArrayAdapter<String>(this, android.R.layout.simple_list_item_1, myStringArray)
    

Java

    ArrayAdapter<String> adapter = new ArrayAdapter<String>(this,
            android.R.layout.simple_list_item_1, myStringArray);
    

Argumenty tego konstruktora są następujące:

  • Twoja aplikacja Context
  • Układ, który zawiera TextView dla każdego ciągu znaków w tablicy
  • Tablica ciągu znaków

Następnie wywołaj setAdapter() na urządzeniu ListView:

Kotlin

    val listView: ListView = findViewById(R.id.listview)
    listView.adapter = adapter
    

Java

    ListView listView = (ListView) findViewById(R.id.listview);
    listView.setAdapter(adapter);
    

Aby dostosować wygląd każdego elementu, możesz zastąpić metodę toString() w przypadku obiektów w tablicy. Możesz też utworzyć widok dla każdego elementu innego niż TextView (jeśli na przykład potrzebujesz klasy ImageView dla każdego elementu tablicy), rozszerz klasę ArrayAdapter i zastąp getView(), aby zwracać odpowiedni typ widoku dla każdego elementu.

SimpleCursorAdapter
Używaj tego adaptera, gdy dane pochodzą z urządzenia Cursor. Jeśli używasz właściwości SimpleCursorAdapter, określ układ, w jakim mają być używane poszczególne wiersze w tabeli Cursor, oraz kolumny z tabeli Cursor, które chcesz wstawić w widokach wybranego układu. Jeśli na przykład chcesz utworzyć listę imion i nazwisk oraz numerów telefonów, możesz wykonać zapytanie, które zwróci wartość Cursor zawierającą wiersz dla każdej osoby oraz kolumny z imionami i nazwiskami oraz numerami. Następnie utworzysz tablicę ciągu znaków, określającą kolumny ze zbioru Cursor, które mają się znaleźć w układzie każdego wyniku, oraz tablicę liczby całkowitej wskazującą widoki, w których należy umieścić każdą kolumnę:

Kotlin

    val fromColumns = arrayOf(ContactsContract.Data.DISPLAY_NAME,
                              ContactsContract.CommonDataKinds.Phone.NUMBER)
    val toViews = intArrayOf(R.id.display_name, R.id.phone_number)
    

Java

    String[] fromColumns = {ContactsContract.Data.DISPLAY_NAME,
                            ContactsContract.CommonDataKinds.Phone.NUMBER};
    int[] toViews = {R.id.display_name, R.id.phone_number};
    

Gdy tworzysz wystąpienie elementu SimpleCursorAdapter, przekaż układ, którego chcesz użyć w przypadku każdego wyniku, obiekt Cursor zawierający wyniki oraz te 2 tablice:

Kotlin

    val adapter = SimpleCursorAdapter(this,
            R.layout.person_name_and_number, cursor, fromColumns, toViews, 0)
    val listView = getListView()
    listView.adapter = adapter
    

Java

    SimpleCursorAdapter adapter = new SimpleCursorAdapter(this,
            R.layout.person_name_and_number, cursor, fromColumns, toViews, 0);
    ListView listView = getListView();
    listView.setAdapter(adapter);
    

SimpleCursorAdapter tworzy widok dla każdego wiersza w Cursor przy użyciu podanego układu, wstawiając każdy element fromColumns do odpowiedniego widoku toViews.

Jeśli w trakcie działania aplikacji zmienisz dane źródłowe odczytywane przez adapter, wywołaj notifyDataSetChanged(). Informuje to załączony widok o zmianie danych i odświeża się automatycznie.

Obsługa zdarzeń kliknięcia

Możesz reagować na zdarzenia kliknięcia każdego elementu w elemencie AdapterView, implementując interfejs AdapterView.OnItemClickListener. Na przykład:

Kotlin

listView.onItemClickListener = AdapterView.OnItemClickListener { parent, view, position, id ->
    // Do something in response to the click.
}

Java

// Create a message handling object as an anonymous class.
private OnItemClickListener messageClickedHandler = new OnItemClickListener() {
    public void onItemClick(AdapterView parent, View v, int position, long id) {
        // Do something in response to the click.
    }
};

listView.setOnItemClickListener(messageClickedHandler);

Dodatkowe materiały

Zobacz, jak używane są układy, w aplikacji demonstracyjnej Sunflower na GitHubie.