Tworzenie komponentów widoku niestandardowego

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

Android oferuje zaawansowany i zaawansowany model z komponentami do tworzenia interfejsu użytkownika oparty na podstawowych klasach układu View i ViewGroup. Platforma zawiera różne gotowe podklasy View i ViewGroup, nazywane odpowiednio widżetami i układami, których można używać do tworzenia interfejsu użytkownika.

Częściowa lista dostępnych widżetów to Button, TextView, EditText, ListView, CheckBox, RadioButton, Gallery, Spinner i bardziej specjalne AutoCompleteTextView, ImageSwitcher i TextSwitcher.

Dostępnych jest m.in. układy LinearLayout, FrameLayout, RelativeLayout i inne. Więcej przykładów znajdziesz w artykule Typowe układy.

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

Utworzenie własnych podklas View daje Ci precyzyjną kontrolę nad wyglądem i działaniem elementu ekranu. Aby dać wyobrażenie o możliwościach, jakie dają widoki niestandardowe, podajemy kilka przykładów:

  • Możesz utworzyć całkowicie renderowany typ View, np. pokrętło „regulacji głośności” wyrenderowane za pomocą grafiki 2D, który przypomina analogowe sterowanie elektroniczne.
  • Grupę komponentów View możesz połączyć w jeden element. Może to być na przykład pole kombi (wyskakujące okienko z polem tekstowym w polu tekstowym), element sterujący podwójnym selektorem (lewy i prawy panel z listą w każdym miejscu, w którym możesz ponownie przypisać element na liście) itd.
  • Możesz zastąpić sposób renderowania komponentu EditText na ekranie. Przykładowa aplikacja Notatnik wykorzystuje to z dobrym wynikiem do utworzenia strony notatnika z liniami.
  • Możesz rejestrować inne zdarzenia, takie jak naciśnięcia klawiszy, i obsługiwać je w niestandardowy sposób, np. podczas gry.

Poniżej dowiesz się, jak tworzyć widoki niestandardowe i używać ich w aplikacji. Szczegółowe informacje znajdziesz w opisie klasy View.

Podstawowe podejście

Oto ogólny przegląd niezbędnych informacji do tworzenia własnych komponentów View:

  1. Rozszerz istniejące zajęcia View lub podklasy o własne zajęcia.
  2. Zastąp niektóre metody z klasy nadrzędnej. Metody nadrzędne do zastąpienia zaczynają się od on, na przykład onDraw(), onMeasure() i onKeyDown(). Jest to podobne do zdarzeń on w Activity lub ListActivity, które zastępujesz na potrzeby punktów zaczepienia cyklu życia i innych funkcji.
  3. Użyj nowej klasy rozszerzenia. Gdy to zrobisz, możesz użyć nowej klasy rozszerzenia zamiast widoku, na podstawie którego została utworzona.

Komponenty w pełni dostosowane

Możesz tworzyć w pełni dostosowane komponenty graficzne i wyświetlać je w dowolny sposób. Może to być np. graficzny wskaźnik VU, który wygląda jak stary miernik analogowy, lub widok tekstu do śpiewania, w którym odbijająca się kula porusza się, gdy śpiewasz przy urządzeniu do karaoke. Być może potrzebujesz funkcji, której nie da się zrobić we wszystkich komponentach.

