Week 10

ListView, row layout, custom adapter

Oznamy

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 a JSONArray.

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 triedou ArrayAdapter, 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:

    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ý ListView prepojíme s adaptérom zavolaním metódy setAdapter() nad inštanciou triedy ListView:

    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/.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 aktivity DetailActivity do aktivity ListActivity. Tým zabezpečíme spustenie aktivity ListActivity 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:

    ListView

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časie
    • TextView day - deň
    • TextView weather_description - opis počasia
    • TextView max_temp - maximálna teplota dňa
    • TextView 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ázvom list_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:

    ArrayAdapter<String> adapter = new ArrayAdapter<>(
        this,
        R.layout.row,
        R.id.weather_description
        data
    );
  • Po spustení aplikácie bude výsledná aktivita vyzerať nasledovne:

    ListView s vlastným rozložením riadku
  • 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 triedy ArrayAdapter, aby sme využili už pripravenú funkcionalitu.

  • Vytvoríme teda triedu ForecastAdapter, ktorá bude potomkom triedy ArrayAdapter. 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ódu getView() 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ávrhu row.xml.

  • Aby sme teda mohli získať objekt typu View zostavený z XML súboru definujúcom rozloženie riadku zoznamu, využijeme triedu LayoutInflater, 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ódu onResponse(), kde vytvoríme príslušný adaptér z údajov získaných pomocou knižnice Retrofit a následne tento adaptér priradíme ListView-u. Ak aplikáciu spustíme, zobrazí sa 5 prázdnych položiek predpovede. Metóda onResponse() 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:

    Prázdna predpoveď

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éra ForecastAdapter.

  • 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 Weather a samozrejme TextView, 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í.

    Päťdňová predpoveď

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