Android: kontextové menu pre zoznamy ListView

Úvod

Ak máme aktivitu so zoznamom (ListActivity), neraz potrebujeme dodať k položkám kontextové menu. Na bežných operačných systémoch by sme ho zobrazili pravým klikom myši na položke. V Androide myš neexistuje (ani neexistuje klik druhou rukou ;-)), ale vieme ho vyvolať pomocou podržania prstu (“long press”).

Ako dodáme menu do aktivity?

  • zaregistrujeme zoznam do centrálnej metódy v aktivite, kde sa bude obsluhovať kontextové menu,
  • prekryjeme metódu onCreateContextMenu(), kde nachystáme položky do menu
  • prekryjeme centrálnu obslužnú metódu, v ktorej dodáme obslužný kód pre položky menu

Ak používateľ “longpress”ne nad položkou zoznamu, aktivita sama zistí, že sa má vyvolať kontextové menu, zavolá metódu onCreateContextMenu(), naplní položky a zobrazí ich. Používateľ si zvolí niektorú z položiek menu a zavolá sa metóda onContextItemSelected(), kde sa rozhodne, čo presne sa má stať na základe zvolenej položky.

Ukážme si to na príklade projektu histórie volaní z minulých dielov o Androide.

Registrácia widgetu so zoznamom

Widget so zoznamom žiaľ nemá žiadnu metódu v duchu “pridať obsluhu pre podržanie prstu nad položkou”. Namiesto toho sa vieme spoľahnúť na obslužné metódy v aktivite. Stačí oznámiť aktivite, že zoznam sa chce zaregistrovať do centrálnej obsluhy kontextových menu.

V aktivite typu ListActivity stačí spraviť:

@Override
protected void onCreate(Bundle savedInstanceState) {
    /* ... */

    registerForContextMenu(getListView());
}  

Metóda getListView() nám vráti interný widget so zoznamom ListView, v ktorom sú zobrazené položky.

Vytvorenie položiek menu

Každá aktivita má metódu onCreateContextMenu(), kde sa majú vytvoriť položky menu. Táto metóda je centrálna pre všetky prípadné widgety, ktoré chcú zobraziť jednotlivé menu, ale v našom prípade je to jednoduché — máme totiž len jeden kontextomenuchtivý prvok.

Ale i tak je vhodné oifovať, či ideme zobraziť menu pre ten widget, ktorý nás zaujíma.

@Override
public void onCreateContextMenu(ContextMenu menu, View view, ContextMenuInfo menuInfo) {
    if (view.getId() == this.getListView().getId()) {
        menu.setHeaderTitle("Call Entry");

        menu.add(Menu.NONE, MENU_ITEM_ID_DIAL, Menu.NONE, "Dial");
        menu.add(Menu.NONE, MENU_ITEM_ID_SMS, Menu.NONE, "SMS");
    }
}

Metóda má tri parametre:

  • ContextMenu zodpovedajúci menu, do ktorého budeme pridávať položky
  • View, teda objekt, nad ktorým bolo vyvolané kontextové menu. U nás pôjde o celý zoznam ListView.
  • ContextMenuInfo obsahuje dodatočné informácie o konkrétnej položke menu, nad ktorou bolo longpressnuté. V príklade ho nepoužijeme, ale dôjde naňho reč v metóde, kde budeme obsluhovať zvolené položky.

Ak sme usúdili, že každá položka zoznamu má mať rovnaké kontextové menu, je situácia jednoduchá: metódou setHeaderTitle() nastavíme nadpis nad kontextovým menu a metódou add() nahádžeme do menu jednotlivé položky.

V príklade zobrazíme dve položky: pre vytáčanie čísla a pre odoslanie SMSky na číslo z histórie volaní.

Pridanie položiek do menu

Metóda add() má štyri parametre:

  • skupinu pre položky. U nás skupiny ignorujeme, čiže použijeme zabudovanú konštantu Menu.NONE.
  • jednoznačný identifikátor položky. Deklarujeme ho ako konštantu, napr.:

    private static final int MENU_ITEM_ID_DIAL = Menu.FIRST + 1;
    private static final int MENU_ITEM_ID_SMS = Menu.FIRST + 2;
    

    Konštanta Menu.FIRST je opäť zabudovaná a slúži na pohodlné indexovanie identifikátorov.

  • poradie položiek. Nezáleží nám na ňom, preto použijeme klasiku Menu.NONE.

  • reťazec s textom, ktorý sa zobrazí v menu.

