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
JSONObject
aJSONArray
.
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
ListActivity
a 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
ArrayAdapter
si vyžaduje tieto parametre:- kontext aplikácie
- návrh/rozloženie jednej položky
- zoznam prvkov
Samotná implementácia bude vyzerať nasledovne:
<String> adapter = new ArrayAdapter<>(this, android.R.layout.simple_list_item_1, data); ArrayAdapter
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ý
ListView
prepojíme s adaptérom zavolaním metódysetAdapter()
nad inštanciou triedyListView
:ListView lv = (ListView) findViewById(R.id.listView); .setAdapter(adapter); lv
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/.ListActivity
Môžeme však upraviť našu aplikáciu tak, aby bol
ListActivity
prvou aktivitou, ktorá sa má spustiť. To zabezpečíme úpravou súboru s manifestom, v ktorom presunieme<intent-filter>
z aktivityDetailActivity
do aktivityListActivity
. Tým zabezpečíme spustenie aktivityListActivity
priamo 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:ImageView
weather_icon
- ikona reprezentujúca počasieTextView
day
- deňTextView
weather_description
- opis počasiaTextView
max_temp
- maximálna teplota dňaTextView
min_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ýTextView
bude používať na zobrazenie samotných textových info. Ak tak neurobíme (ako sme neurobili ani posledne), automaticky bude použitýTextView
s názvomlist_item_forecast_textview
, ktorý v našom rozložení nie je.Aktualizácia fragmentu kódu adaptéra v triede aktivity
ListActivity
bude teda vyzerať nasledovne:<String> adapter = new ArrayAdapter<>( ArrayAdapterthis, .layout.row, R.id.weather_description R 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
ListView
preto 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
View
zostavený 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) getContext().getSystemService(Context.LAYOUT_INFLATER_SERVICE); LayoutInflater inflater 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) { = response.body(); FiveDaysForecast data // set location name .i(TAG, "Forecast for " + data.city.name + " have been downloaded"); Log.setText(data.city.name); locationTv // create and setup ListView = new ForecastAdapter(getApplicationContext(), data.list); ForecastAdapter adapter .setAdapter(adapter); lv}
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 .tuke.smart.mrilko.models.List item = (sk.tuke.smart.mrilko.models.List) getItem(position); sk
Začneme zobrazením hodnoty weather description. Na jej zobrazenie potrebujeme z údajov o počasí získať objekt triedy
Weather
a 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) getContext().getSystemService(Context.LAYOUT_INFLATER_SERVICE); LayoutInflater inflater View view = inflater.inflate(R.layout.row, parent, false); // get data .tuke.smart.mrilko.models.List item = (sk.tuke.smart.mrilko.models.List) getItem(position); sk= item.weather.get(0); Weather weather // description = (TextView) view.findViewById(R.id.weather_description); tv .setText(weather.description); tv 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) getContext().getSystemService(Context.LAYOUT_INFLATER_SERVICE); LayoutInflater inflater View view = inflater.inflate(R.layout.row, parent, false); // get data .tuke.smart.mrilko.models.List item = (sk.tuke.smart.mrilko.models.List) getItem(position); sk= item.weather.get(0); Weather weather // 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()); .setImageResource(id); iv // date = (TextView) view.findViewById(R.id.day); TextView tv String day; if(position < 2) { // Today, Tomorrow = (String) DateUtils.getRelativeTimeSpanString(item.dt * 1000L, day System.currentTimeMillis(), // now .DAY_IN_MILLIS); DateUtils}else{ // Monday, Tuesday, ..., Sunday SimpleDateFormat dayFormat = new SimpleDateFormat("EEEE"); = dayFormat.format(item.dt * 1000L); day } .setText(day); tv // description = (TextView) view.findViewById(R.id.weather_description); tv .setText(weather.description); tv // temperature - max = (TextView)view.findViewById(R.id.temp_max); tv .setText(String.format("%.2f °C", item.temp.max)); tv // temperature - min = (TextView)view.findViewById(R.id.temp_min); tv .setText(String.format("%.2f °C", item.temp.min)); tv 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)