Week 10
ListView, row layout, custom adapter
Oznamy
- (slide Hackathon review
- (slide) Game Days Review
- (slide) Design Thinking
- (slide) Sprint #3
- (slide) Next Lecture
Mr. Iľko - The State of the Art
- Program na zisťovanie počasia na základe zadania.
- Pomocou explicitných zámerov vieme prechádzať medzi aktivitami a pomocou implicitných zámerov vieme spúšťať doplnkovú funkcionalitu.
- Údaje z externej služby sme sťahovali pomocou inštancie triedy
HttpURLConnection. - S výsledkom, ktorý sme dostali vo forme JSON objektu sme spracovali pomocou tried
JSONObjectaJSONArray.
5 Day Forecast
(slide) Aj keď naša appka začína byť použiteľná, nemôžeme zaspať na vavrínoch. Server openweathermap.org ponúka aj možnosť 5 dňovej predpovede počasia pre danú oblasť/mesto. Pokúsime sa ju teda integrovať aj do našej aplikácie.
Opäť sa bude jednať o formát JSON, aj keď aktuálne budú výsledky uložené v zozname.
ListView
(slide) Zoznam vytvoríme pomocou elementu
ListView(slide) údaje sú so zoznamom prepojené pomocou adaptéru. An adapter manages the data model and adapts it to the individual entries in the widget.
Začneme teda tým, že si vytvoríme novú aktivitu s názvom
ListActivitya umiestnime do nej element<ListView>.Keďže zatiaľ nemáme vyriešený prechod z jednej aktivity do druhej, môžeme na tento účel využiť Activity Manager.
Creating Fake Data
Pre naše experimentovanie si zatiaľ vystačíme s jednoduchým zoznamom položiek, ktoré budú reprezentovať predpoveď na najbližších 5 dní. Tento zoznam umiestnime do metódy
onCreate()novej aktivity a môže vyzerať napr. takto:String[] data = { "Today - Sunny - 15", "Tomorrow - Sunny - 16", "Thu - Rainy - 10", "Fri - Partially Cloudy - 13", "Sat - Sunny - 19" };
Adapter Initialization
V Android-e existuje niekoľko typov adaptérov, ktoré sú podtriedou triedy
Adapter. Dnes si vystačíme s triedouArrayAdapter, kde prvky budú práve v poli.(slide) Konštruktor triedy
ArrayAdaptersi vyžaduje tieto parametre:- kontext aplikácie
- návrh/rozloženie jednej položky
- zoznam prvkov
Samotná implementácia bude vyzerať nasledovne:
ArrayAdapter<String> adapter = new ArrayAdapter<>(this, android.R.layout.simple_list_item_1, data);Ako rozloženie položky použijeme preddefinované rozloženie s názvom
android.R.layout.simple_list_item_1. Tento typ je reprezentovaný jedným reťazcom. Ukážka takéhoto rozloženia sa nachádza na tomto slajde.
Prepojenie ListView-u s adaptérom
Vytvorený
ListViewprepojíme s adaptérom zavolaním metódysetAdapter()nad inštanciou triedyListView:ListView lv = (ListView) findViewById(R.id.listView); lv.setAdapter(adapter);
Overenie
Keďže nemáme zatiaľ žiadne prepojenie ani spôsob, ktorým v rámci aplikácie spustiť túto aktivitu, pomôžeme si opäť Activity Managerom z príkazového riadku
am start -n sk.tuke.smart.mrilko/.ListActivityMôžeme však upraviť našu aplikáciu tak, aby bol
ListActivityprvou aktivitou, ktorá sa má spustiť. To zabezpečíme úpravou súboru s manifestom, v ktorom presunieme<intent-filter>z aktivityDetailActivitydo aktivityListActivity. Tým zabezpečíme spustenie aktivityListActivitypriamo po spustení aplikácie:<activity android:name=".ListActivity"> <intent-filter> <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.LAUNCHER" /> </intent-filter> </activity>Po spustení aplikácie bude výsledná aktivita vyzerať nasledovne:
Custom Layout of List Item
Aktuálne rozloženie položky zoznamu nie je veľmi sexy. Pokúsime sa teda vytvoriť rozloženie, ktoré nebude reprezentované len jedným elementom typu
TextView, ale obohatíme ho ako o obrázok, tak aj rozdelíme jednotlivé jeho položky, ktoré zobrazíme osobitne. Výsledné rozloženie bude vyzerať tak, ako je zobrazené na tomto slajde.Spôsob definície rozloženia položky zoznamu sa nebude nijako líšiť od rozloženia samotnej aktivity. Opäť vytvoríme nový súbor s rozložením (layout), ktorý si pripravíme podľa obrazu nášho ;)
Vytvoríme teda nový súbor
row.xml, ktorý bude reprezentovať rozloženíe jednej položky zoznamu a bude obsahovať tieto elementy:ImageViewweather_icon- ikona reprezentujúca počasieTextViewday- deňTextViewweather_description- opis počasiaTextViewmax_temp- maximálna teplota dňaTextViewmin_temp- minimálna teplota dňa
Samotný kód rozloženia bude nasledovný:
#include "src/row.xml"(slide) Ak ho budeme chcieť použiť s našim
ArrayAdapter-om, tak musíme okrem nového rozloženia špecifikovať ešte aj to, ktorýTextViewbude používať na zobrazenie samotných textových info. Ak tak neurobíme (ako sme neurobili ani posledne), automaticky bude použitýTextViews názvomlist_item_forecast_textview, ktorý v našom rozložení nie je.Aktualizácia fragmentu kódu adaptéra v triede aktivity
ListActivitybude teda vyzerať nasledovne:ArrayAdapter<String> adapter = new ArrayAdapter<>( this, R.layout.row, R.id.weather_description data );Po spustení aplikácie bude výsledná aktivita vyzerať nasledovne:
Týmto však zabezpečíme len to, že využijeme len jednu časť celého rozloženia položky zoznamu. Ako však zabezpečíme aj zobrazenie ikony počasia? Ako oddelíme jednotlivé položky počasia a zobrazíme ich osobitne a každú naštýlujeme ináč? Aby sme toto správanie docielili, budeme si musieť vytvoriť vlastný adaptér.
Custom adapter
ForecastAdapter extends ArrayAdapter
Ako som spomínal, všetky adaptéry sú vlastne potomkami triedy
Adapter. Miesto vytvorenia priameho potomka však vytvoríme len potomka triedyArrayAdapter, aby sme využili už pripravenú funkcionalitu.Vytvoríme teda triedu
ForecastAdapter, ktorá bude potomkom triedyArrayAdapter. Upravíme však jej konštruktor tak, že okrem kontextu aplikácie jej odovzdávame iba zoznam dát pre päťdňovú predpoveď. V konštruktore zavoláme konštruktor predka, ktorému okrem kontextu a údajov odovzdáme aj identifikátor rozloženia položky zoznamu (slide):public class ForecastAdapter extends ArrayAdapter { public ForecastAdapter(Context context, List data) { super(context, R.layout.row, data); } }
getView() method
(slide) Adaptér potrebuje vytvoriť vzhľad, resp. rozloženie pre každý riadok zoznamu. Inštancia triedy
ListViewpreto volá nad adaptérom metódugetView()pre každý prvok dát. V tejto metóde adaptér vytvára rozloženie riadku a mapuje údaje do pohľadov v tomto rozložení.Samotný pohľad, ktorý bude reprezentovať jeden riadok, je opäť len inštanciou triedy
View.Parametrami tejto metódy sú:
position- The position of the item within the adapter’s data set of the item whose view we want.convertView- The old view to reuse, if possible (not null).parent- The parent that this view will eventually be attached to.
Účelom metódy je teda vytvoriť objekt typu
View, ktorý bude reprezentovať príslušný riadok zoznamu. V našom prípade to teda bude pohľad zostavený z XML návrhurow.xml.Aby sme teda mohli získať objekt typu
Viewzostavený z XML súboru definujúcom rozloženie riadku zoznamu, využijeme trieduLayoutInflater, ktorá to urobí za nás:@Override public View getView(int position, View convertView, ViewGroup parent) { LayoutInflater inflater = (LayoutInflater) getContext().getSystemService(Context.LAYOUT_INFLATER_SERVICE); View view = inflater.inflate(R.layout.row, parent, false); return view; }Aktuálna podoba adaptéru je spustiteľná, aj keď ešte nie úplná. Metóda
getView()bude zatiaľ vracať len prázdne položky - pre každý deň jednu. Aktualizujeme teda metóduonResponse(), kde vytvoríme príslušný adaptér z údajov získaných pomocou knižnice Retrofit a následne tento adaptér priradímeListView-u. Ak aplikáciu spustíme, zobrazí sa 5 prázdnych položiek predpovede. MetódaonResponse()bude po úprave vyzerať nasledovne:// bind ListView first @BindView(R.id.list_view) ListView lv; @Override public void onResponse(Call<FiveDaysForecast> call, Response<FiveDaysForecast> response) { FiveDaysForecast data = response.body(); // set location name Log.i(TAG, "Forecast for " + data.city.name + " have been downloaded"); locationTv.setText(data.city.name); // create and setup ListView ForecastAdapter adapter = new ForecastAdapter(getApplicationContext(), data.list); lv.setAdapter(adapter); }Výsledná aktivita s prázdnymi piatimi položkami bude vyzerať nasledovne:
Naplnenie položky zoznamu údajmi o predpovedi
Posledné, čo nám zostáva urobiť, je naplniť pohľad jednej položky zoznamu údajmi s predpoveďou pre daný deň. Aktualizujeme teda metódu
getView()nášho adaptéraForecastAdapter.Začneme tým, že zo zoznamu všetkých položiek vyberieme n-tú položku, kde jej poradové číslo je dané parametrom metódy
position:// get data sk.tuke.smart.mrilko.models.List item = (sk.tuke.smart.mrilko.models.List) getItem(position);Začneme zobrazením hodnoty weather description. Na jej zobrazenie potrebujeme z údajov o počasí získať objekt triedy
Weathera samozrejmeTextView, do ktorého informáciu o počasí zapíšeme. Kód teda aktualizujeme nasledovne:@Override public View getView(int position, View convertView, ViewGroup parent) { LayoutInflater inflater = (LayoutInflater) getContext().getSystemService(Context.LAYOUT_INFLATER_SERVICE); View view = inflater.inflate(R.layout.row, parent, false); // get data sk.tuke.smart.mrilko.models.List item = (sk.tuke.smart.mrilko.models.List) getItem(position); Weather weather = item.weather.get(0); // description tv = (TextView) view.findViewById(R.id.weather_description); tv.setText(weather.description); return view; }Pri získavaní ostatných údajov ako aj ich zapisovaní budeme postupovať podobne. Výsledná podoba metódy
getView()bude vyzerať nasledovne:@Override public View getView(int position, View convertView, ViewGroup parent) { LayoutInflater inflater = (LayoutInflater) getContext().getSystemService(Context.LAYOUT_INFLATER_SERVICE); View view = inflater.inflate(R.layout.row, parent, false); // get data sk.tuke.smart.mrilko.models.List item = (sk.tuke.smart.mrilko.models.List) getItem(position); Weather weather = item.weather.get(0); // icon String main = weather.main.toLowerCase().replace(" ", "_"); ImageView iv = (ImageView) view.findViewById(R.id.weather_icon); int id = getContext() .getResources() .getIdentifier("art_" + main, "drawable", getContext().getPackageName()); iv.setImageResource(id); // date TextView tv = (TextView) view.findViewById(R.id.day); String day; if(position < 2) { // Today, Tomorrow day = (String) DateUtils.getRelativeTimeSpanString(item.dt * 1000L, System.currentTimeMillis(), // now DateUtils.DAY_IN_MILLIS); }else{ // Monday, Tuesday, ..., Sunday SimpleDateFormat dayFormat = new SimpleDateFormat("EEEE"); day = dayFormat.format(item.dt * 1000L); } tv.setText(day); // description tv = (TextView) view.findViewById(R.id.weather_description); tv.setText(weather.description); // temperature - max tv = (TextView)view.findViewById(R.id.temp_max); tv.setText(String.format("%.2f °C", item.temp.max)); // temperature - min tv = (TextView)view.findViewById(R.id.temp_min); tv.setText(String.format("%.2f °C", item.temp.min)); return view; }Po spustení máme k dispozícii prehľad počasia na nasledujúcich 5 dní.
Conclusion
- Aplikácia začína byť použiteľná. Má však stále jeden obrovský hendikep a síce - údaje potrebujeme stiahnuť zakaždým, keď sa aplikácia spustí. Nabudúce sa pozrieme na to, ako zabezpečiť trvalé uchovávanie dát pomocou SQL databázy a ošetríme tiež kliknutie na položku zoznamu.
Conclusion
Additional Resources
- slajdy z prednášky
- Android BroadcastReceiver - Tutorial - Vogella Tutorial
- BroadcastReceiver
- ListView
- Using lists in Android (ListView) - Tutorial - Vogella Tutorial
- Building Layouts with an Adapter
- Using activity manager (am)
- Temps - A simple but smart weather app. (odkukaný layout pre Mr Iľka)