Obsluha položiek menu

Tak, ako sa položky menu vytvárajú v centrálnej metóde aktivity, sa aj obsluha zvolenej položky deje na jednom mieste.

Každá aktivita má metódu:

public boolean onContextItemSelected(MenuItem item)

Jediný parameter nesie v sebe položku menu, ktorú používateľ zvolil.

Čo potrebujeme v príklade? Zistiť, ktorý objekt sa skrýva za zvolenou položkou (keďže zobrazujeme dáta z kurzora, chceme získať Cursor nasmerovaný na príslušný riadok z databázovej tabuľky). Ďalej chceme získať hodnotu stĺpca s telefónnym číslom a potom buď vyvolať dialer (na telefonovanie) alebo aplikáciu na posielanie SMSky.

Ako zistiť riadok tabuľky schovaný za položkou?

Objekt MenuItem v parametri má metódu getMenuInfo(), z ktorej vieme zistiť podrobnejšie informácie o zvolenej položke.

Ak zobrazujeme menu nad widgetom, ktorého dáta pochádzajú z adaptéra (a zoznam ListView takým je, u nás berie dáta zo SimpleCursorAdaptera), v treťom parametri ContextMenuInfo objaví špecifickejší objekt typu AdapterView.AdapterContextMenuInfo, ktorý nesie napr. informáciu o poradí položky v zozname, či dokonca o jej identifikátore.

 -----------------
| ContextMenuInfo |
 -----------------
        /\
       ----
        ||
        ||
 ------------------------------------ 
| AdapterView.AdapterContextMenuInfo |
 ------------------------------------ 
| +id         : long                 |
| +position   : long                 |
| +targetView : long                 |
 ------------------------------------ 

Premenná id obsahuje identifikátor položky: ak položka pochádza z riadka databázovej či providerovej tabuľky, je v nej hodnota stĺpca _id. position zase udáva pozíciu prvku v zozname od začiatku (prvá položka má hodnotu 0.) Posledný parameter, targetView, obsahuje potomkovský view, pre ktorý sa zobrazuje kontextové menu. V prípade zoznamov ListView tu bude konkrétny view pre jednotlivú položku.

To je cesta, ktorou získame konkrétny zvolený objekt: ak zobrazujeme dáta z kurzora, možno by sme chceli získať rovno celý riadok z databázovej tabuľky, vyťahať z neho hodnoty a na základe nich určiť, ktoré položky v menu zobraziť a ktoré nie.

Urobme si pomocnú metódu, ktorá to spraví za nás a zároveň ošetrí okrajové prípady:

public static <T> T getContextItemObject(ListAdapter listAdapter, ContextMenuInfo menuInfo, Class<T> clazz) throws ClassCastException {
    if(menuInfo instanceof AdapterContextMenuInfo) {
        AdapterView.AdapterContextMenuInfo contextMenuInfo = (AdapterContextMenuInfo) menuInfo;
        @SuppressWarnings("unchecked")
        T object = (T) listAdapter.getItem(contextMenuInfo.position);
        return object;
    } else {
        throw new ClassCastException("Menu info object is should be a descendant of AdapterContextMenuInfo.");
    }
}

Metóda dostane na vstup adaptér zoznamu, položku a triedu, ktorá reprezentuje dátový typ výsledku. (Ak budeme mať v adaptéri reťazce, metóda vráti Stringy. V inom prípade sa v adaptéri môžu zobrazovať dáta z kurzora, čím budeme vracať rovno Cursor).

Metóda pretypuje informáciu o zvolenom prvku na správny typ (AdapterContextMenuInfo), čím dostaneme k dispozícii premennú position udávajúcu pozíciu prvku v zozname.

Následne z adaptéra získame príslušný prvok, pretypujeme ho na taký typ, ako si vyžiadal používateľ a vrátime ho von z metódy.

Metódu môžeme umiestniť do pomocnej triedy, napr. s názvom ContextItemUtils.

