Det er ikke uvanlig at utviklere finner seg selv i behov av en LØK komponent som enten ikke er levert av plattformen de målretter mot, eller faktisk leveres, men som mangler en viss egenskap eller atferd. Svaret på begge scenariene er en tilpasset brukergrensesnittkomponent.
Android UI-modellen er iboende tilpassbar, og tilbyr verktøyene for Android-tilpasning , testing og evnen til å skape tilpassede UI-komponenter på forskjellige måter:
Arv en eksisterende komponent (dvs. TextView
, ImageView
osv.), og legg til / overstyr nødvendig funksjonalitet. For eksempel en CircleImageView
som arver ImageView
, overstyrer onDraw()
funksjon for å begrense det viste bildet til en sirkel, og legge til en loadFromFile()
funksjon for å laste et bilde fra eksternt minne.
Lag en sammensatt komponent ut av flere komponenter. Denne tilnærmingen utnytter vanligvis Oppsett for å kontrollere hvordan komponentene er ordnet på skjermen. For eksempel en LabeledEditText
som arver LinearLayout
med horisontal orientering, og inneholder både a TextView
fungerer som en etikett og en EditText
fungerer som et tekstinntastingsfelt.
Denne tilnærmingen kan også bruke den forrige, dvs. de interne komponentene kan være innfødte eller tilpassede.
Den mest allsidige og mest komplekse tilnærmingen er å lage en selvtegnet komponent . I dette tilfellet vil komponenten arve det generiske View
klasse og overstyre funksjoner som onMeasure()
for å bestemme utformingen, onDraw()
for å vise innholdet osv. Komponenter som er opprettet på denne måten, avhenger vanligvis sterkt av Android-enheter 2D tegning API .
CalendarView
Android gir en innfødt CalendarView
komponent . Det fungerer bra og gir den minste funksjonaliteten som forventes fra alle kalenderkomponenter, viser en hel måned og fremhever dagens dag. Noen vil kanskje si at det ser bra ut også, men bare hvis du skal ha et innfødt utseende, og ikke har noen interesse i å tilpasse hvordan det ser ut overhodet.
For eksempel CalendarView
komponenten gir ingen måte å endre hvordan en bestemt dag er merket, eller hvilken bakgrunnsfarge du skal bruke. Det er heller ingen måte å legge til tilpasset tekst eller grafikk, for å markere en spesiell anledning, for eksempel. Kort sagt, komponenten ser slik ut, og nesten ingenting kan endres:
CalendarView
i AppCompact.Light
tema.
Så hvordan går det an å lage sin egen kalendervisning? Noen av tilnærmingene ovenfor vil fungere. Imidlertid vil praktisk bruk utelukke det tredje alternativet (2D-grafikk) og gi oss de to andre metodene, og vi vil bruke en blanding av begge i denne artikkelen.
For å følge med kan du finne kildekoden her .
La oss først begynne med hvordan komponenten ser ut. For å gjøre det enkelt, la oss vise dager i et rutenett, og øverst navnet på måneden sammen med knappene 'neste måned' og 'forrige måned'.
Egendefinert kalendervisning.
Dette oppsettet er definert i filen control_calendar.xml
, som følger. Merk at noen repeterende markeringer er forkortet med ...
:
... Repeat for MON - SAT.
Den forrige layouten kan inkluderes som den er i en Activity
eller a Fragment
og det vil fungere bra. Men innkapsling av den som en frittstående brukergrensesnittkomponent vil forhindre repetisjon av kode og muliggjøre et modulært design, der hver modul håndterer ett ansvar.
UI-komponenten vår vil være en LinearLayout
, for å matche roten til XML-oppsettfilen. Merk at bare de viktige delene vises fra koden. Implementeringen av komponenten ligger i 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); } }
Koden er ganske grei. Ved opprettelsen blåser komponenten opp XML-oppsettet, og når det er gjort, tildeler den interne kontroller til lokale variabler for lettere tilgang senere.
For å få denne komponenten til å oppføre seg som en kalendervisning, er noe forretningslogikk i orden. Det kan virke komplisert i begynnelsen, men det er egentlig ikke mye. La oss bryte det ned:
Kalendervisningen er syv dager bred, og det er garantert at alle månedene starter et sted på første rad.
Først må vi finne ut hvilken posisjon måneden starter på, og deretter fylle alle posisjonene før det med tallene fra forrige måned (30, 29, 28 .. osv.) Til vi når posisjon 0.
Deretter fyller vi ut dagene for inneværende måned (1, 2, 3 ... etc).
Etter det kommer dagene for neste måned (igjen, 1, 2, 3 .. osv.), Men denne gangen fyller vi bare de gjenværende posisjonene i den siste raden (e) i rutenettet.
Følgende diagram illustrerer disse trinnene:
Tilpasset kalendervisningslogikk.
Bredden på rutenettet er allerede spesifisert til å være syv celler, som angir en ukentlig kalender, men hva med høyden? Den største størrelsen for rutenettet kan bestemmes av det verste tilfellet av en 31-dagers måned som starter på en lørdag, som er den siste cellen i første rad, og vil trenge 5 rader til for å vises i sin helhet. Så å sette kalenderen til å vise seks rader (totalt 42 dager) vil være tilstrekkelig til å håndtere alle saker.
Men ikke alle månedene har 31 dager! Vi kan unngå komplikasjoner som oppstår ved å bruke Android's innebygde datofunksjonalitet, og unngå behovet for å finne ut antall dager selv.
Som nevnt tidligere, datofunksjonalitetene gitt av Calendar
klasse gjør implementeringen ganske grei. I komponenten vår er updateCalendar()
funksjon implementerer denne logikken:
private void updateCalendar() { ArrayList 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() 4. Kan tilpasses i hjertet
Siden komponenten som er ansvarlig for visning av individuelle dager, er GridView
, er et bra sted å tilpasse hvordan dager vises Adapter
, siden den er ansvarlig for å holde dataene og blåse visningene for individuelle rutenettceller.
For dette eksemplet vil vi kreve følgende fra CalendearView
:
- Nåværende dag skal være ifet blå tekst.
- Dager utenfor nåværende måned bør værenedtonet.
- Dager med et arrangement skal vise et spesielt ikon.
- Kalenderoverskriften skal endre farger avhengig av sesong (sommer, høst, vinter, vår).
De tre første kravene er enkle å oppnå ved å endre tekstattributter og bakgrunnsressurser. La oss implementere et CalendarAdapter
å utføre denne oppgaven. Det er enkelt nok at det kan være en medlemsklasse i CalendarView
. Ved å overstyre getView()
funksjon, kan vi oppnå kravene ovenfor:
@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; }
Det endelige designkravet krever litt mer arbeid. La oss først legge til fargene for de fire sesongene i /res/values/colors.xml
:
#44eebd82 #44d8d27e #44a1c1da #448da64b
La oss deretter bruke en matrise for å definere sesongen for hver måned (forutsatt at den er nordlige halvkule, for enkelhetens skyld, beklager Australia!). I CalendarView
vi legger til følgende medlemsvariabler:
// 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};
På denne måten gjøres det å velge en passende farge ved å velge riktig sesong (monthSeason[currentMonth]
) og deretter velge den tilsvarende fargen (rainbow[monthSeason[currentMonth]
), og dette legges til updateCalendar()
for å sikre at riktig farge er valgt når kalenderen endres.
// 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));
Med det får vi følgende resultat:
Topptekstfarge endres etter sesong.
Viktig notat på grunn av veien HashSet
sammenligner objekter, sjekket ovenfor eventDays.contains(date)
i updateCalendar()
vil ikke gi sant for datoobjekter med mindre de er nøyaktig identiske. Den utfører ingen spesielle kontroller for Date
data-type. For å omgå dette erstattes denne sjekken med følgende kode:
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. Det ser stygt ut i designtiden
Androids valg for plassholdere i designtid kan være tvilsomt. Heldigvis Android oppretter faktisk komponenten vår for å gjengi den i UI-designeren, og vi kan utnytte dette ved å ringe updateCalendar()
i komponentkonstruktøren. På denne måten vil komponenten faktisk være fornuftig i designtiden.

Hvis initialisering av komponenten krever mye behandling eller laster inn masse data, kan det påvirke ytelsen til IDE. I dette tilfellet gir Android en fin funksjon kalt isInEditMode()
som kan brukes til å begrense datamengden som brukes når komponenten faktisk instantieres i UI-designeren. For eksempel, hvis det er mange hendelser som skal lastes inn i CalendarView
, kan vi bruke isInEditMode()
inne i updateCalendar()
funksjon for å gi en tom / begrenset hendelsesliste i designmodus, og last inn den virkelige ellers.
6. Påkalle komponenten
Komponenten kan inkluderes i XML-oppsettfiler (et eksempel på bruk finner du i activity_main.xml
):
HashSet events = new HashSet(); events.add(new Date()); CalendarView cv = ((CalendarView)findViewById(R.id.calendar_view)); cv.updateCalendar(events);
Og hentet for å samhandle med når oppsettet er lastet:
HashSet
Ovennevnte kode skaper et CalendarView
av hendelser, legger den gjeldende dagen til den, og sender den til CalendarView
. Som et resultat, CalendarView
vil vise gjeldende dag med fet skrift og også sette hendelsesmarkøren på den:
CalendarView
viser en hendelse
7. Legge til attributter
Et annet anlegg fra Android er å tildele attributter til en tilpasset komponent. Dette tillater Android-utviklere ved å bruke komponenten til å velge innstillinger via XML-oppsettet og se resultatet umiddelbart i UI-designeren, i motsetning til å måtte vente og se hvordan dateFormat
ser ut som i kjøretid. La oss legge til muligheten til å endre datoformatvisningen i komponenten, for eksempel for å stave ut hele navnet på måneden i stedet for forkortelsen på tre bokstaver.
For å gjøre dette er følgende trinn nødvendig:
- Erklær attributtet. La oss kalle det
string
og gi den /res/values/attrs.xml
data-type. Legg den til 'MMMM yyyy'
:
TypedArray ta = getContext().obtainStyledAttributes(attrs, R.styleable.CalendarView); dateFormat = ta.getString(R.styleable.CalendarView_dateFormat);
- Bruk attributtet i layoutet som bruker komponenten, og gi den verdien
CalendarView
:
Fragment
- Til slutt, la komponenten bruke attributtverdien:
Activity
] Bygg prosjektet, og du vil legge merke til de viste datoendringene i UI-designer å bruke det fulle navnet på måneden, som “juli 2015”. Prøv å oppgi forskjellige verdier og se hva som skjer.
Endrer // 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; } });
attributter.
8. Samhandle med komponenten
Har du prøvd å trykke på en bestemt dag? De indre UI-elementene i komponenten vår oppfører seg fortsatt på den vanlige forventede måten og vil skyte hendelser som svar på brukerhandlinger. Så hvordan håndterer vi disse hendelsene?
Svaret består av to deler:
- Fang hendelser inne i komponenten, og
- Rapporter hendelser til komponentens overordnede (kan være en
eventHandler
, en CalendarView
eller til og med en annen komponent).
Den første delen er ganske grei. For eksempel, for å håndtere nettpresser med lang trykk, tildeler vi en tilsvarende lytter i komponentklassen vår:
public interface EventHandler { void onDayLongPress(Date date); }
Det er flere metoder for rapportering av hendelser. En direkte og enkel er å kopiere slik Android gjør det: det gir et grensesnitt til komponentens hendelser som er implementert av komponentens overordnede (setEventHandler()
i kodebiten ovenfor).
Grensesnittets funksjoner kan overføres alle data som er relevante for applikasjonen. I vårt tilfelle må grensesnittet eksponere en hendelsesbehandler, som passerer datoen for den pressede dagen. Følgende grensesnitt er definert i @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); HashSet 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(); } }); }
:
GridView
Implementeringen fra foreldrene kan leveres til kalendervisningen via onDayLongPress()
. Her er eksempler på bruk fra `MainActivity.java ':
Intents
Langt trykk på en dag vil avfyre en langpress-hendelse som blir fanget opp og håndtert av BroadcastReceivers
og rapportert ved å ringe Activity
i den medfølgende implementeringen, som igjen viser datoen for den pressede dagen på skjermen:

En annen, mer avansert måte å håndtere dette på er å bruke Android’s Service
og Activity
. Dette er spesielt nyttig når flere komponenter må varsles om kalenderens begivenhet. For eksempel hvis det å trykke på en dag i kalenderen krever at en tekst vises i et EventHandler
og en fil som skal lastes ned med bakgrunn Service
.
Å bruke den forrige tilnærmingen vil kreve Intent
å gi en Activity
til komponenten, håndtere hendelsen og deretter overføre den til Service
. I stedet får komponenten en BroadcastReceivers
og både Activity
og Service
aksepterer det via sine egne isInEditMode()
ikke bare gjør livet lettere, men hjelper også med å koble
|_+_|
og |_+_|
i spørsmålet. Konklusjon
Se den utrolige kraften til Android-tilpasning! kvitring Så dette er hvordan du lager din egen tilpassede komponent i noen få enkle trinn:
- Lag XML-oppsettet og stil det etter dine behov.
- Utled komponentklassen din fra den aktuelle overordnede komponenten, i henhold til XML-oppsettet.
- Legg til komponentens forretningslogikk.
- Bruk attributter for å gjøre det mulig for brukere å endre komponentens atferd.
- For å gjøre det enklere å bruke komponenten i UI-designeren, bruk Android’s
|_+_|
funksjon.
I denne artikkelen opprettet vi en kalendervisning som et eksempel, hovedsakelig fordi aksjekalendervisningen på mange måter mangler. Men du er på ingen måte begrenset til hva slags komponenter du kan lage. Du kan bruke den samme teknikken til å lage alt du trenger, himmelen er grensen!
Takk for at du leser denne guiden. Jeg ønsker deg lykke til i kodingen din!