Výsuvný panel / Navigation Drawer

Hamburgerové menu

Výsuvný navigačný panel (navigation drawer) predstavuje panel, ktorý možno prstom vytiahnuť z ľavého (alebo pravého) okraja displeja. Hodí sa pre situácie, keď má aplikácia väčšie množstvo podaktivít, v ktorých sa používateľ potrebuje zorientovať.

Príkladom je navigačný panel v GMaili:

Ukážka navigačného panela z Google Plus

Tento panel tak predstavuje akýsi rázcestník pre činnosti používateľa. Často sa používa vo chvíľach, keď už karty na lište akcií nevyhovujú, pretože je ich primnoho.

Navigačný panel má dva stavy:

  • vysunutý panel, či už z ľavej alebo pravej strany, prekrýva hlavný obsah tieňom
  • zasunutý panel nie je viditeľný

Stavy panelu možno prepínať nielen ťahaním spoza okraja displeja, ale aj kliknutím na ikonu appky v lište akcií.

Vytváranie výsuvného panelu

Na vytvorenie výsuvného panelu potrebujeme:

  1. deklarovať layout aktivity, ktorý v sebe ponesie vzhľad panela i vzhľad hlavného obsahu
  2. deklarovať v kóde widget pre panel DrawerLayout
  3. deklarovať v kóde ActionBarDrawerToggle, ktorým budeme prepínať vysunutý a zasunutý stav pomocou ikony appky na lište akcií
  4. prepojiť toggle s aktivitou a výsuvným panelom

Architektúra

Deklarovanie layoutu s výsuvným panelom

Výsuvný panel získame prakticky zadarmo: potrebujeme však v aktivite deklarovať layout typu android.support.v4.widget.DrawerLayout. Do layoutového súboru aktivity activity_main.xml teda dajme tento kód (nahraďme ním existujúci RelativeLayout):

<android.support.v4.widget.DrawerLayout
    xmlns:android="https://schemas.android.com/apk/res/android"
    xmlns:tools="https://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity"
    android:id="@+id/drawerLayout"
    >

    <LinearLayout android:id="@+id/contentLayout"
        android:orientation="horizontal"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        />

    <ListView android:id="@+id/navigationListView"
        android:layout_height="match_parent"
        android:layout_width="240dp"
        android:layout_gravity="start"
        android:background="@android:color/holo_orange_light"
        android:entries="@array/shopNavigationItems"
        >

    </ListView>
</android.support.v4.widget.DrawerLayout>

V layoute deklarujeme dva významné elementy:

  1. lineárny layout, ktorý bude obsahovať hlavný obsah aktivity. Musíme mu prideliť identifikátor, ktorý sa využije napríklad v transakciách pri zámene fragmentov po kliknutí na zoznam v navigačnom paneli.
  2. zoznam reprezentujúci obsah navigačného panela. (Samozrejme, nemusí to byť len zoznam, pokojne môžeme do navigačného panela dať vnorený komplexný layout.) Widgetu musíme podobne prideliť identifikátor a nastaviť jeho výšku na celý displej. V prípade šírky však už radšej zvoľme pevnú hodnotu -- dokumentácia odporúča najviac 240 dp, čo je hodnota, ktorá i pri vysunutom paneli ponechá trochu miesta na indikovanie obsahu za panelom. Zároveň nastavme polohu panela: možnosť layout_gravity s hodnotou start umiestni panel vľavo. Voliteľne ešte môžeme nastaviť pozadie (background) a položky zoznamu reprezentované odkazom na resource typu pole.

Položky zoznamu

Položky zoznamu sú reprezentované cez resource súbor typu pole (array). Vložme do súboru strings.xml element <string-array>, v ktorom deklarujeme položky zoznamu vo výsuvnom navigačnom paneli.

<resources>
    ...
    <string-array name="shopNavigationItems">
        <item>Hry</item>
        <item>Knihy</item>
        <item>Filmy</item>
    </string-array>
</resources>

V layoute aktivity sa odkážeme na resource s poľom cez @array/shopNavigationItems.

Kód v aktivite, prvá verzia

Kód aktivity, ktorá poskytne prvú verziu vysúvacieho panela, môže byť minimalistický:

import android.os.Bundle;
import android.support.v7.app.ActionBarActivity;
import android.widget.ListView;

public class MainActivity extends ActionBarActivity  {

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

Nič špeciálne, vlastne... nič tu nie je. Ale po spustení uvidíme layout v akcii! Síce to bude skôr počítačová adventúra, pretože jediný spôsob, ako vysunúť navigačný panel, je ťahať prstom (myšou) spoza ľavého okraja displeja, ale už vidíme základné správanie panela.

Príprava lišty akcií

V ďalšom kroku nachystáme lištu akcií: zobrazíme na nej tlačidlo s ikonou, ktorá bude klikateľná a zároveň umožní vysúvať a zasúvať panel. Do metódy onCreate() teda dodáme:

    getSupportActionBar().setHomeButtonEnabled(true);
    getSupportActionBar().setDisplayHomeAsUpEnabled(true);

Keďže vysúvací panel využíva knižnicu kompatibility a interaguje so spätne kompatibilnou lištou akcií, musíme inštanciu lišty získavať volaním getSupportActionBar(). Výsledok metódy getActionBar() v tomto prípade vracia null.

O niečo neskôr sa z tejto ikony stane spomínaný hamburger, ktorým budeme roztvárať panel.

Oživenie tlačidla

Na ozajstné oživenie tlačidla potrebujeme získať referenciu na DrawerLayout a vytvoriť ActionBarDrawerToggle, teda widget s ikonou hamburgeru.

Toggle má konštruktor, ktorý je prinajmenšom podivný:

