Ú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é oif
ovať, č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žkyView
, teda objekt, nad ktorým bolo vyvolané kontextové menu. U nás pôjde o celý zoznamListView
.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 SimpleCursorAdapter
a), 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 String
y. 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 naswitch
ovaní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 switch
ovanie 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);
}