Na szczęście możesz tworzyć komponenty, które wyglądają i działają w dowolny sposób, ograniczając się tylko Twoją wyobraźnią, rozmiarem ekranu i dostępną mocą obliczeniową. Pamiętaj, że Twoja aplikacja może działać na znacznie mniej wydajnym urządzeniu niż komputerowa stacja robocza.

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

  • Najbardziej ogólny widok, jaki możesz rozszerzyć, to View, więc zwykle na początek tworzysz nowy superkomponent.
  • Możesz dostarczyć konstruktor, który może pobierać atrybuty i parametry z pliku XML oraz korzystać z własnych atrybutów i parametrów, takich jak kolor i zasięg wskaźnika VU lub szerokość i tłumienie igły.
  • Prawdopodobnie chcesz tworzyć własne detektory zdarzeń, metody dostępu właściwości i modyfikatory, a także bardziej zaawansowane działania w klasie komponentu.
  • Prawie na pewno chcesz zastąpić atrybut onMeasure(). Prawdopodobnie będzie też potrzebny zastąpienie onDraw(), jeśli chcesz, by komponent wyświetlał coś. Oba mają działanie domyślne, ale domyślny onDraw() nic nie robi, a domyślny rozmiar onMeasure() zawsze ustawia rozmiar 100 x 100, którego prawdopodobnie nie chcesz używać.
  • W razie potrzeby możesz też zastąpić inne metody on.

Rozszerz onDraw() i onMeasure()

Metoda onDraw() dostarcza element Canvas, do którego możesz zaimplementować cokolwiek, co chcesz: grafikę 2D, inne komponenty standardowe lub niestandardowe, stylizowany tekst lub wszystko, co tylko wymyślisz.

onMeasure() ma większe znaczenie. onMeasure() to kluczowy element umowy renderowania między komponentem a jego kontenerem. Element onMeasure() musi być zastąpiony, aby efektywnie i dokładnie raportować pomiary jego części. Jeszcze bardziej komplikuje to wymagania dotyczące limitów od zasobu nadrzędnego – które są przekazywane do metody onMeasure() – oraz wymaganie wywoływania metody setMeasuredDimension() z mierzoną szerokością i wysokością po ich obliczeniu. Jeśli nie wywołasz tej metody za pomocą zastąpionej metody onMeasure(), w czasie pomiaru stanie się to wyjątek.

Ogólnie implementacja onMeasure() wygląda tak:

  • Zastąpiona metoda onMeasure() jest wywoływana ze specyfikacjami szerokości i wysokości, które są traktowane jako wymagania dotyczące ograniczeń szerokości i wysokości pomiarów. Parametry widthMeasureSpec i heightMeasureSpec są liczbami całkowitymi reprezentującymi wymiary. Pełne informacje o ograniczeniach, których mogą wymagać te specyfikacje, znajdziesz w dokumentacji referencyjnej View.onMeasure(int, int) Ta dokumentacja referencyjna objaśnia również całą operację pomiaru.
  • Metoda onMeasure() komponentu oblicza szerokość i wysokość pomiaru, które są wymagane do wyrenderowania komponentu. Muszą być zgodne z przekazanymi specyfikacjami, ale mogą też przekraczać te wymagania. W tym przypadku rodzic może wybrać, co zrobić – na przykład przyciąć plik, przewinąć, zgłosić wyjątek lub poprosić onMeasure() o spróbowanie jeszcze raz, np. z innymi specyfikacjami pomiaru.
  • Podczas obliczania szerokości i wysokości wywołaj metodę setMeasuredDimension(int width, int height) z obliczonymi pomiarami. Jeśli tego nie zrobisz, zostanie uznany za wyjątek.

Oto podsumowanie innych standardowych metod, z których korzystają widoki danych w ramach platformy:

