Dostosowywanie Androida: jak zbudować komponent interfejsu użytkownika, który robi to, czego chcesz?

Opublikowany: 2022-03-11

Często zdarza się, że programiści potrzebują komponentu interfejsu użytkownika, który nie jest dostarczany przez platformę, na którą są kierowani, lub jest rzeczywiście dostarczany, ale nie ma określonej właściwości lub zachowania. Odpowiedzią na oba scenariusze jest niestandardowy składnik interfejsu użytkownika.

Model interfejsu użytkownika systemu Android jest z natury konfigurowalny, co umożliwia dostosowywanie systemu Android, testowanie i tworzenie niestandardowych składników interfejsu użytkownika na różne sposoby:

  • Dziedzicz istniejący komponent (tj. TextView , ImageView itp.) i dodaj/zastąp potrzebną funkcjonalność. Na przykład CircleImageView , który dziedziczy ImageView , nadpisując funkcję onDraw() w celu ograniczenia wyświetlanego obrazu do okręgu i dodając funkcję loadFromFile() w celu załadowania obrazu z pamięci zewnętrznej.

  • Utwórz komponent złożony z kilku komponentów. Takie podejście zwykle wykorzystuje układy do kontrolowania sposobu rozmieszczenia komponentów na ekranie. Na przykład LabeledEditText , który dziedziczy LinearLayout z orientacją poziomą i zawiera zarówno TextView działającą jako etykieta, jak i EditText działającą jako pole wprowadzania tekstu.

    Takie podejście może również wykorzystać poprzednie, tj. wewnętrzne komponenty mogą być natywne lub niestandardowe.

  • Najbardziej wszechstronnym i najbardziej złożonym podejściem jest stworzenie samodzielnie rysowanego komponentu . W takim przypadku komponent odziedziczy ogólną klasę View i przesłoni funkcje, takie jak onMeasure() , aby określić swój układ, onDraw() , aby wyświetlić jego zawartość itp. Komponenty utworzone w ten sposób zwykle w dużym stopniu zależą od interfejsu API rysowania 2D systemu Android.

Studium przypadku dostosowywania Androida: widok CalendarView

Android udostępnia natywny składnik CalendarView . Działa dobrze i zapewnia minimalną funkcjonalność oczekiwaną od dowolnego komponentu kalendarza, wyświetlając pełny miesiąc i podkreślając bieżący dzień. Niektórzy mogą powiedzieć, że wygląda również dobrze, ale tylko wtedy, gdy wybierasz natywny wygląd i nie jesteś zainteresowany dostosowywaniem tego, jak wygląda.

Na przykład składnik CalendarView nie zapewnia możliwości zmiany sposobu oznaczania określonego dnia ani koloru tła do użycia. Nie ma również możliwości dodania dowolnego niestandardowego tekstu lub grafiki, na przykład z okazji specjalnej okazji. Krótko mówiąc, komponent wygląda tak i prawie nic nie można zmienić:

Zrzut ekranu
CalendarView w motywie AppCompact.Light .

Zrobić własny

Jak więc zabrać się za tworzenie własnego widoku kalendarza? Każde z powyższych podejść zadziała. Jednak praktyczność zazwyczaj wyklucza trzecią opcję (grafika 2D) i pozostawia nam dwie inne metody, a w tym artykule zastosujemy połączenie obu.

Aby kontynuować, możesz znaleźć kod źródłowy tutaj.

1. Układ komponentów

Najpierw zacznijmy od wyglądu komponentu. Aby uprościć sprawę, wyświetlajmy dni w siatce, a na górze nazwę miesiąca wraz z przyciskami „następny miesiąc” i „poprzedni miesiąc”.

Zrzut ekranu
Niestandardowy widok kalendarza.

Ten układ jest zdefiniowany w pliku control_calendar.xml w następujący sposób. Zauważ, że niektóre powtarzające się znaczniki zostały skrócone do ... :

 <?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:andro android:orientation="vertical" android:layout_width="match_parent" android:layout_height="match_parent" android:background="@android:color/white"> <!-- date toolbar --> <RelativeLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:paddingTop="12dp" android:paddingBottom="12dp" android:paddingLeft="30dp" android:paddingRight="30dp"> <!-- prev button --> <ImageView android: android:layout_width="30dp" android:layout_height="30dp" android:layout_centerVertical="true" android:layout_alignParentLeft="true" android:src="@drawable/previous_icon"/> <!-- date title --> <TextView android: android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_centerVertical="true" android:layout_toRightOf="@+id/calendar_prev_button" android:layout_toLeftOf="@+id/calendar_next_button" android:gravity="center" android:textAppearance="@android:style/TextAppearance.Medium" android:textColor="#222222" android:text="current date"/> <!-- next button --> <ImageView android: ... Same layout as prev button. android:src="@drawable/next_icon"/> </RelativeLayout> <!-- days header --> <LinearLayout android: android:layout_width="match_parent" android:layout_height="40dp" android:gravity="center_vertical" android:orientation="horizontal"> <TextView android:layout_width="0dp" android:layout_height="wrap_content" android:layout_weight="1" android:gravity="center_horizontal" android:textColor="#222222" android:text="SUN"/> ... Repeat for MON - SAT. </LinearLayout> <!-- days view --> <GridView android: android:layout_width="match_parent" android:layout_height="340dp" android:numColumns="7"/> </LinearLayout>

