Personalizare Android: Cum să construiți o componentă UI care face ceea ce doriți
Publicat: 2022-03-11Nu este neobișnuit ca dezvoltatorii să aibă nevoie de o componentă de UI care fie nu este furnizată de platforma pe care o vizează, fie este, într-adevăr, furnizată, dar nu are o anumită proprietate sau comportament. Răspunsul la ambele scenarii este o componentă personalizată a interfeței de utilizare.
Modelul Android UI este în mod inerent personalizabil, oferind mijloace de personalizare, testare Android și capacitatea de a crea componente personalizate de UI în diferite moduri:
Moșteniți o componentă existentă (ex.
TextView
,ImageView
etc.) și adăugați/înlocuiți funcționalitatea necesară. De exemplu, unCircleImageView
care moșteneșteImageView
, suprascriind funcțiaonDraw()
pentru a restricționa imaginea afișată la un cerc și adăugând oloadFromFile()
pentru a încărca o imagine din memoria externă.Creați o componentă compusă din mai multe componente. Această abordare profită de obicei de Aspecte pentru a controla modul în care componentele sunt aranjate pe ecran. De exemplu, un
LabeledEditText
care moșteneșteLinearLayout
cu orientare orizontală și conține atât unTextView
care acționează ca o etichetă, cât și unEditText
care acționează ca un câmp de introducere a textului.Această abordare ar putea folosi și cea anterioară, adică componentele interne ar putea fi native sau personalizate.
Cea mai versatilă și mai complexă abordare este crearea unei componente auto-desenate . În acest caz, componenta ar moșteni clasa generică
View
și va suprascrie funcții precumonMeasure()
pentru a-și determina aspectul,onDraw()
pentru a-și afișa conținutul etc. Componentele create în acest fel depind de obicei foarte mult de API-ul de desen 2D al Android.
Studiu de caz de personalizare Android: CalendarView
Android oferă o componentă nativă CalendarView
. Funcționează bine și oferă funcționalitatea minimă așteptată de la orice componentă a calendarului, afișând o lună întreagă și evidențiind ziua curentă. Unii ar putea spune că arată bine, dar numai dacă sunteți în căutarea unui aspect nativ și nu aveți niciun interes să personalizați cum arată.
De exemplu, componenta CalendarView
nu oferă nicio modalitate de a schimba modul în care este marcată o anumită zi sau ce culoare de fundal să fie utilizată. De asemenea, nu există nicio modalitate de a adăuga text sau grafică personalizată, pentru a marca o ocazie specială, de exemplu. Pe scurt, componenta arată astfel și aproape nimic nu poate fi schimbat:
CalendarView
în AppCompact.Light
.
Fă-ți propriul tău
Deci, cum se procedează pentru a-și crea propria vizualizare a calendarului? Oricare dintre abordările de mai sus ar funcționa. Cu toate acestea, caracterul practic va exclude, de obicei, a treia opțiune (grafică 2D) și ne va lăsa cu celelalte două metode, iar în acest articol vom folosi un amestec al ambelor.
Pentru a urmări, puteți găsi codul sursă aici.
1. Aspectul componentelor
Mai întâi, să începem cu cum arată componenta. Pentru a rămâne simplu, să afișăm zilele într-o grilă și, în partea de sus, numele lunii împreună cu butoanele „luna următoare” și „luna anterioară”.
Acest aspect este definit în fișierul control_calendar.xml
, după cum urmează. Rețineți că unele markupuri repetitive au fost prescurtate cu ...
:
<?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. Clasa de componente
Aspectul anterior poate fi inclus așa cum este într-o Activity
sau într-un Fragment
și va funcționa bine. Dar încapsularea acestuia ca componentă de interfață autonomă va preveni repetarea codului și va permite un design modular, în care fiecare modul se ocupă de o singură responsabilitate.
Componenta noastră UI va fi un LinearLayout
, pentru a se potrivi cu rădăcina fișierului de aspect XML. Rețineți că numai părțile importante sunt afișate din cod. Implementarea componentei rezidă în 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); } }
Codul este destul de simplu. La creare, componenta umflă aspectul XML și, când aceasta este făcută, atribuie controalele interne variabilelor locale pentru un acces mai ușor mai târziu.
3. Este nevoie de ceva logică
Pentru ca această componentă să se comporte ca o vizualizare calendaristică, este necesară o anumită logică de afaceri. Poate părea complicat la început, dar chiar nu este mare lucru. Să o descompunem:
Vizualizarea calendarului are șapte zile și este garantat că toate lunile vor începe undeva în primul rând.
Mai întâi, trebuie să ne dăm seama la ce poziție începe luna, apoi completăm toate pozițiile anterioare cu numerele din luna anterioară (30, 29, 28.. etc.) până ajungem la poziția 0.
Apoi, completăm zilele pentru luna curentă (1, 2, 3... etc).
După aceea vin zilele pentru luna următoare (din nou, 1, 2, 3.. etc), dar de data aceasta umplem doar pozițiile rămase în ultimul rând(e) al grilei.
Următoarea diagramă ilustrează acești pași:
Lățimea grilei este deja specificată pentru a fi șapte celule, indicând un calendar săptămânal, dar ce zici de înălțime? Cea mai mare dimensiune a grilei poate fi determinată de cel mai rău scenariu al unei luni de 31 de zile care începe într-o sâmbătă, care este ultima celulă din primul rând și va avea nevoie de încă 5 rânduri pentru a fi afișate în întregime. Deci, setarea calendarului pentru a afișa șase rânduri (în total 42 de zile) va fi suficientă pentru a gestiona toate cazurile.
Dar nu toate lunile au 31 de zile! Putem evita complicațiile care decurg din aceasta utilizând funcționalitatea de dată încorporată a Android, evitând nevoia să ne dăm seama de numărul de zile.
După cum am menționat anterior, funcționalitățile date oferite de clasa Calendar
fac implementarea destul de simplă. În componenta noastră, funcția updateCalendar()
implementează această logică:
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. Personalizat la inimă
Deoarece componenta responsabilă pentru afișarea zilelor individuale este un GridView
, un loc bun pentru a personaliza modul în care sunt afișate zilele este Adapter
, deoarece este responsabil pentru păstrarea datelor și umflarea vizualizărilor pentru celulele grilei individuale.
Pentru acest exemplu, vom solicita următoarele de la CalendearView
:
- Ziua actuală ar trebui să fie în text albastru aldine .
- Zilele din afara lunii curente ar trebui să fie închise cu gri .
- Zilele cu un eveniment ar trebui să afișeze o pictogramă specială.
- Antetul calendarului ar trebui să-și schimbe culorile în funcție de sezon (vară, toamnă, iarnă, primăvară).
Primele trei cerințe sunt ușor de îndeplinit prin modificarea atributelor textului și a resurselor de fundal. Să implementăm un CalendarAdapter
pentru a îndeplini această sarcină. Este suficient de simplu încât poate fi o clasă membru în CalendarView
. Prin suprascrierea funcției getView()
, putem îndeplini cerințele de mai sus:
@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; }
Cerința finală de proiectare necesită ceva mai multă muncă. Mai întâi, să adăugăm culorile pentru cele patru sezoane în /res/values/colors.xml
:

<color name="summer">#44eebd82</color> <color name="fall">#44d8d27e</color> <color name="winter">#44a1c1da</color> <color name="spring">#448da64b</color>
Apoi, să folosim o matrice pentru a defini sezonul pentru fiecare lună (presupunând că emisfera nordică, pentru simplitate; scuze Australia!). În CalendarView
adăugăm următoarele variabile de membru:
// 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};
În acest fel, selectarea unei culori adecvate se face selectând sezonul corespunzător ( monthSeason[currentMonth]
) și apoi alegând culoarea corespunzătoare ( rainbow[monthSeason[currentMonth]
), aceasta este adăugată la updateCalendar()
pentru a vă asigura că este selectată culoarea corespunzătoare. ori de câte ori calendarul este schimbat.
// 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));
Cu asta, obținem următorul rezultat:
Notă importantă , datorită modului în care HashSet
compară obiectele, verificarea de mai sus eventDays.contains(date)
din updateCalendar()
nu va fi adevărată pentru obiectele date decât dacă acestea sunt exact identice. Nu efectuează verificări speciale pentru tipul de date Date
. Pentru a rezolva acest lucru, această verificare este înlocuită cu următorul cod:
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. Arată urât în timpul designului
Alegerea Android pentru substituenți în timpul proiectării poate fi discutabilă. Din fericire, Android instanțiază de fapt componenta noastră pentru a o reda în designerul UI și putem exploata acest lucru apelând updateCalendar()
în constructorul componentei. În acest fel, componenta va avea sens în timpul proiectării.
Dacă inițializarea componentei necesită o mulțime de procesare sau încarcă o mulțime de date, aceasta poate afecta performanța IDE-ului. În acest caz, Android oferă o funcție ingenioasă numită isInEditMode()
care poate fi utilizată pentru a limita cantitatea de date utilizate atunci când componenta este de fapt instanțiată în designerul UI. De exemplu, dacă există o mulțime de evenimente care trebuie încărcate în CalendarView
, putem folosi isInEditMode()
în cadrul updateCalendar()
pentru a furniza o listă de evenimente goală/limitată în modul de proiectare și, în caz contrar, o încărcăm pe cea reală.
6. Invocarea componentei
Componenta poate fi inclusă în fișierele de aspect XML (un exemplu de utilizare poate fi găsit în activity_main.xml
):
<samples.aalamir.customcalendar.CalendarView android: android:layout_width="match_parent" android:layout_height="wrap_content"/>
Și preluat pentru a fi interacționat odată ce aspectul este încărcat:
HashSet<Date> events = new HashSet<>(); events.add(new Date()); CalendarView cv = ((CalendarView)findViewById(R.id.calendar_view)); cv.updateCalendar(events);
Codul de mai sus creează un HashSet
de evenimente, îi adaugă ziua curentă, apoi îl trece la CalendarView
. Ca rezultat, CalendarView
va afișa ziua curentă în albastru aldine și va pune, de asemenea, marcatorul de eveniment pe ea:
CalendarView
afișează un eveniment
7. Adăugarea de atribute
O altă facilitate oferită de Android este de a atribui atribute unei componente personalizate. Acest lucru le permite dezvoltatorilor Android care folosesc componenta să selecteze setările prin formatul XML și să vadă rezultatul imediat în designerul UI, spre deosebire de a trebui să aștepte și să vadă cum arată CalendarView
în timpul rulării. Să adăugăm posibilitatea de a schimba afișarea formatului datei în componentă, de exemplu pentru a scrie numele complet al lunii în loc de abrevierea din trei litere.
Pentru a face acest lucru, sunt necesari următorii pași:
- Declarați atributul. Să-i numim
dateFormat
și să-i dăm tipul de datestring
. Adăugați-l la/res/values/attrs.xml
:
<resources> <declare-styleable name="CalendarDateElement"> <attr name="dateFormat" format="string"/> </declare-styleable> </resources>
- Utilizați atributul din aspectul care utilizează componenta și dați-i valoarea
"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"/>
- În cele din urmă, puneți componenta să folosească valoarea atributului:
TypedArray ta = getContext().obtainStyledAttributes(attrs, R.styleable.CalendarView); dateFormat = ta.getString(R.styleable.CalendarView_dateFormat);
] Construiți proiectul și veți observa modificările de dată afișate în designerul UI pentru a utiliza numele complet al lunii, cum ar fi „Iulie 2015”. Încercați să oferiți valori diferite și vedeți ce se întâmplă.
CalendarView
.
8. Interacțiunea cu componenta
Ați încercat să apăsați într-o anumită zi? Elementele interfeței de utilizare din componenta noastră încă se comportă în modul normal așteptat și vor declanșa evenimente ca răspuns la acțiunile utilizatorului. Deci, cum gestionăm aceste evenimente?
Răspunsul presupune două părți:
- Capturați evenimente în interiorul componentei și
- Raportați evenimente părintelui componentei (ar putea fi un
Fragment
, oActivity
sau chiar o altă componentă).
Prima parte este destul de simplă. De exemplu, pentru a gestiona elementele grilei apăsate lung, atribuim un ascultător corespunzător în clasa noastră de componente:
// 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; } });
Există mai multe metode de raportare a evenimentelor. Una directă și simplă este să copiați modul în care Android o face: oferă o interfață pentru evenimentele componentei care este implementată de părintele componentei ( eventHandler
în fragmentul de cod de mai sus).
Funcțiile interfeței pot fi transmise oricăror date care sunt relevante pentru aplicație. În cazul nostru, interfața trebuie să expună un handler de evenimente, căruia îi trece data pentru ziua apăsată. Următoarea interfață este definită în CalendarView
:
public interface EventHandler { void onDayLongPress(Date date); }
Implementarea furnizată de părinte poate fi furnizată în vizualizarea calendarului printr-un setEventHandler()
. Iată un exemplu de utilizare din „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(); } }); }
Apăsarea lungă a unei zile va declanșa un eveniment de apăsare lungă care este capturat și gestionat de GridView
și raportat prin apelarea onDayLongPress()
în implementarea furnizată, care, la rândul său, va afișa data zilei apăsate pe ecran:
Un alt mod, mai avansat, de a gestiona acest lucru este utilizarea Intents
și BroadcastReceivers
de la Android. Acest lucru este deosebit de util atunci când mai multe componente trebuie să fie notificate despre evenimentul calendarului. De exemplu, dacă apăsarea unei zile în calendar necesită afișarea unui text într-o Activity
și descărcarea unui fișier de către un Service
de fundal .
Utilizarea abordării anterioare va necesita ca Activity
să furnizeze un EventHandler
componentului, să gestioneze evenimentul și apoi să-l transmită Service
. În schimb, faptul că componenta difuzează o Intent
și atât Activity
, cât și Service
care o acceptă prin propriile lor BroadcastReceivers
nu numai că face viața mai ușoară, ci și ajută la decuplarea Activity
și a Service
în cauză.
Concluzie
Așadar, așa vă creați propria componentă personalizată în câțiva pași simpli:
- Creați aspectul XML și stilați-l pentru a se potrivi nevoilor dvs.
- Derivați clasa dvs. de componente din componenta părinte corespunzătoare, în funcție de aspectul dvs. XML.
- Adăugați logica de afaceri a componentei dvs.
- Utilizați atribute pentru a permite utilizatorilor să modifice comportamentul componentei.
- Pentru a facilita utilizarea componentei în designerul UI, utilizați funcția
isInEditMode()
de la Android.
În acest articol, am creat o vizualizare calendar ca exemplu, în principal pentru că vizualizarea calendar stoc este, în multe privințe, lipsă. Dar, nu sunteți în niciun fel limitat la ce fel de componente puteți crea. Puteți folosi aceeași tehnică pentru a crea orice aveți nevoie, cerul este limita!
Vă mulțumesc că ați citit acest ghid, vă doresc mult succes în eforturile voastre de codare!