Kategoria Metody Opis
na podstawie trendów Konstruktorzy Istnieje forma konstruktora, która jest wywoływana, gdy widok jest tworzony na podstawie kodu, i formularz, który jest wywoływany, gdy widok zostanie przekroczony z pliku układu. W drugim formularzu analizuje i stosuje atrybuty zdefiniowane w pliku układu.
onFinishInflate() Wywoływane po wyświetleniu widoku i wszystkich jego elementach 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ływane, gdy ten widok musi przypisać rozmiar i pozycję do wszystkich swoich elementów podrzędnych.
onSizeChanged(int, int, int, int) Wywoływane po zmianie rozmiaru tego widoku.
Rysunek onDraw(Canvas) Wywoływana, gdy widok musi wyrenderować swoją zawartość.
Przetwarzanie zdarzeń onKeyDown(int, KeyEvent) Wywoływane, gdy wystąpi zdarzenie wyłączenia klucza.
onKeyUp(int, KeyEvent) Wywoływane po wystąpieniu zdarzenia kluczowego.
onTrackballEvent(MotionEvent) Wywoływane po wystąpieniu zdarzenia ruchu kulki.
onTouchEvent(MotionEvent) Wywoływane po wystąpieniu zdarzenia ruchu na ekranie dotykowym.
główny temat filmu, onFocusChanged(boolean, int, Rect) Wywoływane, gdy widok zyskuje lub przestaje być aktywny.
onWindowFocusChanged(boolean) Wywoływane, gdy okno zawierające widok zyskuje lub traci zaznaczenie.
Dołączanie onAttachedToWindow() Wywoływana, gdy widok jest dołączony do okna.
onDetachedFromWindow() Wywoływana po odłączeniu widoku od okna.
onWindowVisibilityChanged(int) Wywoływane po zmianie widoczności okna zawierającego widok.

Elementy sterujące komponentem

Jeśli nie chcesz tworzyć w pełni niestandardowego komponentu, ale wolisz połączyć komponent wielokrotnego użytku z grupą istniejących elementów sterujących, najlepszym rozwiązaniem będzie utworzenie komponentu złożonego (czyli elementu sterującego złożonego). Podsumowując, takie działanie łączy wiele bardziej atomowych elementów sterujących lub widoków w logiczną grupę elementów, które można traktować jak jedną rzecz. Na przykład pole kombi może się składać z pojedynczego pola EditText w pojedynczym wierszu i przyległego przycisku z dołączoną listą wyskakujących okienek. Jeśli użytkownik kliknie przycisk i wybierze coś z listy, pole EditText zostanie wypełnione. Jeśli wolisz, może też wpisać coś bezpośrednio w polu EditText.

Na urządzeniu z Androidem możesz to zrobić za pomocą 2 innych widoków: Spinner i AutoCompleteTextView. Dobrym przykładem jest jednak to pole kombi.

Aby utworzyć komponent złożony:

  • Podobnie jak w przypadku obiektu Activity, utwórz zawarte w nim komponenty za pomocą deklaratywnej (opartej na formacie XML) lub zagnieźdź je programowo na podstawie kodu. Zazwyczaj punktem początkowym jest Layoutjakiś rodzaj, utwórz więc klasę, która rozszerza Layout. W przypadku pola kombi możesz użyć pola LinearLayout w orientacji poziomej. W środku możesz zagnieździć inne układy, aby komponent złożony może być dowolnie złożony i mieć strukturę.
  • W konstruktorze nowej klasy pobierz parametry oczekiwane przez klasę nadrzędną i najpierw przekaż je do konstruktora klasy nadrzędnej. Następnie możesz skonfigurować inne widoki do użycia w nowym komponencie. Tutaj utworzysz pole EditText i listę w wyskakującym okienku. Możesz wprowadzić do kodu XML własne atrybuty i parametry, które będzie mógł pobierać i wykorzystywać Twój konstruktor.
  • Opcjonalnie utwórz detektory zdarzeń, które mogą generować zawarte przez Ciebie widoki. Przykładem jest odbiornik, który po wybraniu elementu listy aktualizuje zawartość elementu EditText w celu aktualizowania jego zawartości.
  • Opcjonalnie utwórz własne właściwości z akcesoriami i modyfikatorami. Na przykład ustaw wartość EditText w komponencie na początku, a w razie potrzeby wysyłaj zapytanie o jej zawartość.
  • Opcjonalnie zastąp onDraw() i onMeasure(). Zazwyczaj nie jest to konieczne przy rozszerzaniu właściwości Layout, ponieważ układ działa domyślnie i prawdopodobnie zadziała.
  • Opcjonalnie możesz zastąpić inne metody on, takie jak onKeyDown(), np. aby wybrać określone wartości domyślne z wyskakującej listy pola kombi po kliknięciu określonego klawisza.

