Tworzenie komponentów widoku niestandardowego

Wypróbuj metodę Compose
Jetpack Compose to zalecany zestaw narzędzi interfejsu na Androida. Dowiedz się, jak pracować z układami w Compose.

Android oferuje zaawansowany i wydajny model komponentowy do tworzenia interfejsu użytkownika oparty na podstawowych klasach układu ViewViewGroup. Platforma zawiera różne gotowe podklasy ViewViewGroup, zwane odpowiednio widżetami i układami, których możesz używać do tworzenia interfejsu.

Częściowa lista dostępnych widżetów obejmuje Button, TextView, EditText, ListView, CheckBox, RadioButton, Gallery, Spinner oraz bardziej specjalistyczne AutoCompleteTextView, ImageSwitcherTextSwitcher.

Dostępne układy to m.in. LinearLayout, FrameLayout, RelativeLayout i inne. Więcej przykładów znajdziesz w sekcji Typowe układy.

Jeśli żaden z gotowych widżetów ani układów nie spełnia Twoich potrzeb, możesz utworzyć własną Viewpodklasę. Jeśli chcesz wprowadzić tylko niewielkie zmiany w istniejącym widżecie lub układzie, możesz utworzyć podklasę widżetu lub układu i zastąpić jego metody.

Tworzenie własnych View podklas daje precyzyjną kontrolę nad wyglądem i funkcją elementu ekranu. Aby dać Ci wyobrażenie o tym, jaką kontrolę zapewniają widoki niestandardowe, podajemy kilka przykładów ich zastosowań:

  • Możesz utworzyć w pełni niestandardowy typ View, np. pokrętło „głośności”, renderowane za pomocą grafiki 2D, które przypomina analogowe sterowanie elektroniczne.
  • Możesz połączyć grupę komponentów View w jeden nowy komponent, np. aby utworzyć pole kombi (połączenie listy wyskakującej i pola tekstowego do wpisywania dowolnych danych), dwupanelowy selektor (lewy i prawy panel z listą w każdym z nich, w którym możesz ponownie przypisać element do listy) itp.
  • Możesz zastąpić sposób renderowania komponentu EditText na ekranie. Przykładowa aplikacja NotePad wykorzystuje to do tworzenia strony notatnika w linie.
  • Możesz rejestrować inne zdarzenia, np. naciśnięcia klawiszy, i obsługiwać je w niestandardowy sposób, np. w grze.

W sekcjach poniżej znajdziesz informacje o tym, jak tworzyć widoki niestandardowe i używać ich w aplikacji. Szczegółowe informacje znajdziesz w dokumentacji klasy View.

Podstawowe podejście

Oto ogólny przegląd informacji, które musisz znać, aby tworzyć własne Viewkomponenty:

  1. Rozszerz istniejącą klasę View lub podklasę o własną klasę.
  2. Zastąp niektóre metody z klasy nadrzędnej. Metody klasy nadrzędnej do zastąpienia zaczynają się od on, np. onDraw(), onMeasure()onKeyDown(). Jest to podobne do zdarzeń onActivity lub ListActivity, które zastępujesz w przypadku cyklu życia i innych funkcji.
  3. Użyj nowej klasy rozszerzenia. Po zakończeniu tego procesu możesz używać nowej klasy rozszerzenia zamiast widoku, na którym była oparta.

W pełni dostosowane komponenty

Możesz tworzyć w pełni dostosowane komponenty graficzne, które będą wyświetlane w dowolny sposób. Możesz na przykład wybrać graficzny miernik VU, który wygląda jak stary analogowy wskaźnik, lub widok tekstu do śpiewania, w którym skacząca piłka przesuwa się wzdłuż słów, gdy śpiewasz z maszyną do karaoke. Może się zdarzyć, że potrzebujesz czegoś, czego nie da się uzyskać za pomocą wbudowanych komponentów, niezależnie od tego, jak je połączysz.

Na szczęście możesz tworzyć komponenty, które wyglądają i działają w dowolny sposób. Ogranicza Cię tylko wyobraźnia, rozmiar ekranu i dostępna moc obliczeniowa. Pamiętaj, że aplikacja może działać na urządzeniu o znacznie mniejszej mocy niż stacja robocza.