2. Klasa komponentów

Poprzedni układ można uwzględnić w Activity lub Fragment bez zmian i będzie działać poprawnie. Ale zamknięcie go jako samodzielnego komponentu interfejsu użytkownika zapobiegnie powtarzaniu kodu i pozwoli na modułową konstrukcję, w której każdy moduł ponosi jedną odpowiedzialność.

Naszym komponentem interfejsu użytkownika będzie LinearLayout , aby dopasować katalog główny pliku układu XML. Zauważ, że tylko ważne części są pokazane w kodzie. Implementacja komponentu znajduje się w CalendarView.java :

 public class CalendarView extends LinearLayout { // internal components private LinearLayout header; private ImageView btnPrev; private ImageView btnNext; private TextView txtDate; private GridView grid; public CalendarView(Context context) { super(context); initControl(context); } /** * Load component XML layout */ private void initControl(Context context) { LayoutInflater inflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE); inflater.inflate(R.layout.control_calendar, this); // layout is inflated, assign local variables to components header = (LinearLayout)findViewById(R.id.calendar_header); btnPrev = (ImageView)findViewById(R.id.calendar_prev_button); btnNext = (ImageView)findViewById(R.id.calendar_next_button); txtDate = (TextView)findViewById(R.id.calendar_date_display); grid = (GridView)findViewById(R.id.calendar_grid); } }

Kod jest dość prosty. Po utworzeniu komponent rozszerza układ XML, a gdy to jest zrobione, przypisuje wewnętrzne kontrolki do zmiennych lokalnych, aby później był do nich łatwiejszy dostęp.

3. Potrzebna jest pewna logika

Aby ten składnik faktycznie zachowywał się jak widok kalendarza, potrzebna jest pewna logika biznesowa. Na początku może się to wydawać skomplikowane, ale tak naprawdę nie ma w tym wiele. Rozłóżmy to:

  1. Widok kalendarza ma siedem dni i gwarantuje, że wszystkie miesiące zaczną się gdzieś w pierwszym rzędzie.

  2. Najpierw musimy ustalić, na jakiej pozycji zaczyna się miesiąc, a następnie wypełnić wszystkie pozycje przed tą liczbą numerami z poprzedniego miesiąca (30, 29, 28.. itd.) aż do pozycji 0.

  3. Następnie wypełniamy dni dla bieżącego miesiąca (1, 2, 3… itd.).

  4. Potem przychodzą dni na następny miesiąc (znowu 1, 2, 3.. itd.), ale tym razem wypełniamy tylko pozostałe pozycje w ostatnich wierszach siatki.

Poniższy diagram ilustruje te kroki:

Zrzut ekranu
Niestandardowa logika biznesowa widoku kalendarza.

Szerokość siatki jest już określona jako siedem komórek, co oznacza kalendarz tygodniowy, ale co z wysokością? Największy rozmiar siatki można określić na podstawie najgorszego scenariusza 31-dniowego miesiąca rozpoczynającego się w sobotę, która jest ostatnią komórką w pierwszym rzędzie, i będzie potrzebować 5 dodatkowych wierszy, aby wyświetlić w całości. Tak więc ustawienie kalendarza tak, aby wyświetlał sześć wierszy (w sumie 42 dni) wystarczy do obsługi wszystkich spraw.

Ale nie wszystkie miesiące mają 31 dni! Możemy uniknąć komplikacji wynikających z tego, korzystając z wbudowanej funkcji daty w Androidzie, unikając konieczności samodzielnego obliczania liczby dni.

Jak wspomniano wcześniej, funkcje dat dostarczane przez klasę Calendar sprawiają, że implementacja jest dość prosta. W naszym komponencie funkcja updateCalendar() realizuje tę logikę:

 private void updateCalendar() { ArrayList<Date> cells = new ArrayList<>(); Calendar calendar = (Calendar)currentDate.clone(); // determine the cell for current month's beginning calendar.set(Calendar.DAY_OF_MONTH, 1); int monthBeginningCell = calendar.get(Calendar.DAY_OF_WEEK) - 1; // move calendar backwards to the beginning of the week calendar.add(Calendar.DAY_OF_MONTH, -monthBeginningCell); // fill cells (42 days calendar as per our business logic) while (cells.size() < DAYS_COUNT) { cells.add(calendar.getTime()); calendar.add(Calendar.DAY_OF_MONTH, 1); } // update grid ((CalendarAdapter)grid.getAdapter()).updateData(cells); // update title SimpleDateFormat sdf = new SimpleDateFormat("MMM yyyy"); txtDate.setText(sdf.format(currentDate.getTime())); }

4. Możliwość dostosowania w Sercu

Ponieważ komponentem odpowiedzialnym za wyświetlanie poszczególnych dni jest GridView , dobrym miejscem do dostosowania sposobu wyświetlania dni jest Adapter , ponieważ jest on odpowiedzialny za przechowywanie danych i napełnianie widoków dla poszczególnych komórek siatki.

W tym przykładzie z naszego CalendearView będziemy wymagać:

  • Współczesny dzień powinien być zaznaczony pogrubionym niebieskim tekstem .
  • Dni poza bieżącym miesiącem powinny być wyszarzone .
  • Dni z wydarzeniem powinny wyświetlać specjalną ikonę.
  • Nagłówek kalendarza powinien zmieniać kolory w zależności od pory roku (lato, jesień, zima, wiosna).

Pierwsze trzy wymagania można łatwo osiągnąć, zmieniając atrybuty tekstu i zasoby w tle. Zaimplementujmy CalendarAdapter do wykonania tego zadania. Jest na tyle proste, że może być klasą członkowską w CalendarView . Nadpisując funkcję getView() , możemy osiągnąć powyższe wymagania:

 @Override public View getView(int position, View view, ViewGroup parent) { // day in question Date date = getItem(position); // today Date today = new Date(); // inflate item if it does not exist yet if (view == null) view = inflater.inflate(R.layout.control_calendar_day, parent, false); // if this day has an event, specify event image view.setBackgroundResource(eventDays.contains(date)) ? R.drawable.reminder : 0); // clear styling view.setTypeface(null, Typeface.NORMAL); view.setTextColor(Color.BLACK); if (date.getMonth() != today.getMonth() || date.getYear() != today.getYear()) { // if this day is outside current month, grey it out view.setTextColor(getResources().getColor(R.color.greyed_out)); } else if (date.getDate() == today.getDate()) { // if it is today, set it to blue/bold view.setTypeface(null, Typeface.BOLD); view.setTextColor(getResources().getColor(R.color.today)); } // set text view.setText(String.valueOf(date.getDate())); return view; }

Ostateczny wymóg projektowy wymaga nieco więcej pracy. Najpierw dodajmy kolory dla czterech pór roku w /res/values/colors.xml :

 <color name="summer">#44eebd82</color> <color name="fall">#44d8d27e</color> <color name="winter">#44a1c1da</color> <color name="spring">#448da64b</color>

Następnie użyjmy tablicy do zdefiniowania pory roku dla każdego miesiąca (zakładając dla uproszczenia półkulę północną; przepraszam Australię!). W CalendarView dodajemy następujące zmienne składowe:

 // seasons' rainbow int[] rainbow = new int[] { R.color.summer, R.color.fall, R.color.winter, R.color.spring }; int[] monthSeason = new int[] {2, 2, 3, 3, 3, 0, 0, 0, 1, 1, 1, 2};

W ten sposób wybór odpowiedniego koloru odbywa się poprzez wybranie odpowiedniej pory roku ( monthSeason[currentMonth] ), a następnie wybranie odpowiedniego koloru ( rainbow[monthSeason[currentMonth] ), jest to dodawane do updateCalendar() , aby upewnić się, że wybrano odpowiedni kolor każdorazowe zmiany kalendarza.

 // set header color according to current season int month = currentDate.get(Calendar.MONTH); int season = monthSeason[month]; int color = rainbow[season]; header.setBackgroundColor(getResources().getColor(color));

Dzięki temu otrzymujemy następujący wynik:

Dostosowywanie Androida
Kolor nagłówka zmienia się w zależności od pory roku.

Ważna uwaga ze względu na sposób, w jaki HashSet porównuje obiekty, powyższe sprawdzenie eventDays.contains(date) w updateCalendar() nie zwróci wartości true dla obiektów daty, chyba że są one dokładnie identyczne. Nie wykonuje żadnych specjalnych kontroli dla typu danych Date . Aby obejść ten problem, to sprawdzenie jest zastępowane następującym kodem:

 for (Date eventDate : eventDays) { if (eventDate.getDate() == date.getDate() && eventDate.getMonth() == date.getMonth() && eventDate.getYear() == date.getYear()) { // mark this day for event view.setBackgroundResource(R.drawable.reminder); break; } }

5. Wygląda brzydko w czasie projektowania

Wybór przez Androida symboli zastępczych w czasie projektowania może być wątpliwy. Na szczęście Android tworzy instancję naszego komponentu w celu renderowania go w projektancie interfejsu użytkownika i możemy to wykorzystać, wywołując updateCalendar() w konstruktorze komponentu. W ten sposób komponent będzie miał sens w czasie projektowania.

Zrzut ekranu

Jeśli inicjowanie komponentu wymaga dużej ilości przetwarzania lub ładowania dużej ilości danych, może to wpłynąć na wydajność środowiska IDE. W tym przypadku system Android udostępnia fajną funkcję o nazwie isInEditMode() , której można użyć do ograniczenia ilości danych używanych, gdy komponent jest faktycznie tworzony w projektancie interfejsu użytkownika. Na przykład, jeśli istnieje wiele zdarzeń do załadowania do CalendarView , możemy użyć isInEditMode() wewnątrz funkcji updateCalendar() , aby zapewnić pustą/ograniczoną listę zdarzeń w trybie projektowania, a w przeciwnym razie załadować prawdziwą.

6. Wywołanie komponentu

Komponent można umieścić w plikach układu XML (przykładowe użycie można znaleźć w activity_main.xml ):

 <samples.aalamir.customcalendar.CalendarView android: android:layout_width="match_parent" android:layout_height="wrap_content"/>

I pobierane do interakcji po załadowaniu układu:

 HashSet<Date> events = new HashSet<>(); events.add(new Date()); CalendarView cv = ((CalendarView)findViewById(R.id.calendar_view)); cv.updateCalendar(events);

Powyższy kod tworzy HashSet zdarzeń, dodaje do niego bieżący dzień, a następnie przekazuje go do CalendarView . W rezultacie CalendarView wyświetli bieżący dzień pogrubioną czcionką na niebiesko, a także umieści na nim znacznik zdarzenia:

Zrzut ekranu
CalendarView wyświetlający wydarzenie

7. Dodawanie atrybutów

Innym ułatwieniem dostarczanym przez Androida jest przypisanie atrybutów do niestandardowego komponentu. Dzięki temu deweloperzy systemu Android korzystający ze składnika mogą wybierać ustawienia za pomocą kodu XML układu i natychmiast wyświetlać wyniki w projektancie interfejsu użytkownika, w przeciwieństwie do konieczności czekania i sprawdzania, jak wygląda CalendarView w czasie wykonywania. Dodajmy możliwość zmiany formatu wyświetlania daty w komponencie, na przykład, aby przeliterować pełną nazwę miesiąca zamiast trzyliterowego skrótu.

Aby to zrobić, potrzebne są następujące kroki:

  • Zadeklaruj atrybut. Nazwijmy go dateFormat i dateFormat mu typ danych typu string . Dodaj go do /res/values/attrs.xml :
 <resources> <declare-styleable name="CalendarDateElement"> <attr name="dateFormat" format="string"/> </declare-styleable> </resources>
  • Użyj atrybutu w układzie, który używa komponentu, i nadaj mu wartość "MMMM yyyy" :
 <samples.aalamir.customcalendar.CalendarView xmlns:calendarNS="http://schemas.android.com/apk/res/samples.aalamir.customcalendar" android: android:layout_width="match_parent" android:layout_height="wrap_content" calendarNS:dateFormat="MMMM yyyy"/>
  • Wreszcie, niech komponent korzysta z wartości atrybutu:
 TypedArray ta = getContext().obtainStyledAttributes(attrs, R.styleable.CalendarView); dateFormat = ta.getString(R.styleable.CalendarView_dateFormat);

] Zbuduj projekt, a zauważysz, że wyświetlana data zmienia się w projektancie interfejsu użytkownika, aby używać pełnej nazwy miesiąca, np. „Lipiec 2015”. Spróbuj podać różne wartości i zobacz, co się stanie.

Zrzut ekranu
Zmiana atrybutów CalendarView .

8. Interakcja z komponentem

Czy próbowałeś naciskać w konkretnym dniu? Wewnętrzne elementy interfejsu użytkownika w naszym komponencie nadal zachowują się w normalny oczekiwany sposób i będą uruchamiać zdarzenia w odpowiedzi na działania użytkownika. Jak więc radzimy sobie z tymi wydarzeniami?

Odpowiedź składa się z dwóch części:

  • Przechwytuj zdarzenia wewnątrz komponentu i
  • Zgłaszaj zdarzenia do rodzica komponentu (może to być Fragment , Activity lub nawet inny składnik).

Pierwsza część jest dość prosta. Na przykład, aby obsłużyć długo naciskane elementy siatki, przypisujemy odpowiedni odbiornik w naszej klasie komponentów:

 // long-pressing a day grid.setOnItemLongClickListener(new AdapterView.OnItemLongClickListener() { @Override public boolean onItemLongClick(AdapterView<?> view, View cell, int position, long id) { // handle long-press if (eventHandler == null) return false; Date date = view.getItemAtPosition(position); eventHandler.onDayLongPress(date); return true; } });

Istnieje kilka metod zgłaszania zdarzeń. Bezpośrednim i prostym jest skopiowanie sposobu, w jaki robi to Android: zapewnia interfejs do zdarzeń komponentu, który jest zaimplementowany przez rodzica komponentu ( eventHandler w powyższym fragmencie kodu).

Do funkcji interfejsu można przekazywać dowolne dane, które są istotne dla aplikacji. W naszym przypadku interfejs musi ujawnić jeden program obsługi zdarzeń, który minął datę naciśniętego dnia. W CalendarView zdefiniowano następujący interfejs:

 public interface EventHandler { void onDayLongPress(Date date); }

Implementację dostarczoną przez rodzica można dostarczyć do widoku kalendarza za pomocą setEventHandler() . Oto przykładowe użycie z `MainActivity.java':

 @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); HashSet<Date> events = new HashSet<>(); events.add(new Date()); CalendarView cv = ((CalendarView)findViewById(R.id.calendar_view)); cv.updateCalendar(events); // assign event handler cv.setEventHandler(new CalendarView.EventHandler() { @Override public void onDayLongPress(Date date) { // show returned day DateFormat df = SimpleDateFormat.getDateInstance(); Toast.makeText(MainActivity.this, df.format(date), LENGTH_SHORT).show(); } }); }

Długie naciśnięcie dnia wywoła zdarzenie długiego naciśnięcia, które jest przechwytywane i obsługiwane przez GridView i zgłaszane przez wywołanie onDayLongPress() w dostarczonej implementacji, która z kolei pokaże datę naciśniętego dnia na ekranie:

Zrzut ekranu

Innym, bardziej zaawansowanym sposobem radzenia sobie z tym jest użycie funkcji Android's Intents i BroadcastReceivers . Jest to szczególnie przydatne, gdy kilka komponentów musi zostać powiadomionych o wydarzeniu w kalendarzu. Na przykład, jeśli naciśnięcie dnia w kalendarzu wymaga wyświetlenia tekstu w Activity i pobrania pliku przez Service działającą w tle .

Użycie poprzedniego podejścia będzie wymagało Activity , aby dostarczyło EventHandler do składnika, obsłużyło zdarzenie, a następnie przesłało je do Service . Zamiast tego, posiadanie przez komponent rozgłaszania Intent i akceptacja zarówno Activity , jak i Service za pośrednictwem własnych BroadcastReceivers nie tylko ułatwia życie, ale także pomaga oddzielić Activity od danej Service .

Wniosek

Zobacz niesamowitą moc dostosowywania Androida!
Ćwierkać

Oto jak w kilku prostych krokach tworzysz własny niestandardowy komponent:

  • Utwórz układ XML i dostosuj go do swoich potrzeb.
  • Wyprowadź klasę komponentu z odpowiedniego komponentu nadrzędnego, zgodnie z układem XML.
  • Dodaj logikę biznesową swojego komponentu.
  • Użyj atrybutów, aby umożliwić użytkownikom modyfikowanie zachowania komponentu.
  • Aby ułatwić korzystanie z komponentu w projektancie interfejsu użytkownika, użyj funkcji isInEditMode() systemu Android.

W tym artykule stworzyliśmy widok kalendarza jako przykład, głównie dlatego, że pod wieloma względami brakuje widoku kalendarza giełdowego. Ale nie jesteś w żaden sposób ograniczony co do rodzaju komponentów, które możesz stworzyć. Możesz użyć tej samej techniki, aby stworzyć wszystko, czego potrzebujesz, niebo jest granicą!

Dziękuję za przeczytanie tego przewodnika, życzę powodzenia w programowaniu!