S príchodom lišty akcií v Androide 4.x sa otvorili nové možnosti. Nielenže na nej môžeme zobrazovať tlačidlá, karty, prípadne rozbaľovacie zoznamy, môžeme sa tiež prepínať medzi viacerými rozličnými sadami widgetov a to podľa toho, akej činnosti sa používateľ chce venovať.
Ak vyberiete text v textovom políčku, zrazu sa lišta akcií prepne do režimu zobrazovania tlačidiel pre prácu so schránkou (teda miestna obdoba Ctrl-C, Ctrl-X a Ctrl-V).
Takýchto režimov, presnejšie režimov akcií (action mode) môže mať aplikácia viacero.
Krásny príklad ekšn módu predstavuje hromadná práca s položkami zoznamu.
Ak máme zoznam ListView
, používateľ môže podržať prst nad ľubovoľnou položkou,
čím sa prepne do režimu viacnásobného výberu položiek. Vybrané položky vie
označiť ako obľúbené, zdieľať, či odstrániť. Z tohto režimu sa kedykoľvek
vie vrátiť späť do bežnej práce so zoznamom, kde klik na položku zobrazí
detailovú aktivitu.
Vytvorme si ukážkovú aplikáciu Webookmark, ktorá zobrazí v zozname všetky stránky, ktoré má používateľ uložené ako záložky v prehliadači.
Aplikácia bude mať nasledovnú štruktúru:
BookmarkListFragment
MainActivity
obaľujúca fragmentZoznam záložiek bude zobrazovaný v zoznamovom fragmente. Prečo? Aktivita s widgetom ListView
je skvelá a užitočné, ale vyskúšajme si iný prístup: Android ponúka triedu android.app.ListFragment
, čo je presne fragment s jediným zoznamom na celú šírku displeja a so skvelými pomocnými metódami, ktoré napríklad umožnia zobraziť zástupný text, ak v zozname nie sú žiadne dáta,
Dáta zoznamu budeme vyťahovať zo systémového content providera pre záložky a históriu prehliadača, ktorý je identifikovaný adresou Uri
v konštante Browser.BookmarkColumns
. Z tabuľky využijeme stĺpec TITLE
s popiskom záložky.
Samozrejme, pri načítavaní dát využijeme mechanizmus loaderov a callbackov, aby sme dodržali zásady o efektívnom a asynchrónnom prístupe k dátam.
Vytvorme podľa toho kostru fragmentu:
public class BookmarkListFragment extends ListFragment implements LoaderManager.LoaderCallbacks<Cursor> {
private static final int BOOKMARK_LIST_LOADER_ID = 0;
private SimpleCursorAdapter listViewAdapter;
@Override
public void onViewCreated(View view, Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
initListAdapter();
setListAdapter(this.listViewAdapter);
getLoaderManager().initLoader(BOOKMARK_LIST_LOADER_ID, Bundle.EMPTY, this);
}
private void initListAdapter() {
String[] from = { Browser.BookmarkColumns.TITLE, Browser.BookmarkColumns.URL };
int[] to = { android.R.id.text1, android.R.id.text2 };
this.listViewAdapter = new SimpleCursorAdapter(
getActivity(),
android.R.layout.simple_list_item_2,
Defaults.NO_CURSOR,
from, to,
Defaults.NO_FLAGS);
}
@Override
public Loader<Cursor> onCreateLoader(int id, Bundle args) {
CursorLoader loader = new CursorLoader(getActivity());
loader.setUri(Browser.BOOKMARKS_URI);
return loader;
}
@Override
public void onLoadFinished(Loader<Cursor> loader, Cursor cursor) {
this.listViewAdapter.swapCursor(cursor);
}
@Override
public void onLoaderReset(Loader<Cursor> loader) {
this.listViewAdapter.swapCursor(Defaults.NO_CURSOR);
}
}
Všimnime si špeciality prístupu k tradičným komponentom:
getActivity()
), keďže fragment nie je kontextom,getListView()
,ListView
cez zdedenú setListAdapter()
.Záznam z tabuľky zobrazíme v položke s dvoma podpoložkami, reprezentovanej systémovým layoutom android.R.layout.simple_list_item_2
. Tomu zodpovedajú aj dva prvky v poliach from
a to
, kde sa stĺpec TITLE
namapuje na textové políčko so systémovým identifikátorom android.R.id.text1
a adresa URL
záložky sa namapuje na políčko so systémovým identifikátorm android.R.id.text2
.
To je všetko, čo sa týka fragmentu, poďme ho obaliť aktivitou!
public class MainActivity extends ActionBarActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
getFragmentManager()
.beginTransaction()
.add(android.R.id.content, new BookmarkListFragment())
.commit();
}
}
Hlavná aktivita nepotrebuje nič špeciálne (dokonca ani layout v XML!), pretože jej jedinou úlohou je zahrnúť do seba fragment v rámci transakcie.
Zoznam ListView
má takmer automatickú podporu pre akčný režim. Potrebujeme niekoľko konfiguračných nastavení:
potrebujeme nastaviť režim výberu položiek na hromadný výber s použitím akčného režimu:
getListView().setChoiceMode(AbsListView.CHOICE_MODE_MULTIPLE_MODAL);
potrebujeme nastaviť poslucháča pre udalosti prepínania z a do akčného režimu:
getListView().setMultiChoiceModeListener(...)
Na zoznamovom widgete môžeme zaregistrovať poslucháča na hromadný výber v akčnom režime, ktorého reprezentuje objekt implementujúci interfejs AbsListView.MultiChoiceModeListener
.
boolean onCreateActionMode(ActionMode mode, Menu menu)
: metóda sa zavolá vo chvíli, keď sa zoznamový widget prepína do akčného režimu. V tomto prípade by sme mali nafúknuť zo XML definíciu položiek lišty akcií, ktoré sa zobrazia v tomto režime.boolean onPrepareActionMode(ActionMode mode, Menu menu)
: volá sa hneď po vytvorení akčného režimu alebo v situácii, keď sa položky lišty akcií v akčnom režime zneplatnili (napr. volaním ActionMode#invalidate()
)void onDestroyActionMode(ActionMode mode)
zavolaná pri vypínaní akčného režimuĎalšie dve metódy súvisia s obsluhou kliknutia:
void onItemCheckedStateChanged(ActionMode mode, int position, long id, boolean checked)
sa zavolá pri výbere, čiže pri zahrnutí položky do hromadného výberuboolean onActionItemClicked(ActionMode mode, MenuItem item)
je volaná vo chvíli, keď sa klikne na tlačidlo na lište akcií.Metódy, ktoré vracajú booleovskú hodnotu, majú vrátiť true
, ak implementácia obslúžila danú metódu. V opačnom prípade to znamená, že sa využije štandardná obsluha (neprepneme sa do akčného módu, príprava nie je potrebná atď) z rodičovskej implementácie.
Poslucháčom akčného módu môže byť samotná inštancia fragmentu, ktorú upravíme, aby implementovala interfejs MultiChoiceModeListener
:
public class BookmarkListFragment extends ListFragment implements LoaderManager.LoaderCallbacks<Cursor>, AbsListView.MultiChoiceModeListener
V metóde onCreateActionMode()
nafúkneme položky lišty akcií, ktoré reprezentujú akcie, ktoré môže používateľ vykonať s hromadným výberom a následne nastavíme text pre hlavičku, ktorá navrhuje používateľovi, čo má v akčnom režime robiť:
@Override
public boolean onCreateActionMode(ActionMode mode, Menu menu) {
mode.getMenuInflater().inflate(R.menu.bookmarks_cab, menu);
mode.setTitle("Select Items");
return true;
}
Čo znamená resource R.menu.bookmars_cab
? Vytvorme nový súbor pre resource (New Android Resource File) typu Menu s názvom bookmarks_cab.xml
a s obsahom:
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="https://schemas.android.com/apk/res/android">
<item android:id="@+id/listViewDeleteSelectedAction" android:title="Delete" />
</menu>
Jediná položka na lište akcií v akčnom režime umožní zmazať vybrané položky zoznamu.
Metóda pre prípravu položiek lišty akcií v akčnom móde nebude potrebná, rovnako ako metóda volaná pri zatváraní režimu akcií. Ponecháme ich prázdne:
@Override
public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
return false;
}
@Override
public void onDestroyActionMode(ActionMode mode) {
// do nothing
}
Ak používateľ klikne na položku zoznamu, chceme, aby to znamenalo jej zahrnutie do hromadného výberu (alebo odobratie z neho). Ak prekryjeme metódu onItemCheckedStateChanged()
, a umne nastavíme vizuál pre layout položky, dostaneme skvelú vec: nielenže nám bude zoznam evidovať zoznam vybraných položiek, ale zároveň ich bude zvýrazňovať preddefinovanou farbou.
Na to, aby to fungovalo, potrebujeme:
android.R.layout.simple_list_item_activated_2
. Tento layout položky zoznamu reprezentuje dve podpoložky (väčším písmom pre hlavičku a menším pre popis), ktorá zároveň podporuje aktivovaný stav, teda stav užitočný pre vybrané položky.getCheckedXXX()
.@Override
public void onItemCheckedStateChanged(ActionMode mode, int position, long id, boolean checked) {
int checkedCount = getListView().getCheckedItemCount();
mode.setSubtitle("Selected item(s): " + checkedCount);
}
Nastavenie podtitulku na objekte mode
ovplyvní druhý riadok na lište akcií.
OK, vybrali sme položky v zozname, a čo s nimi? Môžeme ich zmazať!
V metóde onActionItemClicked()
sa dozvieme, na ktoré tlačidlo v lište akcií používateľ klikol, a zavoláme pomocnú metódu:
@Override
public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
switch(item.getItemId()) {
case R.id.listViewDeleteSelectedAction:
deleteSelectedListItems();
return true;
}
return false;
}
Mazanie položiek nijak mimoriadne nesúvisí s kontextovou lištou akcií. Najdôležitejšie je zistiť identifikátory vybraných položiek, ktoré zodpovedajú primárnym kľúčom riadkov tabuľky content providera. Vďaka metóde getCheckedItemIds()
je to ľahké.
Každý vybraný identifikátor použijeme ako parameter pri mazaní z content providera cez jeho metódu delete()
.
private void deleteSelectedListItems() {
for (long id : getListView().getCheckedItemIds()) {
getActivity().getContentResolver().delete(Browser.BOOKMARKS_URI, BaseColumns._ID + " = " + id, Defaults.NO_SELECTION_ARGS);
getLoaderManager().restartLoader(BOOKMARK_LIST_LOADER_ID, Bundle.EMPTY, this);
}
}
Je veľmi dôležité aktualizovať zoznam po jeho vymazaní: a na to využijeme možnosť reštartovať príslušný kurzorový loader.
Po vymazaní položiek sa akčný mód automaticky skončí.
Všimnime si, ako tvoríme hlavičku aktivity v prípade akčného režimu: cez reťazec "Selected Item(s)". Ak by sme vytvárali lokalizovanú aplikáciu, určite by sme neboli šťastní, keby sme videli v slovenčine "Vybrané položky: 1". Našťastie, i na toto mysleli dizajnéri Androidu. Vieme definovať jazykové resources, ktoré korektne vyskloňujú a naformátujú reťazce takéhoto typu. Dynamicky potom vieme zistiť, že nemáme "Žiadne vybrané položky", ale budeme mať "1 vybratá položka", ale i "5 vybratých položiek."
V súbore strings.xml
zaveďme
<plurals name="selected_bookmarks">
<item quantity="one">One selected bookmark</item>
<item quantity="other">%d bookmarks selected</item>
</plurals>
Definujeme plurálový reťazec, ktorý pre jednu (one) položku vrátime reťazec iný než pre prípad iného (other) počtu položiek. Vo formátovaní vieme využívať zástupné parametre, napr. %d
reprezentujúce číselný parameter dosadený za behu.
Po úprave kódu bude vyzerať metóda:
@Override
public void onItemCheckedStateChanged(ActionMode mode, int position, long id, boolean checked) {
int checkedCount = getListView().getCheckedItemCount();
String checkedCountMessage = getResources().getQuantityString(R.plurals.selected_bookmarks, checkedCount, checkedCount);
mode.setSubtitle(checkedCountMessage);
}
Najprv získame getResource()
a z neho reťazec kvantity (getQuantityString()
), kde získame referenciu na príslušný plurálový resource. Ďalší parameter metódy reprezentuje počet položiek, z ktorého sa vyberie príslušný reťazec a druhá (zopakovaná) položka predstavuje hodnotu zamenenú za zástupný znak %d
v reťazci.
Celý kód fragmentu, ktorý implementuje LoaderCallbacks
pre callbacky loaderov a `MultiChoiceListener* pre callbacky akčného módu, vyzerá nasledovne:
public class BookmarkListFragment extends ListFragment implements LoaderManager.LoaderCallbacks<Cursor>, AbsListView.MultiChoiceModeListener {
@Override
public void onViewCreated(View view, Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
initListAdapter();
initList();
setListAdapter(this.listViewAdapter);
getLoaderManager().initLoader(BOOKMARK_LIST_LOADER_ID, Bundle.EMPTY, this);
}
private void initList() {
getListView().setChoiceMode(AbsListView.CHOICE_MODE_MULTIPLE_MODAL);
getListView().setMultiChoiceModeListener(this);
}
// ----- Fragment UI Logic
private void deleteSelectedListItems() {
for (long id : getListView().getCheckedItemIds()) {
getActivity().getContentResolver().delete(Browser.BOOKMARKS_URI, BaseColumns._ID + " = " + id, Defaults.NO_SELECTION_ARGS);
getLoaderManager().restartLoader(BOOKMARK_LIST_LOADER_ID, Bundle.EMPTY, this);
}
}
// ----- Contextual Action Bar callbacks
@Override
public void onItemCheckedStateChanged(ActionMode mode, int position, long id, boolean checked) {
int checkedCount = getListView().getCheckedItemCount();
String checkedCountMessage = getResources().getQuantityString(R.plurals.selected_bookmarks, checkedCount, checkedCount);
mode.setSubtitle(checkedCountMessage);
}
@Override
public boolean onCreateActionMode(ActionMode mode, Menu menu) {
MenuInflater menuInflater = getActivity().getMenuInflater();
menuInflater.inflate(R.menu.bookmarks_cab, menu);
mode.setTitle("Select Items");
return true;
}
@Override
public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
return false;
}
@Override
public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
switch(item.getItemId()) {
case R.id.listViewDeleteSelectedAction:
deleteSelectedListItems();
return true;
}
return false;
}
@Override
public void onDestroyActionMode(ActionMode mode) {
// do nothing
}
}
Výsledný kód nájdete na GitHube, v repozitári novotnyr/android-webbookmark-demo-2015
.
Výsledná aplikácia ukazuje klasický príklad práce so zoznamom a content providerom, kde navyše položky zoznamu umožňujú hromadné spracovanie. Táto situácia sa hodí pre viacero typických aplikácií, od zoznamov úloh až po spracovanie mailov.