Vytvorme aplikáciu, ktorá zobrazí históriu volaní v utešenej mriežke a la Windows Phone.
GridView
ViewBinder
V Android Studiu vytvorme nový projekt CallGrid s jedinou aktivitou. Kompilovať budeme oproti platforme 4.0.3.
V projekte budeme využívať prichádzajúce, odchádzajúce a zmeškané hovory. Žiaľ, emulátor Genymotion podporuje tieto vlastnosti len v platených verziách, čo nás núti využiť len pomalší pôvodný emulátor.
Jestvuje mnoho pekných situácií pre použitie mriežky namiesto zoznamu. Telefónne čísla, vlajky štátov, galéria obrázkov a tak ďalej. Ak je každá položka pomerne krátka, dokážeme tak na displeji zobraziť omnoho viac údajov a zároveň prehľadnejšie, než v prípade zoznamu. (Nehovoriac o tom, že položky zoznamu v ListView
sa na širokom displeji zobrazujú do priveľkej šírky.)
Na mriežkové dáta máme v Androide dva spôsoby: buď využijeme špeciálny layout manager alebo si poradíme s mriežkovým komponentom.
Mriežkový layout manager je reprezentovaný triedou GridLayout
, do ktorého naukladáme komponenty. Jeho nevýhodou je nedostupnosť v starších Androidoch (do API Level 13).
Pre nás bude oveľa jednoduchšie použiť dedikovaný komponent GridView
, ktorý má API podobné klasickému zoznamu ListView
, ale namiesto ukladania položiek pod seba podporuje presne mriežkovú reprezentáciu. Veľká výhoda spočíva vo využití adaptérov, teda modelov s dátami, ktoré sme si ukázali v minulom dieli.
Do XML súboru pre layout aktivity dodajme komponent GridView
:
<GridView
android:id="@+id/callLogGridView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:numColumns="auto_fit"
android:columnWidth="90dp"
android:listSelector="@null"
/>
Layoutu pridáme nasledovné atribúty:
match_parent
, pretože komponent zaberie celú aktivitu.numColumns
s hodnotou auto_fit
. Ak nastavíme šírku stĺpca columnWidth
na pevnú hodnotu, pri otáčaní na šírku budú pribúdať nové stĺpce.GridView
u, vymažme všetky atribúty android:padding
na LinearLayoute
a vypnime vizuálny indikátor kliknutia na položu reprezentovaný tzv. list selectorom listSelector
(nastavme ho na @null
).V tejto fáze naplníme mriežku údajmi z adaptéra, ktorý sme videli už minule. Definujeme si ArrayAdaptér
obsahujúci reťazce, ktorý naplníme vzorovými telefónnymi číslami.
String[] adapterData = { "0905223223", "0905123456", "112", "055123456" };
ArrayAdapter<String> adapter = new ArrayAdapter<String>(this, android.R.layout.simple_list_item_1, android.R.id.text1, adapterData);
Podľa zásad z minula bude každá položka mriežky využívať zabudovaný layout s jedinou textovou položkou (android.R.layout.simple_list_item_1
) a reťazec s telefónnym číslom namapuje na túto textovú položku, ktorej systémový identifikátor je android.R.id.text1
.
Adaptér potom asociujeme s GridView
om cez metódu setAdapter()
:
Kompletný kód vyzerá nasledovne:
public class MainActivity extends Activity {
private GridView callLogGridView;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
String[] adapterData = { "0905223223", "0905123456", "112", "055123456" };
ArrayAdapter<String> adapter = new ArrayAdapter<String>(this, android.R.layout.simple_list_item_1, android.R.id.text1, adapterData);
callLogGridView = (GridView) findViewById(R.id.callLogGridView);
callLogGridView.setAdapter(adapter);
callLogGridView.setOnItemClickListener(this);
}
}
Po spustení uvidíme neveľmi vábnu aplikáciu, ale aspoň máme možnosť sledovať tvorbu mriežky i preskupovanie jej stĺpcov, ak začneme točiť zariadením:
GridView
nad dátami z content providera¶Doposiaľ sme dáta ťahali z poľového adaptéra, kde boli zadané natvrdo v kóde. Je čas ich ťahať zo skutočných dát v telefóne!
Androiďácke zariadenie nesie v sebe množstvo interných údajov: či už ide o kontakty, SMSky, históriu volaní, údaje v kalendári, fotky v galérii alebo MP3ky či videá. Každý typ dát sa pritom môže nachádzať na rozličných miestach, či už v relačnej databáze, alebo v súborovom systéme (to sa týka MP3jek a fotiek) alebo kdekoľvek inde.
Content Provider (poskytovateľ obsahu) je mechanizmus, ktorý dokáže sprístupniť ľubovoľné dáta jednotným spôsobom bez ohľadu na konkrétny spôsob ich uloženia.
Každý content provider v systéme poskytuje konkrétny typ dát -- máme teda content providera pre fotky, iného providera pre históriu volaní, či onakého providera pre kontakty.
A keďže už starí Rimania vedeli, že ukladať dáta relačným spôsobom je osvedčená metóda, content provider ju preberá a využíva. Dáta sú vždy chápané relačným, teda tabuľkovým spôsobom, kde údaje sú uložené v akýchsi riadkoch, ich vlastnosti (atribúty), sú uložené v pomenovaných stĺpcoch. Každý content provider tak hovorí, aké ,,tabuľky" chce sprístupniť a aké ,,stĺpce" ponúka.
Všetky zabudované providery sú v Androide dostupné v balíčku android.content
a sú reprezentované bežnými triedami. Každý provider má v API niekoľko vnútorných tried, ktoré zodpovedajú jeho tabuľkám. A ak sa ponoríme ešte hlbšie, zistíme, že každá tabuľková trieda definuje jednak sadu konštát zodpovedajúcu názvom stĺpcom a jednak špeciálnu konštantu CONTENT_URI
, ktorá reprezentuje jednoznačný systémový identifikátor (URI
), pomocou ktorého vieme k danej "tabuľke" providera pristúpiť.
Vezmeme si príklad histórie volaní. V dokumentácii sa dozvieme, že existuje trieda content providera android.provider.CallLog
, ktorá obsahuje jedinú "tabuľkovú" podtriedu CallLog.Calls
. Jednoznačný identifikátor tejto tabuľky je definovaný v konštante URI konštante CallLog.Calls.CONTENT_URI
a jednotlivé stĺpce sú špecifikované jednak názvami (napr. NUMBER
a jednak popisom dátového typu v dokumentácii.)
K tabuľkovým dátam pristupujeme pomocou kurzorov.
Kurzor je objekt, ktorý reprezentuje výsledok dopytu (teda tabuľkové dáta) na content providera. Plní dvojakú úlohu: jednak obsahuje celý výsledok a jednak predstavuje akýsi kurzor, či ukazovateľ, ktorý stále mieri na konkrétny riadok vo výsledku.
Získať dáta z content providera znamená získať kurzor, ktorý je na začiatku nastavený pred prvý riadok, a postupne sa bude posúvať po jednotlivých riadkoch výsledku až na koniec tabuľkových dát.
O tom, ako konkrétne získať kurzor z content providera, si povieme neskôr, pretože ešte musíme zvládnuť dve dôležité veci: porozprávať sa o loaderoch a zmeniť typ adaptéra pre GridView
.
Doposiaľ sme používali poľový adaptér nad pevnými dátami, ale ten už nestačí. Vyhoďme teda jeho inicializáciu z kódu.
Našťastie, kurzory sú natoľko častá technika dát, že ním bola venovaná špeciálna trieda pre adaptéry (teda modely) zoznamu. Používajú sa totiž nielen pri práci s content providermi, ale tiež pri SQL databázach.
Nazýva sa SimpleCursorAdapter
a pri vytváraní potrebuje šesť parametrov. Výsledok si poznačme do inštančnej premennej:
this.adapter = new SimpleCursorAdapter(this, android.R.layout.simple_list_item_1, NO_CURSOR, from, to, NO_FLAGS);
this
predstavuje kontext, v tomto prípade vlastniacu aktivituandroid.R.layout.simple_list_item1
, kde položku tvorí jeden reťazec. (Tento layout sme použili už pri zozname úloh.)kurzor obsahujúci dáta zatiaľ nemáme k dispozícii, čo vyriešime hodnotou null
. Pre čitateľnosť si definujeme konštantu NO_CURSOR
, ktorá sa nám hodí ešte na jednom mieste v budúcnosti:
public static final Cursor NO_CURSOR = null;
pole názvov stĺpcov z tabuľky získanej z content providera, ktoré sa zobrazia v podpoložkách položky zoznamu (pozri nižšie)
NO_FLAGS
s hodnotou 0
.Štvrtý a piaty parameter si ukážme na príklade:
Z tabuľky histórie volaní sa rozhodneme zobrazovať len posledné telefónne číslo, čomu zodpovedá stĺpec CallLog.Calls.NUMBER
(zistili sme to z dokumentácie k tabuľke Calls
v content provideri CallLog
). Pole from
teda bude obsahovať len jeden prvok s názvom stĺpca:
String[] from = { CallLog.Calls.NUMBER };
Layout pre celú položku definovaný v druhom parametri, ktorý je v tomto prípade zabudovaný androidovský layout android.R.layout.simple_list_item_1
, obsahuje len jeden widget, a to s identifikátorom android.R.id.text1
. Pole to
bude teda vyzerať:
int[] to = { android.R.id.text1 };
Na prvý prvok z poľa from
sa teda bude mapovať widget z prvého prvku poľa to
. Ak by widgetov v to
bolo viac, tak i-ty prvok z poľa from
sa bude mapovať na i-ty widget z poľa to
.
Ak máme hotový adaptér pre zoznamy, stačí ho asociovať s mriežkou cez:
callLogGridView.setAdapter(adapter);
Konštruktor SimpleCursorAdaptera
public class MainActivity extends ActionBarActivity {
public static final Cursor NO_CURSOR = null;
public static final int NO_FLAGS = 0;
private SimpleCursorAdapter adapter;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
String[] from = { CallLog.Calls.NUMBER };
int[] to = { R.id.grid_item_text };
adapter = new SimpleCursorAdapter(this, R.layout.grid_item, NO_CURSOR, from, to, NO_FLAGS);
/*...*/
}
Povedzme si to rovno: prístup k dátam content loadera je drahý a pomalý. Samozrejme, nie taký pomalý, aby sme ho museli úplne zavrhnúť, ale predsa dosť pomalý na to, aby jeho nesprávne používanie viedlo k pomalému a nepoužiteľnému používateľskému rozhraniu.
Celé používateľské rozhranie Androidu totiž beží v jednom vlákne. Ak pracujeme s UI, každá naša aktivita (ťapnutie prstom, posun, písanie po klávesnici) vyvoláva vždy novú udalosť, ktorá sa postupne zaraďuje do frontu udalostí. Vlákno používateľského rozhrania vyberá jednotlivé udalosti zo začiatku frontu, spracováva ich a postupne prekresľuje používateľské rozhranie. Ak sa však vo fronte zjaví udalosť, ktorá trvá veľmi dlho, prekresľovanie prestane byť plynulé a používateľ znervóznie a nadobudne pocit pomalosti, ktorý následne vyventiluje na fórach v príspevkoch s textom "Android je pomalý".
Keďže databázových aktivít je ako maku, v Androide 4.x sa objavilo API v podobe loaderov, ktoré tento problém riešia spoľahlivým spôsobom.
Loader je objekt, ktorý načítava ľubovoľné dáta, typicky kurzory, v separátnom vlákne na pozadí. Načítané dáta dokáže preklopiť do vlákna používateľského rozhrania, odkiaľ sa dajú zobraziť v jednotlivých komponentoch.
Správne implementovaný loader dodržiava životný cyklus aktivity, korektne spravuje kurzory a uvoľnuje ich, ak ich už netreba a zároveň spoľahlivým spôsobo upozorňuje adaptéry na zmenené či nové dáta, a tým garantuje správne prekresľovanie widgetov.
CursorLoader
je loader, ktorá dokáže načítať dáta v podobe Cursor
a z content providera. Na načítavanie
mu stačí Uri
adresa content providera.
Kurzorový loader je najčastejšie používaný loader: nielen v prípade content providerov, ale i v prípade, že budeme v budúcnosti budovať databázové aplikácie.
Už vieme, čo je content provider, vieme (aspoň teoreticky) využívať kurzory, všeobecne vieme o užitočnosti loaderov, a teraz je čas to prepojiť.
Plán práce bude nasledovný:
Jedna aktivita môže načítavať dáta z viacerých loaderov: nie je vylúčené, že vyťahujete dáta z content providera pre kontakty, a zároveň potrebujete i dáta z histórie volaní.
Loader Manager je objekt, ktorý je prostredníkom medzi aktivitou a loadermi, ktoré sa v nej majú využívať.
Konkrétny loader inicializujeme v aktivite prostredníctvom loader managera, a to typicky v jej metóde onCreate()
:
getLoaderManager().initLoader(LOADER_ID_GRIDVIEW, NO_BUNDLE, this);
Inicializácia potrebuje tri parametre:
public static final int LOADER_ID_GRIDVIEW = 0
. Použitie konštánt nám sprehľadní kód.null
. (Je užitočné si definovať konštantu NO_BUNDLE
, ktorá sprehľadní čítanie.)Každý loader vie odosielať správy (udalosti) o svojom stave, na ktoré môže reagovať ľubovoľný poslucháč. Tieto udalosti sú tri:
V praxi sa poslucháčom stane aktivita, ak implementuje interfejs LoaderCallbacks, a dodá kód do jeho troch metód, ktoré zodpovedajú trom udalostiam.
onCreateLoader()
onLoaderFinished()
onLoaderReset()
. Ak to naplánujeme ešte podrobnejšie, tak:
LoaderCallbacks
onCreateLoader()
vytvoríme kurzorový loader CursorLoader
nasmerovaný na Uri
content providera histórie volaníonLoaderFinished()
dostaneme kurzor Cursor
s novými dátami, ktorý priradíme do adaptéra mriežkyonLoaderReset()
odpojíme kurzor od adaptéra mriežky. Nechajme našu aktivitu implementovať interfejs LoaderCallbacks
:
public class MainActivity extends ActionBarActivity implements LoaderManager.LoaderCallbacks<Cursor>
Generický typ Cursor
znamená, že chceme pracovať s kurzorovými loadermi.
V metóde onCreate()
inicializujeme loader prostredníctvom loader managera:
@Override
protected void onCreate(Bundle savedInstanceState) {
...
getLoaderManager().initLoader(LOADER_ID_GRIDVIEW, NO_BUNDLE, this);
}
Doimplementujme kód do troch metód (spravíme to hneď) a prepojme získané dáta s mriežkou cez adaptér.
onCreateLoader()
¶Metóda onCreateLoader()
je zavolaná loader managerom vo chvíli, keď má vzniknúť nový loader. Keďže v našom prípade využívame content providery, veľmi sa hodí CursorLoader
, ktorý využijeme nasledovne:
@Override
public Loader onCreateLoader(int id, Bundle args) {
if(id == LOADER_ID_GRIDVIEW) {
CursorLoader loader = new CursorLoader(this);
loader.setUri(CallLog.Calls.CONTENT_URI);
return loader;
}
return null;
}
Metóda má dva parametre: id
reprezentujúci identifikátor loadera, ktorý sa má vytvoriť (ide o rovnaký identifikátor, aký bol uvedený v metóde initLoader()
) a voliteľný bundle. Pomocou if
-u zistíme, ktorý loader je potrebné vytvoriť.
V CursorLoader
i definujeme URI
adresu content providera a príslušnej tabuľky (teda jeho content URI). Ak nenastavíme na loaderi žiadne iné vlastnosti, znamená to, že chceme načítať celú históriu volaní.
Ak sa kurzorovému loaderu podarí na pozadí načítať dáta, zavolá sa metóda onLoadFinished()
, a dáta sa ocitnú v druhom argumente. U nás načítavame kurzory, čo znamená, že dostaneme k dispozícii Cursor
.
Čo s máme spraviť s dátami? Potrebujeme ich zobraziť v mriežke (ešte sme nezabudli na mriežku?). A ako ich dopravíme do mriežky? Cez adaptér!
Adaptér SimpleCursorAdapter
máme definovaný v inštančnej premennej adapter
, kurzor s dátami dostaneme v parametri metódy, a prepojíme ich volaním metódy swapCursor()
.
@Override
public void onLoadFinished(Loader loader, Cursor cursor) {
adapter.swapCursor(cursor);
}
Ak náhodou prestanú byť dáta v kurzore aktuálne (napr. sa zmenili dáta v content provideri), alebo je loader odsúdený na zánik, potrebujeme zrušiť prepojenie medzi nimi a adaptérom. Obvyklé riešenie je prosté: zavoláme metódu swapCursor()
do ktorej dodáme null
. Samozrejme, opäť kvôli prehľadnosti zrecyklujeme konštantu pre "žiadny kurzor".
@Override
public void onLoaderReset(Loader loader) {
adapter.swapCursor(NO_CURSOR);
}
Ak by sme spustili aplikáciu už teraz, uvideli by sme Force Close, teda pád ihneď po štarte, a v logcat
e.
Naša aplikácia totiž pristupuje k citlivým údajom. (Chceli by ste mať svoju históriu údajov odosielanú na pochybný server?).
Aplikácie v Androide majú veľký potenciál, ale to v sebe nesie aj bezpečnostné riziká. Telefonovanie na thajské čísla na pozadí, odosielanie SMSiek bez súhlasu používateľa, využívanie internetu alebo fotoaparátu.
Riešenie tohto problému spočíva v definícii systému oprávnení (permissions). Android ako platforma definuje zoznam potenciálnych oprávnení, a každá z aplikácii určí, ktoré oprávnenia potrebuje využívať.
Naša aplikácia potrebuje čítať z histórie volaní, čomu zodpovedá oprávnenie android.permission.READ_CONTACTS
(resp. v novších verziách Androidu android.permission.CALL_PHONE
). V budúcich fázach aplikácie budeme tiež potrebovať telefonovať, a teda žiadať o oprávnenie android.permission.CALL_PHONE
.
Oprávnenia sa definujú v manifeste. Uvedené požiadavky uvedieme do elementu <manifest>
v elementoch <uses-permissions>
<uses-permission android:name="android.permission.READ_CONTACTS"/>
<uses-permission android:name="android.permission.CALL_PHONE"/>
Požadované oprávnenia sa zobrazia používateľovi pri inštalovaní aplikácie z trhu aplikácií Google Play (v emulátore je táto možnosť potlačená).
Žiaľ, zodpovednosť za bezpečnosť je ponechaná na používateľa. Ak používateľ nesúhlasí s ktorýmkoľvek z oprávnení, aplikácia sa vôbec nenainštaluje. Nie je možné odoprieť konkrétne oprávnenia -- teda ak používateľ chce hrať pochybný Tetris vyžadujúci telefonovanie, nemôže mu odoprieť právo telefonovať a ponechať si ostatné oprávnenia.
Typický používateľ tak často "odklikne" oprávnenia len preto, aby sa aplikácia nainštalovala.
Teraz, keď sa tešíme z aplikácie, ktorá zobrazuje dáta z content providera, dajme si pauzu a po nej doimplementujme obsluhu kliku na položku.
Ak chceme reagovať na kliknutia na položky, stačí na objekte GridView
zaregistrovať poslucháča typu AdapterView.OnItemClickListener
a to pomocou metódy setOnItemClickListener()
.
Keďže naša aktivita bude obsahovať len jediný komponent, môžeme za poslucháča vyhlásiť práve ju, čím sa zbavíme nutnosti používať anonymnú vnútornú triedu. Nechajme aktivitu implementovať zmienený interfejs:
public class MainActivity extends ActionBarActivity
implements LoaderManager.LoaderCallbacks<Cursor>,
AdapterView.OnItemClickListener {
...
a dodajme kód pre jedinú metódu z tohto interfejsu:
@Override
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
}
V rámci tejto metódy potrebujeme zistiť, na ktorú položku sme klikli, vytiahnuť z nej telefónne číslo a niečo pekné s ním spraviť.
Pri práci s poľovým adaptérom sme využívali argument position
, ktorý nám dokázal vrátiť príslušnú položku na základe jej poradia v zdrojovom zozname. V tomto prípade však berieme dáta z content providera, ktoré nemajú konkrétny definovaný typ (dáta zodpovedajú "riadkom" z "tabuliek" content providera).
Našťastie máme k dispozícii parameter long id
, ktorý obsahuje primárny kľúč, čiže jednoznačný identifikátor vybraného riadka v content providerovi. Cez tento primárny kľúč sa vieme content providera spýtať na aktuálnu položku, z ktorej zistíme všetko, čo potrebujeme (primárne vybrané telefónne číslo).
Na získanie konkrétneho riadka využijeme priamy prístup ku content provideru. Dosiahneme ho však cez prostredníka, a to triedu content resolver.
Content Resolver je trieda, ktorá umožňuje pristupovať k údajom v content providerom. Poskytuje metódy pre dopytovanie, ktoré vracajú Cursor
, ale i vkladanie, či mazanie dát. Z implementačného hľadiska je to továreň pre získavanie content providerov.
Ku content resolveru sa v aktivite dostaneme cez metódu getContentResolver()
. Keďže chceme získavať dáta z content providera, využijeme metódu query()
, kde uvedieme parametre zodpovedajúce SQL dopytu.
Metóda query()
má mnoho parametrov:
CONTENT_URI
. Zodpovedá klauzule FROM [názov tabuľky]
.SELECT ___*
. Ak uvedieme null
, získajú sa všetky stĺpce.WHERE
,WHERE
podmienku,ORDER BY
.Výsledkom metódy query()
je kurzor.
Ak chceme pristúpiť ku konkrétnemu riadku z histórie volaní, využime:
Cursor cursor = getContentResolver().query(
CallLog.Calls.CONTENT_URI,
null, /* všetky stĺpce */
CallLog.Calls._ID = " + id,
null /* žiadne parametre selekcie */,
null) /* žiadne triedenie */;
Podmienka sa odkazuje na primárny kľúč definovaný konštantou _ID
.
Kurzor získaný z metódy query()
by mal vo výsledku obsahovať jediný riadok, prislúchajúci telefónnemu číslu vybranému v mriežke. Vyššie sme vraveli, že kurzor reprezentuje nielen výsledok, ale aj odkaz smerujúci na konkrétny riadok, ktorý môžeme posúvať smerom ku koncu tabuľkových dát.
Metóda moveToNext()
dokáže posúvať kurzor na ďalší riadok, a vráti true
, ak taký ďalší riadok existuje. Ak sa už žiadny záznam vo výsledku nenachádza, vráti prirodzene false
.
Na začiatku je kurzor nasmerovaný pred ktorýkoľvek riadok, a prvé volanie tejto metódy nám dokáže zistiť, či vôbec nejaké dáta existujú vo výsledku.
Dáta z riadku, na ktorý ukazuje kurzor, získame pomocou sady metód getXXX()
metódy, kde XXX
je dátový typ. Príkladom je getString()
, ktorá vráti konkrétnu hodnotu zo zadaného stĺpca ako reťazec. Pozor však! Parameter týchto metód nie je názov stĺpca, ale jeho index!
Aby sme nemuseli ručne zisťovať mapovanie medzi stĺpcami a indexami, využijeme pomocnú metódu cursor.getColumnIndex(*meno*)
, ktorá nám to povie.
Posledná dôležitá zásada súvisí s uvoľnovaním prostriedkov: po skončení práce s kurzorom ho nezabudneme zatvoriť! Inak nám vyfiltrované dáta budú zbytočne zaberať miesto v pamäti.
Ukážme si situáciu, keď vybranú položku zobrazíme v toaste:
@Override
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
Cursor cursor = getContentResolver().query(CallLog.Calls.CONTENT_URI, null, CallLog.Calls._ID + "=" + id, null, null);
if(cursor.moveToNext()) {
String phoneNumber = cursor.getString(cursor.getColumnIndex(CallLog.Calls.NUMBER));
Toast.makeText(this, phoneNumber, Toast.LENGTH_SHORT).show();
}
cursor.close();
}
Ak chceme po kliknutí na telefónne číslo začať rovno vytáčať hovor, využijeme na to intent, ktorým oznámime úmysel telefonovať. V tomto prípade však nemôžeme uviesť názov triedy aktivity, ktorá uskutoční hovor, keďže ho nepoznáme. Namiesto toho môžeme uviesť v konštruktore intentu názov akcie, v tomto prípade ACTION_CALL
, ktorá hovorí o telefonovaní, a Android sa sám postará o vytočenie hovoru.
Samozrejme, musíme tiež vedieť, aké číslo mienime vytočiť. Realizujeme to cez dáta intentu, a v súlade s dokumentáciou číslo zareprezentujeme ako URI v tvare tel:______
.
Intent callIntent = new Intent(Intent.ACTION_CALL);
callIntent.setData(Uri.parse(WebView.SCHEME_TEL + number));
startActivity(callIntent);
Všimnime si aktualizáciu dát, ak nasimulujeme telefonát, ktorý zavesíme, po návrate našej aktivity na popredie sa mriežka okamžite aktualizuje. Content provider totiž umožňuje registráciu poslucháčov na zmeny a SimpleCursorAdapter
dokáže byť takým poslucháčom a pri zmenách aktualizovať mriežku.
Doposiaľ sme používali pre jednotlivé "bunky" mriežky zabudovaný systémový layout android.R.layout.simple_list_item_1
, kde text bol namapovaný na TextView
s identifikátorom android.R.id_text1
.
Ak chceme používať farebné a väčšie bunky, definujme si vlastný layout, v adresári layout
a nazvime ho grid_item.xml
.
Kliknime pravým myšidlom na adresár layout
v projekte, z podmenu New zvoľme Layout Resource File a nazvime ho grid_item.xml
.
res/layout/grid_item.xml
¶<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="https://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical" >
<TextView
android:id="@+id/grid_item_text"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:height="90dp"
android:background="@android:color/black"
android:textColor="@android:color/white"
android:gravity="center"
/>
</LinearLayout>
Zvolíme lineárny layout (aj keď to nie je až také podstatné, lebo využijeme len jeden komponent) a vložme doň jeden TextView
, ktorému zároveň pridelíme identifikátor, aby sme sa naň vedeli odkázať v SimpleCursorAdapter
i.
Výšku a šírku v layoute nastavíme podľa rodiča, ale výšku nastavíme napevno na 90 dp
, čím zachováme štvorcovitosť bunky (šírka 90 dp
je nastavená v layoute aktivity, v šírke stĺpca columnWidth
). Aby sme dali textu v bunke trochu vzdušnosti, nastavíme vhodný padding (vzdialenosť medzi obsahom a okrajom komponentu) a celý text v bunke vycentrujeme cez gravity
(pozor, nemýliť si s layout_gravity
!).
Okrem toho ešte nastavíme biely text a čierne pozadie, a to cez odkazy na zabudované farby Androidu.
Upravme potom inicializáciu kurzorového adaptéra. V konštruktore už budeme využívať layout definovaný v našom novom súbore a pole to
bude obsahovať identifikátor textového políčka z nášho layoutu.
int[] to = { R.id.grid_item_text } ;
adapter = new SimpleCursorAdapter(this, R.layout.grid_item, NO_CURSOR, from, to);
Ak teraz spustíme aplikáciu, je možné, že uvidíme podivnú mriežku s čiernymi bunkami na bielom pozadí. Súvisí to so systémovou farebnou schémou, ktorá nastaví pozadie aktivity na biele. Tento estetický nedostatok odstránime na konci tutoriálu.
Poďme teraz definovať rozličné farby pre bunky: prichádzajúce hovory nech sú zelené, odchádzajúce červené a zmeškané modré.
Definícia farieb sa môže diať buď v kóde, alebo v resources. Vytvorme si nový resources súbor (File | New | XML | Values XML File) a nazvime ho color.xml
.
V adresári values
vznikne nový súbor color.xml
.
Uvedieme doňho 6 farieb, kde v dvojici nastavíme pozadie i text:
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="incomingCallBackground">#99CC00</color>
<color name="incomingCallForeground">#FFFFFF</color>
<color name="outgoingCallBackground">#33B5E5</color>
<color name="outgoingCallForeground">#FFFFFF</color>
<color name="missedCallBackground">#FF4444</color>
<color name="missedCallForeground">#FFFFFF</color>
</resources>
SimpleCursorAdapter.ViewBinder
¶Ako prepojíme farby s mriežkou GridView
? Cez view binder.
Prispôsobenie vykresľovania dát, či úprava dát z adaptéra pred ich zobrazením v komponentoch je v prípade SimpleCursorAdaptéra
možná cez tzv. view binder
Interfejs SimpleCursorAdapter.ViewBinder
má jednu metódu:
public boolean setViewValue(View view, Cursor cursor, int columnIndex)
Tá sa volá toľkokrát, koľko je záznamov v kurzore, čo zodpovedá počtu buniek v mriežke. Pri každom volaní dostaneme v parametri view
, teda widget, na ktorý sa má namapovať príslušná hodnota zo stĺpca tabuľky. Druhý parameter je kurzor bude nastavený na príslušný záznam a columnIndex
obsahuje index stĺpca v tabuľke, z ktorého máme namapovať dáta na widget.
Metóda má vrátiť true
, ak úspešne vykonala mapovanie medzi dátami z tabuľkami. Ak vráti false
, indikuje tým kurzorovému adaptéru, že má použiť vlastné štandardné správanie, ktoré sme videli v predošlom dieli.
Celý kód pre náš príklad vyzerá:
public class CallLogViewBinder implements SimpleCursorAdapter.ViewBinder {
@Override
public boolean setViewValue(View view, Cursor cursor, int columnIndex) {
if(view instanceof TextView) {
TextView textView = (TextView) view;
int type = cursor.getInt(cursor.getColumnIndex(CallLog.Calls.TYPE));
switch(type) {
case CallLog.Calls.INCOMING_TYPE:
textView.setBackgroundColor(
textView.getResources()
.getColor(R.color.incomingCallBackground)
);
break;
case CallLog.Calls.OUTGOING_TYPE:
textView.setBackgroundColor(
textView.getResources()
.getColor(R.color.outgoingCallBackground)
);
break;
case CallLog.Calls.MISSED_TYPE:
textView.setBackgroundColor(
textView.getResources()
.getColor(R.color.missedCallBackground)
);
break;
}
}
return false;
}
}
Predovšetkým, musíme zistiť, či je komponent vo view
správneho typu. Keďže kurzorový adaptér pracuje nad layoutom z grid_item.xml
, kde máme jediný komponent TextView
, sme povinní sa spýtať, či ideme naozaj prispôsobovať správny komponent.
Následne zistíme typ hovoru, zo stĺpca CallLog.Calls.TYPE
.
int type = cursor.getInt(cursor.getColumnIndex(CallLog.Calls.TYPE));
Veľký switch
odlíši konkrétny typ hovoru, a ako nastavíme farbu? Najprv musíme získať odkaz na definičný súbor s farbami. Z komponentu view
získame tento objekt:
Resources resources = view.getResources();
Následne získame z tohto objektu farbu:
int incomingCallBackgroundColor = resources.getColor(R.color.incomingCallBackground)
A túto farbu nastavíme na komponente (napríklad pre farbu prichádzajúceho hovoru):
textView.setBackgroundColor(resources.getColor(R.color.incomingCallBackground));
Pozor! V tomto prípade sa používa int
v dvoch významoch: jednak ako identifikátor konkrétneho resourcu ale jednak ako reprezentácia namiešanej farby. Prevody z resources nás zbavia nečakaných farebných prekvapení.
Metóda je povinná vrátiť true
, ak sme prispôsobovali či upravovali dáta mapované na komponent (môže sa to stať napríklad v prípade dátumu hovoru, ktorý musíme predspracovať). V tomto prípade sa žiadna úprava dát nediala a preto vrátime false
, čo znamená, že sa spoľahneme na štandardné správanie, ktoré využíva toString()
na hodnotách z kurzora.
View binder prepojíme s adaptérom v metóde onCreate()
v aktivite:
adapter.setViewBinder(new CallLogViewBinder());
V manifeste nastavíme tému aplikácie v elemente <application>
a v atribúte theme
. Ak nastavíme tému Theme.AppCompat.NoActionBar
, využijeme tmavé pozadie a zároveň schováme lištu akcií, ktoré je v tejto aplikácii zbytočná.
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:theme="@style/Theme.AppCompat.NoActionBar" >
Zdrojové kódy výslednej aplikácie sú dostupné na Githube v repe novotnyr/android-callgrid-2015
.