  1. potrebuje inštanciu kontextu, teda aktivity,
  2. potrebuje inštanciu DrawerLayoutu, ktorú získame pomocou findViewById(),
  3. a finálne dvojperlie: dva parametre reprezentujúce identifikátory reťazcov v resource súboroch, ktoré obsahujú reťazcový popis akcie pre vysúvanie a zasúvanie panela. (Prečítajte si to 5x po sebe). Pre tieto dva parametre uveďme identifikátory R.string.openDrawer a R.string.closeDrawer, ktoré vytvoríme dodatočne.

Tlačidlo teda vytvorme a priraďme do inštančnej premennej typu ActionBarDrawerToggle.

    drawerToggle = new ActionBarDrawerToggle(this,
            drawerLayout,
            R.string.openDrawer,
            R.string.closeDrawer
    );

Toggle má dve implementácie: jednu z knižnice v4 a novšiu z knižnice v7. V ukážke používame novšiu verziu z balíčka android.support.v7.app.ActionBarDrawerToggle.

V tejto chvíli je samozrejme dvojica zázračných parametrov zvýraznená chybou. Našťastie, Studio umožní klávesovou skratkou Alt-Enter využiť pomoc Create String resource value 'openDrawer'.

Stačí v dialógu vyplniť hodnotu a výsledok sa objaví v súbore strings.xml.

Postup zopakujeme ešte raz a nahliadnime do súboru, kde uvidíme dve deklarácie:

<resources>
    ...
    <string name="openDrawer">Otvoriť</string>
    <string name="closeDrawer">Zatvoriť</string>
    ...    
</resources>

Prepojenie tlačidla a výsuvného panela

Ak máme panel reprezentovaný DrawerLayoutom a tlačidlo toggle, prepojíme ich jednoducho: panel umožnuje registráciu poslucháča na vysúvanie a zasúvanie a práve tlačidlo môže byť takýmto poslucháčom.

drawerLayout.setDrawerListener(drawerToggle);

Celý kód metódy onCreate() vyzerá nasledovne:

private ActionBarDrawerToggle drawerToggle;

private DrawerLayout drawerLayout;

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

    getSupportActionBar().setHomeButtonEnabled(true);
    getSupportActionBar().setDisplayHomeAsUpEnabled(true);

    drawerLayout = (DrawerLayout) findViewById(R.id.drawerLayout);

    drawerToggle = new ActionBarDrawerToggle(this,
            drawerLayout,
            R.string.openDrawer,
            R.string.closeDrawer
    );

    drawerLayout.setDrawerListener(drawerToggle);
}

Synchronizácia stavu tlačidla so stavom aktivity

Aby to všetko fungovalo správne, musíme synchronizovať stav aktivity s tlačidlom po vytvorení aktvity:

@Override
protected void onPostCreate(Bundle savedInstanceState) {
    super.onPostCreate(savedInstanceState);
    drawerToggle.syncState();
}

Podobne musíme synchronizovať stav pri zmene konfigurácie systému:

@Override
public void onConfigurationChanged(Configuration newConfig) {
    super.onConfigurationChanged(newConfig);
    drawerToggle.onConfigurationChanged(newConfig);
}

Obsluha kliku na hamburger

Tlačidlo hamburgera sa pre účely obsluhy tvári ako akékoľvek iné tlačidlo. Ak ho chceme obslúžiť, v metóde onOptionsItemSelected() najprv delegujeme volanie tejto metódy na toggle. Ak toggle nedokáže obslúžiť aktuálne vybrané tlačidlo na lište akcií (napríklad klikáme na iné tlačidlo než toggle), využijeme štandardné správanie:

@Override
public boolean onOptionsItemSelected(MenuItem item) {
    if(drawerToggle.onOptionsItemSelected(item)) {
        return true;
    }
    return super.onOptionsItemSelected(item);
}

Obsluha kliku na položku zoznamu vo výsuvnom paneli

Ak chceme obslúžiť kliknutie na položku zoznamu vo výsuvnom paneli, použijeme známe veci. V metóde onCreate() si vytiahneme z layoutu zoznam ListView a pridáme mu poslucháča na kliknutie. Poslucháčom môže byť aktivita:

ListView navigationListView = (ListView) findViewById(R.id.navigationListView);
navigationListView.setOnItemClickListener(this);

Samozrejme, tá musí implementovať interfejs OnItemClickListener a poskytnúť príslušnú metódu:

public class MainActivity ... implements AdapterView.OnItemClickListener {
    @Override
    public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
        ...
    }
}

V metóde potom môžeme zamienať fragmenty v hlavnom obsahu, alebo spúšťať nové aktivity, čím úplne oživíme výsuvný panel. V ukážke si zistíme reťazec s popiskom v položke, na ktorú sa kliklo a nastavíme ho ako hlavičku aktivity. Následne ešte musíme zavrieť vysúvací panel.

@Override
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
    String selectedItemTitle = (String) parent.getAdapter().getItem(position);
    setTitle(selectedItemTitle);

    drawerLayout.closeDrawers();
}

Záver

Výsledný kód aplikácie možno nájsť v Githube v repozitári novotnyr/android-drawerlayout-demo-2015.