Aby utworzyć w pełni dostosowany komponent, weź pod uwagę te kwestie:

  • Najbardziej ogólnym widokiem, który możesz rozszerzyć, jest View, więc zwykle zaczynasz od rozszerzenia tego widoku, aby utworzyć nowy superkomponent.
  • Możesz podać konstruktor, który może pobierać atrybuty i parametry z pliku XML, a także korzystać z własnych atrybutów i parametrów, takich jak kolor i zakres miernika VU czy szerokość i tłumienie igły.
  • Prawdopodobnie będziesz też chcieć utworzyć własne detektory zdarzeń, akcesory właściwości i modyfikatory, a także bardziej zaawansowane zachowania w klasie komponentu.
  • Prawie na pewno chcesz zastąpić onMeasure(), a jeśli chcesz, aby komponent coś wyświetlał, prawdopodobnie musisz też zastąpić onDraw(). Oba mają domyślne działanie, ale domyślny element onDraw() nic nie robi, a domyślny element onMeasure() zawsze ustawia rozmiar 100x100, co prawdopodobnie nie jest pożądane.
  • W razie potrzeby możesz też zastąpić inne metody on.

Rozszerzanie metod onDraw() i onMeasure()

Metoda onDraw() dostarcza element Canvas, na którym możesz zaimplementować wszystko, co chcesz: grafikę 2D, inne standardowe lub niestandardowe komponenty, sformatowany tekst lub cokolwiek innego, co przyjdzie Ci do głowy.

onMeasure() jest nieco bardziej skomplikowane. onMeasure() to kluczowy element umowy renderowania między komponentem a jego kontenerem. onMeasure() musi zostać zastąpiona, aby skutecznie i dokładnie raportować pomiary zawartych w niej części. Jest to nieco bardziej skomplikowane ze względu na wymagania dotyczące limitów ze strony elementu nadrzędnego, które są przekazywane do metody onMeasure(), oraz ze względu na konieczność wywołania metody setMeasuredDimension() z obliczoną szerokością i wysokością. Jeśli nie wywołasz tej metody z zastąpionej metody onMeasure(), w momencie pomiaru wystąpi wyjątek.

Ogólnie implementacja onMeasure() wygląda mniej więcej tak:

  • Zastąpiona metoda onMeasure() jest wywoływana ze specyfikacjami szerokości i wysokości, które są traktowane jako wymagania dotyczące ograniczeń pomiarów szerokości i wysokości. Parametry widthMeasureSpecheightMeasureSpec to kody całkowite reprezentujące wymiary. Pełne odniesienie do rodzaju ograniczeń, które mogą być wymagane przez te specyfikacje, można znaleźć w dokumentacji referencyjnej w sekcji View.onMeasure(int, int). Dokumentacja referencyjna wyjaśnia też całą operację pomiarową.
  • Metoda onMeasure() komponentu oblicza szerokość i wysokość pomiaru, które są wymagane do renderowania komponentu. Musi próbować zachować zgodność z przekazanymi specyfikacjami, ale może je przekraczać. W takim przypadku rodzic może podjąć decyzję, co zrobić, np. przyciąć, przewinąć, zgłosić wyjątek lub poprosić onMeasure() o ponowną próbę, być może z innymi specyfikacjami pomiaru.
  • Po obliczeniu szerokości i wysokości wywołaj metodę setMeasuredDimension(int width, int height) z obliczonymi wymiarami. Jeśli tego nie zrobisz, wystąpi wyjątek.

Oto podsumowanie innych standardowych metod, które framework wywołuje w przypadku widoków:

Kategoria Metody Opis
Utworzenie Zespoły Istnieje forma konstruktora, która jest wywoływana, gdy widok jest tworzony z kodu, oraz forma, która jest wywoływana, gdy widok jest rozwijany z pliku układu. Druga forma analizuje i stosuje atrybuty zdefiniowane w pliku układu.
onFinishInflate() Wywoływana po utworzeniu widoku i wszystkich jego elementów podrzędnych z pliku XML.
Układ onMeasure(int, int) Wywoływana w celu określenia wymagań dotyczących rozmiaru tego widoku i wszystkich jego elementów podrzędnych.
onLayout(boolean, int, int, int, int) Wywoływana, gdy ten widok musi przypisać rozmiar i pozycję do wszystkich swoich elementów podrzędnych.
onSizeChanged(int, int, int, int) Wywoływana, gdy zmieni się rozmiar tego widoku.
Rysunek onDraw(Canvas) Wywoływana, gdy widok musi renderować swoją zawartość.
Przetwarzanie zdarzeń onKeyDown(int, KeyEvent) Wywoływana, gdy wystąpi zdarzenie naciśnięcia klawisza.
onKeyUp(int, KeyEvent) Wywoływana, gdy wystąpi zdarzenie zwolnienia klawisza.
onTrackballEvent(MotionEvent) Wywoływana, gdy wystąpi zdarzenie ruchu trackballa.
onTouchEvent(MotionEvent) Wywoływana, gdy wystąpi zdarzenie ruchu na ekranie dotykowym.
Koncentracja onFocusChanged(boolean, int, Rect) Wywoływana, gdy widok zyskuje lub traci fokus.
onWindowFocusChanged(boolean) Wywoływana, gdy okno zawierające widok zyskuje lub traci fokus.
Dołączanie onAttachedToWindow() Wywoływana, gdy widok jest dołączony do okna.
onDetachedFromWindow() Wywoływana, gdy widok jest odłączony od okna.
onWindowVisibilityChanged(int) Wywoływana, gdy zmienia się widoczność okna zawierającego widok.

