Prstom posúvame obsah

ViewPager: prstom posúvame obsah

Nejedna aplikácia spočíva z dvoch aktívit: v jednej zobrazujeme zoznam mailov, úloh, súborov, a po kliknutí na položku prejdeme do druhej aktivity, kde zobrazíme všetky podrobnosti.

Lenže ak nás naraz zaujíma viacero položiek (napr. chceme čítať viacero nových mailov), budeme sa preklikávať zo zoznamu do detailu, potom naspäť do zoznamu, kde vyberieme novú položku, jedným slovom hore-dole ako na pružine.

Vďaka view pageru môžeme zobraziť podrobnosti o jednej položke a ťahaním prsta ju vieme odsunúť mimo displej tak, aby uvoľnila miesto nasledovnej položke. Máme teda niečo ako filmový pás položiek, ktorý vieme prstom posúvať v oboch smerom.

Screenshot aplikácie

Ako vidno, pager je skvelý pre prípady, kde by ste sa tradične posúvali medzi položkami pomocou tlačidiel Ďalší a Predošlý

Ukážme si príklad použitia!

Prehliadanie krajov

V ukážke o kartách sme vytvorili aplikáciu zobrazujúcu informácie o krajoch. Predveďme si to ešte raz, s použitím view pagera. Založme si teda nový projekt s jednou prázdnou aktivitou RegionBrowserActivity.

Použitie ViewPagera

Ak chceme zaviesť tento widget do aplikácie, budeme potrebovať:

  1. definovať layout v XML obsahujúci widget
  2. v kóde vytvoriť adaptér s dátami zobrazovaný v jednotlivých položkách
  3. oboznámiť sa s adaptérom FragmentStatePagerAdapter, ktorý zobrazí položku reprezentovanú fragmentom
  4. vytvoriť fragment s widgetmi jednej položky

Neskôr dodáme ešte jeden krok:

  1. vytvoríme vizuálny indikátor pozície položky v prechádzanom zozname, čím zorientujeme používateľa.

Celá architektúra

Architektúra

Widget stránkovača ViewPager preberá dáta do stránok skrz adaptér PageAdapter, kde máme na výber z dvoch implementácií (povieme o nich viac neskôr), ktoré napĺňajú obsahy stránok z fragmentov.

Voliteľnou súčasťou sú navigačné prudy (pager strips), kde máme opäť na výber dva typy: statický a interaktívny, pričom si ukážeme použitie oboch.

Definícia layoutu

Vyrobme layout aktivity, ktorá obsiahne widget pre view pager. Do súboru activity_region_browser.xml dodajme obsah:

<android.support.v4.view.ViewPager xmlns:android="https://schemas.android.com/apk/res/android"
    android:id="@+id/regionViewPager"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent">

    <android.support.v4.view.PagerTabStrip
        android:id="@+id/pager_title_strip"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_gravity="top"
        android:background="#33b5e5"
        android:textColor="#fff"
        android:paddingTop="4dp"
        android:paddingBottom="4dp" />

</android.support.v4.view.ViewPager>

Jadro tvorí widget android.support.v4.view.ViewPager, ktorý vyhlásime za koreňový element layoutu. Žiadny špeciálny layout (napr. RelativeLayout či LinearLayout) nám netreba, pretože pager sa sám postará o správu komponentov.

Adaptéry a dáta

V kóde aktivity potrebujeme vyhľadať v layoute inštanciu widgetu typu ViewPager, a priradiť mu adaptér, ktorý bude reprezentovať dáta pre jednotlivé položky, medzi ktorými sa bude používateľ hýbať.

Odkiaľ získame dáta? Jednoducho: zoznam krajov Slovenska definujeme v reťazcovom resource podobným spôsobom, ako sme to spravili pri kartách. V súbore strings.xml uvedieme pole reťazcov:

<string-array name="regionNames">
    <item>Bratislavský</item>
    ...
    <item>Košický</item>
</string-array>

A ako nastavíme adaptér? Ako je známe, každý widget má obvykle svoj vlastný typ adaptéra. V prípade ViewPagera sú dáta reprezentované adaptérom typu PagerAdapter. Táto trieda sa však nevytvára priamo, ale zvyčajne sa využije jeden z dvoch vzorov pripravených v platforme. Android tak ponúka:

  • FragmentPagerAdapter je jednoduchý adaptér, ktorý fragmenty a ich stav udržuje v pamäti. Je jednoduchý pre prípady, keď je fragmentov málo (cca 4) a ich počet sa nemení. Fragment, ktorý zodpovedá práve zobrazovanej položke, sa napojí na aktuálnu aktivitu (cez metódu attach() na transakcii), a ostatné, nezobrazované fragmenty, sú odpájané cez detach().
  • FragmentStatePagerAdapter. Fragmenty vytvára a ničí dynamicky, podľa potreby. Mení sa počet fragmentov? Máte ich veľa? S týmto adaptérom ušetríte.