Użycie elementu Layout jako podstawy elementu sterującego niestandardowego ma swoje zalety:

  • Układ możesz określić za pomocą deklaratywnego pliku XML (tak jak w przypadku ekranu aktywności) lub możesz programowo tworzyć widoki i zagnieżdżać je w układzie z poziomu kodu.
  • Metody onDraw() i onMeasure() oraz większość pozostałych 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 jednym elementem.

Modyfikowanie istniejącego typu widoku

Jeśli występuje komponent podobny do Twojego, możesz go rozszerzyć i zastąpić działanie, które chcesz zmienić. Korzystając z w pełni niestandardowego komponentu, możesz wykonywać wszystkie te czynności, ale zaczynając od bardziej specjalistycznej klasy w hierarchii View, możesz uzyskać pewne zachowanie, które pozwoli Ci to robić bezpłatnie.

Na przykład przykładowa aplikacja NotePad pokazuje wiele aspektów korzystania z platformy Androida. Między innymi rozszerzyliśmy widok EditText, aby utworzyć notatnik z liniami. To nie jest doskonały przykład, a interfejsy API służące do tego celu mogą się zmienić, ale obrazuje zasady.

Zaimportuj przykładowy notatnik do Android Studio lub sprawdź źródło, korzystając z podanego linku. Sprawdź definicję właściwości LinedEditText w pliku NoteEditor.java.

Oto kilka informacji, które warto uwzględnić w tym pliku:

  1. Definicja

    Klasa jest zdefiniowana w tym wierszu:
    public static class LinedEditText extends EditText

    LinedEditText jest zdefiniowaną jako klasa wewnętrzna w aktywności NoteEditor, ale jest publiczna, aby można było uzyskać do niej dostęp jako NoteEditor.LinedEditText spoza klasy NoteEditor.

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

    LinedEditText rozszerza zakres EditText, który w tym przypadku należy dostosować. Gdy skończysz, nowa klasa może zastąpić zwykły widok EditText.

  2. Inicjowanie klas

    Pierwszeństwo jest zawsze nazywane super. Nie jest to konstruktor domyślny, ale z parametrami. Element EditText jest tworzony z tymi parametrami, gdy jest powiększany z pliku układu XML. Dlatego konstruktor musi je przyjąć i przekazać do konstruktora klasy nadrzędnej.

  3. Zastąpione metody

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

    W tym przykładzie zastąpienie metody onDraw() umożliwia malowanie niebieskich linii w obszarze roboczym widoku EditText. Obszar roboczy jest przekazywany do zastąpionej metody onDraw(). Metoda super.onDraw() jest wywoływana przed zakończeniem działania. Należy wywołać metodę nadklasy. W tym przypadku wywołaj go na końcu po pomalowaniu wierszy, które chcesz uwzględnić.

  4. Komponent niestandardowy

    Masz już komponent niestandardowy. Jak mogę go wykorzystać? W przykładzie Notatnika komponent niestandardowy jest używany bezpośrednio z układu deklaratywnego, więc sprawdź 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 ogólny widok w pliku XML, a klasa jest określana w pełnym pakiecie. Do zdefiniowanej klasy wewnętrznej odwołuje się zapis NoteEditor$LinedEditText, który 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 oddzielnym plikiem zajęć. Jeśli klasa jest zagnieżdżona w klasie NoteEditor, ta metoda nie działa.

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

Tworzenie komponentów niestandardowych jest skomplikowane tylko wtedy, gdy jest to potrzebne.

Bardziej zaawansowany komponent może zastąpić jeszcze więcej metod on i wprowadzić własne metody pomocnicze, znacząco dostosowując jego właściwości i działanie. Jedynym ograniczeniem jest Twoja wyobraźnia i to, do czego służy komponent.