Złożone elementy sterujące

Jeśli nie chcesz tworzyć w pełni dostosowanego komponentu, ale chcesz utworzyć komponent wielokrotnego użytku składający się z grupy istniejących elementów sterujących, najlepszym rozwiązaniem może być utworzenie komponentu złożonego (lub złożonego elementu sterującego). Podsumowując, łączy to kilka bardziej szczegółowych elementów sterujących lub widoków w logiczną grupę elementów, które można traktować jako jedną całość. Na przykład pole kombi może być połączeniem pola EditText jedno wierszowego i sąsiadującego z nim przycisku z dołączoną listą wyskakującą. Jeśli użytkownik kliknie przycisk i wybierze coś z listy, pole EditText zostanie wypełnione, ale w razie potrzeby może też wpisać coś bezpośrednio w polu EditText.

W Androidzie są dostępne 2 inne widoki, które umożliwiają to działanie: SpinnerAutoCompleteTextView. Niezależnie od tego ta koncepcja pola kombi jest dobrym przykładem.

Aby utworzyć komponent złożony:

  • Podobnie jak w przypadku Activity, możesz użyć podejścia deklaratywnego (opartego na XML) do tworzenia komponentów zawartych lub zagnieżdżać je programowo w kodzie. Zwykle punktem wyjścia jest jakiś Layout, więc utwórz klasę, która rozszerza Layout. W przypadku pola wyboru możesz użyć LinearLayout o orientacji poziomej. Możesz zagnieżdżać w nim inne układy, dzięki czemu komponent złożony może być dowolnie złożony i strukturalny.
  • W konstruktorze nowej klasy przyjmij wszystkie parametry, których oczekuje nadklasa, i przekaż je najpierw do konstruktora nadklasy. Następnie możesz skonfigurować inne widoki do użycia w nowym komponencie. W tym miejscu utworzysz pole EditText i listę wyskakującą. Możesz wprowadzić do pliku XML własne atrybuty i parametry, które konstruktor może pobrać i wykorzystać.
  • Opcjonalnie możesz utworzyć detektory zdarzeń, które mogą generować widoki zawarte. Przykładem jest metoda detektora kliknięcia elementu listy, która aktualizuje zawartość elementu EditText po dokonaniu wyboru na liście.
  • Opcjonalnie możesz utworzyć własne właściwości z funkcjami dostępu i modyfikatorami. Na przykład ustaw wartość EditText początkowo w komponencie i w razie potrzeby wysyłaj zapytania o jego zawartość.
  • Opcjonalnie możesz zastąpić wartości onDraw()onMeasure(). Zwykle nie jest to konieczne w przypadku rozszerzania Layout, ponieważ układ ma domyślne działanie, które prawdopodobnie będzie odpowiednie.
  • Opcjonalnie możesz zastąpić inne metody on, np. onKeyDown(), aby wybrać określone wartości domyślne z listy wyskakującej pola kombi po naciśnięciu określonego klawisza.

Używanie elementu Layout jako podstawy niestandardowego komponentu ma wiele zalet, m.in.:

  • Układ możesz określić za pomocą deklaratywnych plików XML, tak jak w przypadku ekranu aktywności, lub utworzyć widoki programowo i zagnieździć je w układzie z poziomu kodu.
  • Metody onDraw()onMeasure() oraz większość innych metod on działają prawidłowo, więc nie musisz ich zastępować.
  • Możesz szybko tworzyć dowolnie złożone widoki złożone i używać ich ponownie tak, jakby były pojedynczym komponentem.

Modyfikowanie dotychczasowego typu widoku

Jeśli istnieje komponent podobny do tego, którego potrzebujesz, możesz go rozszerzyć i zastąpić zachowanie, które chcesz zmienić. Możesz robić wszystko to, co w przypadku w pełni dostosowanego komponentu, ale zaczynając od bardziej wyspecjalizowanej klasy w hierarchii View, możesz bezpłatnie uzyskać zachowanie, które Cię interesuje.

Na przykład aplikacja przykładowa NotePad pokazuje wiele aspektów korzystania z platformy Android. Jedną z nich jest rozszerzenie widoku, aby utworzyć notatnik w linie.EditText To nie jest idealny przykład, a interfejsy API do tego celu mogą się zmienić, ale pokazuje on zasady działania.