FragmentStatePagerAdapter

V aplikácii implementujeme adaptér cez pamäťovo efektívnejšiu predlohu FragmentStatePagerAdapter. Potrebujeme implementovať dve metódy:

  • getCount() vráti počet fragmentov, teda počet položiek v zobrazovanej kolekcii
  • getItem() vráti konkrétnu inštanciu fragmentu na danej pozícii.

Kostra bude vyzerať nasledovne:

public class RegionBrowserActivity extends ActionBarActivity {

    private ViewPager viewPager;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_region_browser);

        final String[] regionNames = getResources().getStringArray(R.array.regionNames);
        this.viewPager = (ViewPager) findViewById(R.id.regionViewPager);
        this.viewPager.setAdapter(new FragmentStatePagerAdapter(getSupportFragmentManager()) {
            @Override
            public Fragment getItem(int i) {
                return ________________________;
            }

            @Override
            public int getCount() {
                return regionNames.length;
            }
        });
    }
}

Fragment pre konkrétnu položku

Každá zobrazovaná položka v kolekcii, ktorú zobrazujeme s využitím adaptéra na základe fragmentov, potrebuje, áno, fragment. Vytvorme teda fragment i jeho layout.

Layout fragmentu bude jednoduchý:

<?xml version="1.0" encoding="utf-8"?>
<TextView
    xmlns:android="https://schemas.android.com/apk/res/android" 
    android:id="@+id/regionDetailTextView"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:gravity="center"
    android:textSize="50sp"
    />

Kód fragmentu nebude ničím mimoriadnym: azda len „konštruktorom“, ktorý vezme reťazec zobrazený v obrovskom textovom políčku. Na prenos reťazca využijeme argumenty v podobe objektu typu Bundle.

Widget ViewPager pochádza z knižnice kompatibility a preto i fragmenty musia dediť od triedy z tejto knižnice, čiže od triedy typu android.support.v4.app.Fragment.

Výsledný zdrojový kód teda vyzerá:

public class RegionDetailFragment extends android.support.v4.app.Fragment {
    public static final String ARG_REGION_NAME = "RegionName";
    private TextView regionDetailTextView;

    public static RegionDetailFragment newInstance(String regionName) {
        Bundle args = new Bundle();
        args.putString(ARG_REGION_NAME, regionName);

        RegionDetailFragment regionDetailFragment = new RegionDetailFragment();
        regionDetailFragment.setArguments(args);

        return regionDetailFragment;
    }

    @Nullable
    @Override
    public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
        return inflater.inflate(R.layout.fragment_region_detail, container, false);
    }

    @Override
    public void onViewCreated(View view, @Nullable Bundle savedInstanceState) {
        super.onViewCreated(view, savedInstanceState);

        this.regionDetailTextView = (TextView) view.findViewById(R.id.regionDetailTextView);
        Bundle args = getArguments();
        if(args != null && args.containsKey(ARG_REGION_NAME)) {
            this.regionDetailTextView.setText(args.getString(ARG_REGION_NAME));
        }
    }
}

Ak je fragment hotový, môžeme ho využiť v adaptéri!

Prepojenie fragmentu a adaptéra

Metóda getItem() v adaptéri musí vrátiť novú inštanciu fragmentu, ktorý obsiahne dáta práve zobrazenej položky.

@Override
public Fragment getItem(int i) {
    return RegionDetailFragment.newInstance(regionNames[i]);
}

Ako vidno, využili sme pseudokonštruktor, do ktorého uvedieme reťazec s názvom kraja, ktorý vytiahneme z poľa podľa indexu aktuálne zobrazenej položky z kolekcie krajov.

Spustenie aplikácie

Už teraz môžeme spustiť aplikáciu a kochať sa, ako ťahaním prsta posúvame orloj s veľkými názvami krajov.

Screenshot aplikácie

Vizuálna indikácia polohy

Ak máte príliš veľa položiek, ľahko sa v nich stratíte. Najmä keď je fragment tvorený z viacerých komponentov. Našťastie, existujú rovno dva spôsoby, ktorými môžete naznačiť používateľovi, ktorá položka je vľavo či vpravo od práve zobrazovaného fragmentu.

  • PagerTitleStrip je neinteraktívny úzky pruh, kde sa v strede zobrazuje názov aktuálneho fragmentu a vľavo i vpravo sú titulky susediacich fragmentov.

PagerTitleStrip

  • PagerTabStrip je interaktívny pruh, kde používateľ nielen vidí, ale môže i klikať na titulky susediacich fragmentov.

PagerTabStrip