Potom môžeme pokračovať: čerstvo vytvorenou metódou získame na základe zvolenej položky kurzor nastavený na príslušný riadok. Ošetríme okrajový prípad, keď je náhodou kurzor null (čo sa stane len vo veľmi svojskej situácii) a následne z neho vytiahneme telefónne číslo.

@Override
public boolean onContextItemSelected(MenuItem item) {
    Cursor cursor = ContextItemUtils.getContextItemObject(getListAdapter(), item.getMenuInfo(), Cursor.class);
    if(cursor == null) {
        Log.e(TAG, "No Cursor data for item " + item);
        return false;
    }
    String phoneNumber = cursor.getString(cursor.getColumnIndex(CallLog.Calls.NUMBER));

    ...
}

Zistenie vybranej položky

Objekt MenuItem v sebe nesie identifikátor zvolenej položky. Áno, presne ten identifikátor, ktorý sme nastavovali v druhom parametri metódy add() na objekte Menu.

Jednoduchým naswitchovaním vytvoríme rozhodovaciu logiku pre jednotlivé položky menu.

Nezabudnime dodržať kontrakt metódy! Ak obslúžime výber položky, vrátime true. Ak náhodou ideme obsluhovať položku menu, ktorú nepoznáme (napr. v prípade zložitejších aplikácií, kde sú v menu aj ďalšie položky, ktoré sa zjavili nejakým automatickým spôsobom), ponecháme obsluhu na rodičovskú implementáciu metódy.

@Override
public boolean onContextItemSelected(MenuItem item) {
    // ...

    switch(item.getItemId()) {
    case MENU_ITEM_ID_DIAL:
        //...obsluha Dial

        return true;

    case MENU_ITEM_ID_SMS:
        //... obsluha SMS

        return true;
    default:
        return super.onContextItemSelected(item);
    }
}

Ako telefonovať a SMSkovať?

Ak máme telefónne číslo, zavolať cez dialer je jednoduché. Stačí vytvoriť správny intent a naštartovať aktivitu:

case MENU_ITEM_ID_DIAL:
    Intent callIntent = new Intent(Intent.ACTION_CALL, Uri.fromParts("tel", phoneNumber, null));
    startActivity(callIntent);

    return true;

SMSkovanie je analogické:

case MENU_ITEM_ID_SMS:
        Intent smsIntent = new Intent(Intent.ACTION_VIEW, Uri.fromParts("sms", phoneNumber, null));
        startActivity(smsIntent);
        return true;

A to je všetko. Stiahnite si ukážkový projekt a spustite ho a uvidíte nádherné kontextové menu pre každú položku.

Dodatok: vytvorenie položiek menu: variant B

Ak sme si istí, že položky menu sú pre každú položku zoznamu rovnaké, namiesto metódy Menu#add() môžeme využiť definíciu menu v XML súbore.

Vytvorenie resource súboru pre menu

Pomocou sprievodcu vytvoríme nový súbor typu Android XML File. Typ resource (Resource Type) bude Menu a názov súboru môže byť napr. calllog_item.xml.

Obsah súboru nech je:

<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android" >
    <item android:id="@+id/calllog_menu_dial" android:title="Dial"/>   
    <item android:id="@+id/calllog_menu_sms" android:title="Send SMS"/>         
</menu>

Každá položka je v elemente <item>, má pridelený vlastný identifikátor (calllog_menu_dial, resp. calllog_menu_sms).

Vytváranie menu

Vytváranie menu potom využije službu menu inflatera, ktorý načíta XML definíciu a na jej základe naplní objekt typu Menu:

@Override
public void onCreateContextMenu(ContextMenu menu, View view, ContextMenuInfo menuInfo) {
    if (view.getId() == this.getListView().getId()) {
        menu.setHeaderTitle("Call Entry");

        getMenuInflater().inflate(R.menu.calllog_item, menu);
    }
}

Obsluha výberu položky

Následne musíme upraviť aj switchovanie nad identifikátorom položky. Už nepoužívame hodnoty identifikátorov z kódu, ale automaticky pridelené ID z resource súboru.

switch(item.getItemId()) {
case R.menu.calllog_menu_dial:
    //...   

    return true;

case R.menu.calllog_menu_sms:
    //...
    return true;
default:
    return super.onContextItemSelected(item);
}

Pridaj komentár

Vaša e-mailová adresa nebude zverejnená. Vyžadované polia sú označené *