Jeśli jeszcze tego nie zrobiono, zaimportuj przykładową aplikację NotePad do Androida Studio lub wyświetl kod źródłowy, korzystając z podanego linku. W szczególności zapoznaj się z definicją LinedEditText w pliku NoteEditor.java.

Oto kilka kwestii, na które warto zwrócić uwagę w tym pliku:

  1. Definicja

    Klasa jest zdefiniowana za pomocą tego wiersza:
    public static class LinedEditText extends EditText

    LinedEditText jest zdefiniowana jako klasa wewnętrzna w aktywności NoteEditor, ale jest publiczna, więc można do niej uzyskać dostęp jako NoteEditor.LinedEditText spoza klasy NoteEditor.

    Poza tym LinedEditText jest static, co oznacza, że nie generuje tzw. „metod syntetycznych”, które umożliwiają dostęp do danych z klasy nadrzędnej. Oznacza to, że zachowuje się jak osobna klasa, a nie coś ściśle powiązanego z NoteEditor. Jest to prostszy sposób tworzenia klas wewnętrznych, jeśli nie potrzebują one dostępu do stanu klasy zewnętrznej. Dzięki temu wygenerowana klasa jest mała i można jej łatwo używać w innych klasach.

    LinedEditText rozszerza EditText, czyli widok, który należy w tym przypadku dostosować. Gdy skończysz, nowe zajęcia mogą zastąpić normalny widok EditText.

  2. Inicjowanie klasy

    Jak zawsze najpierw wywoływana jest funkcja super. Nie jest to konstruktor domyślny, ale konstruktor sparametryzowany. Element EditText jest tworzony z tymi parametrami, gdy jest rozwijany z pliku układu XML. Dlatego konstruktor musi je przyjmować i przekazywać do konstruktora klasy nadrzędnej.

  3. Zastąpione metody

    W tym przykładzie zastąpiona jest tylko metoda onDraw(), ale podczas tworzenia własnych komponentów niestandardowych może być konieczne zastąpienie innych metod.

    W tym przykładzie zastąpienie metody onDraw() umożliwia rysowanie niebieskich linii na EditText. Obiekt Canvas jest przekazywany do zastąpionej metody onDraw(). Metoda super.onDraw() jest wywoływana przed zakończeniem metody. Metoda klasy nadrzędnej musi zostać wywołana. W takim przypadku wywołaj go na końcu po narysowaniu linii, które chcesz uwzględnić.

  4. Komponent niestandardowy

    Masz już komponent niestandardowy, ale jak go używać? W przykładzie NotePad komponent niestandardowy jest używany bezpośrednio z układu deklaratywnego, więc zajrzyj do note_editor.xml w folderze res/layout:

    <view xmlns:android="http://schemas.android.com/apk/res/android"
        class="com.example.android.notepad.NoteEditor$LinedEditText"
        android:id="@+id/note"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:background="@android:color/transparent"
        android:padding="5dp"
        android:scrollbars="vertical"
        android:fadingEdge="vertical"
        android:gravity="top"
        android:textSize="22sp"
        android:capitalize="sentences"
    />

    Komponent niestandardowy jest tworzony jako widok ogólny w XML, a klasa jest określana za pomocą pełnego pakietu. Zdefiniowana klasa wewnętrzna jest przywoływana za pomocą notacji NoteEditor$LinedEditText, która jest standardowym sposobem odwoływania się do klas wewnętrznych w języku programowania Java.

    Jeśli komponent widoku niestandardowego nie jest zdefiniowany jako klasa wewnętrzna, możesz zadeklarować komponent widoku za pomocą nazwy elementu XML i wykluczyć atrybut class. Na przykład:

    <com.example.android.notepad.LinedEditText
      id="@+id/note"
      ... />

    Zwróć uwagę, że klasa LinedEditText jest teraz osobnym plikiem klasy. Jeśli klasa jest zagnieżdżona w klasie NoteEditor, ta technika nie działa.

    Pozostałe atrybuty i parametry w definicji to te, które są przekazywane do konstruktora komponentu niestandardowego, a następnie do konstruktora EditText, więc są to te same parametry, których używasz w przypadku widoku EditText. Możesz też dodawać własne parametry.

Tworzenie komponentów niestandardowych jest tak skomplikowane, jak tego potrzebujesz.

Bardziej zaawansowany komponent może zastąpić jeszcze więcej on metod i wprowadzić własne metody pomocnicze, znacznie dostosowując swoje właściwości i działanie. Ogranicza Cię tylko wyobraźnia i to, do czego ma służyć komponent.