Pre použitie stačí vložiť deklaráciu príslušného komponentu do layoutu, pod element <android.support.v4.view.ViewPager>.

<android.support.v4.view.PagerTitleStrip
    android:id="@+id/pager_title_strip"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:layout_gravity="top"
    android:background="#33b5e5"
    android:textColor="#fff"
    android:paddingTop="4dp"
    android:paddingBottom="4dp" />

Dôležité je nastaviť šírku (na celý displej), výšku (primeranú výške položiek) a uviesť pozíciu pruhu pomocou layout_gravity: buď tradične hore (top) alebo dole (bottom). Ostatné nastavenia sú estetické: farba pozadia, textu, či priestor medzi textom a horným či dolným okrajom.

Na to, aby to naozaj fungovalo, je nutné implementovať v adaptéri ešte jednu metódu, ktorá vráti titulok fragmentu zobrazený v pruhu:

this.viewPager.setAdapter(new FragmentStatePagerAdapter(getSupportFragmentManager()) {
    ...

    @Override
    public CharSequence getPageTitle(int position) {
        return regionNames[position];
    }
});

Vyskúšajte si meniť typy pruhov, je to zábava! (A vôbec nemusíte meniť Java kód.)

Karty a view pagers

View pagers nemusia zobrazovať aktuálne pozície len cez pruhy Pager___Strip. Druhou možnosťou je prepojiť ich s kartami na lište akcií!

Komunikácia medzi lištou akcií a stránkovačom pobeží dvoma drôtmi:

  • v jednom bude poslucháč prijímať informácie o zmene výberu karty, na čo zareaguje zmenou aktuálne zobrazeného fragmentu v stránkovači. Poslucháč je typu ActionBar.TabListener.
  • v druhom bude poslucháč sledovať ťahanie prstom nad stránkovačom a pri zmene výberu sa prestaví aktuálne vybratá karta. Poslucháč je typu ViewPager.OnPageChangeListener.

Komunikácia medzi kartami a stránkovačmi

Úlohu oboch poslucháčov vie zastúpiť aktivita implementujúca oba interfejsy.

public class RegionBrowserActivity extends ActionBarActivity 
    implements ActionBar.TabListener, 
               ViewPager.OnPageChangeListener {

}

Následne musí implementovať šesť metód: s prefixom onTab____ patria poslucháčovi zmien výberu kariet, a onPage____ patria poslucháčovi zmien stránkovača.

Metódy výberu kariet

Metódy kariet sú známe z minulých dielov, ale u nás stačí implementovať onTabSelected(), kde nastavíme aktuálne vybratú položku v stránkovači na základe pozície karty v lište aktivít:

@Override
public void onTabSelected(ActionBar.Tab tab, FragmentTransaction fragmentTransaction) {
    this.viewPager.setCurrentItem(tab.getPosition());
}

Ostatné dve metódy, onTabUnselected() a onTabReselected() nechajme prázdne.

Metódy výberu v stránkovači

Stránkovač poskytuje tiež tri metódy: onPageSelected() sa zavolá pri zmene aktuálne zobrazenej položky v stránkovači, a práve tam vyvoláme zmenu aktuálne vybratej karty v lište akcií:

@Override
public void onPageSelected(int pageIndex) {
    getSupportActionBar().setSelectedNavigationItem(pageIndex);
}

Ostatné dve metódy slúžia na podrobné zisťovanie stavu zámen položiek v stránkovači: onPageScrolled() počúva na zmeny v posune karty a onPageScrollStateChanged() počúva na zmeny stavu stránkovača, kde možno zistiť, či používateľ práve posúva prstom stránky (a vymieňa položky), alebo už je zámena položiek dokončená. Nad tým sa ale nemusíme trápiť: obe metódy môžeme nechať prázdne.

@Override
public void onPageScrolled(int i, float v, int i2) {

}

@Override
public void onPageScrollStateChanged(int i) {

Inicializácia kariet

Aby to naozaj fungovalo, musíme ručne vytvoriť karty podobne, ako sme to robili pri tutoriáli o kartách. V tomto prípade budú karty kreované s titulkami podľa jednotlivých krajov:

protected void onCreate(Bundle savedInstanceState) {
    ...

    ActionBar actionBar = getSupportActionBar();
    actionBar.setNavigationMode(ActionBar.NAVIGATION_MODE_TABS);
    for (String regionName : regionNames) {
        ActionBar.Tab tab = actionBar.newTab()
                .setText(regionName)
                .setTabListener(this);
        actionBar.addTab(tab);
    }
}

Aplikácia je hotová!

Vyskúšajte si hotovú verziu! Klikaním na karty meníte výber a posúvaním stránkovača zároveň posúvate karty.

Výsledná aplikácia je k dispozícii na Githube, v repozitári novotnyr/android-viewpager-demo-2015.