Ukážková aplikácia
Vytvorme aplikáciu, ktorá zobrazí históriu volaní v utešenej mriežke a la Windows Phone.
Koncepty, ktoré zvládneme
- existujúce content providery
- korektná práca s kurzormi na pozadí cez loadery
- mriežka položiek
GridView
- vlastný layout položiek cez
ViewBinder
- farby ako resources
- využitie podpornej knižnice support library
Výsledná aplikácia
- viď zdrojáky na GitHube
- výzor:
Úvod
- vytvorme nový projekt CallGrid, ktorý kompilujme oproti 2.3.x
- s pomocou podpornej knižnice vieme využívať veci z budúcnosti, teda z Androidu 4.X
Zobrazovanie údajov v mriežke
- k dispozícii je
GridLayout
, ale až od API Level 14 (Android 4.0) - jednoduchšie je použiť
GridView
. Má veľmi podobné API akoListView
, údaje však kreslí do mriežky (a nie pod seba).
Level 1: GridView
nad pevnými dátami a implicitným formátom
Layout
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
tools:context=".MainActivity" >
<GridView
android:id="@+id/callLogGridView"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:numColumns="auto_fit"
android:stretchMode="columnWidth"
android:columnWidth="90dp"
android:listSelector="@null"
android:horizontalSpacing="5dp"
android:verticalSpacing="5dp"
/>
</LinearLayout>
- môžeme nastaviť dynamický počet stĺpcov:
numColumns
rovnýauto_fit
. Ak nastavíme šírku stĺpcacolumnWidth
, pri otáčaní na šírku budú pribúdať nové stĺpce. - ak sa chceme zbaviť okraja (paddingu) okolo gridviewu, vymažme všetky
android:padding
naLinearLayoute
a vypnimelistSelector
(nastavme ho na@null
)
Java
package sk.upjs.ics.android.callgrid;
import android.content.Intent;
import android.net.Uri;
import android.os.Bundle;
import android.support.v4.app.FragmentActivity;
import android.view.View;
import android.widget.AdapterView;
import android.widget.AdapterView.OnItemClickListener;
import android.widget.ArrayAdapter;
import android.widget.GridView;
import static android.webkit.WebView.SCHEME_TEL;;
public class MainActivity extends Activity implements OnItemClickListener {
private ArrayAdapter<String> adapter;
private GridView callLogGridView;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
String[] adapterData = { "0905223223", "0905123456", "112", "055123456" };
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);
}
@Override
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
String phoneNumber = (String) adapter.getItem(position);
Intent callIntent = new Intent(Intent.ACTION_CALL, Uri.parse(SCHEME_TEL + phoneNumber));
startActivity(callIntent);
}
}
- trieda aktivity implementuje
OnItemClickListener
, a prekrývaonItemClick()
, kde priamo z adaptéra vyťahuje objekt - telefonovanie vyriešime:
- vytvorením intentu s URI
Intent.ACTION_CALL
- dátami je adresa v tvare
tel:0905123456
- spustením aktivity
- vytvorením intentu s URI
Oprávnenia
- aplikácia môže potenciálne robiť záškodnícku činnosť
- posielať potichu SMSky do Thajska
- definuje sa systém oprávnení permissions
- do elementu
<manifest>
uvádzameuses-permissions
príklad: aplikácia chce čítať kontakty a telefonovať
<uses-permission android:name="android.permission.READ_CONTACTS"/> <uses-permission android:name="android.permission.CALL_PHONE"/>
ak aplikácia chce činiť akcie, na ktoré nemá oprávnenia, spadne na
Permission Denied
Oprávnenia v Androide sú borknuté
- používateľ môže len odsúhlasiť všetky požiadavky na oprávnenia
- ak nesúhlasí s ktorýmkoľvek, aplikácia sa nenainštaluje vôbec
- ak mám Tetris, čo chce telefonovať, nemôžem mu ako používateľ odoprieť právu telefónu
Výsledná aplikácia
Zatiaľ vyzeráme škaredo:
Level 1: GridView
nad dátami z content providera a implicitným formátom
Support Library
- využime podpornú knižnicu support library
- získame možnosti niektorých API z Android 4.x aj pre staršie telefóny bežiace nad Android 2.x.
- v Eclipse v projekte by v Android Private Libraries mala byť
android-support-v4.jar
Content Provider
- množstvo aplikácií potrebuje sprístupniť svoje dáta – kontakty – história volaní – fotky v galérii – …
- dáta môžu byť na rozličných miestach – v SQLite databáze – v súborovom systéme – …
Content Providers: cesta ako sprístupniť dáta efektívnou cestou bez ohľadu na spôsob uloženia
dáta sú chápané tabuľkovo-relačným štýlom
- tabuľky
- stĺpce
- riadky
- každý content provider hovorí, aké ,,tabuľky” chce sprístupniť a aké ,,stĺpce” ponúka
- content provider v systéme je jednoznačne identifikovaný URI adresou
- preddefinovaní provideri sú v balíčku android.provider
- k dátam sa následne pristupuje pomocou kurzora
Príklad: história volaní
- trieda
android.provider.CallLog
- obsahuje jedinú “tabuľkovú” podtriedu
CallLog.Calls
- URI adresa je v konštante
CallLog.Calls.CONTENT_URI
- v nej sú názvy stĺpcov ako inštančné premenné, ktorých dátové typy sú v dokumentácii
- URI adresa je v konštante
Loadery
- prístup k databáze je pomalý
- ak beží z hlavného vlákna okna, UI sa môže ťahať ako lekvár
- “ANDROID JE POMALÝ!!!!!”
- rovnaký problém ako vo Swingu a všetkých ostatných GUI frameworkoch
- riešenie vo Swingu:
SwingWorker
- riešenie vo Swingu:
- loader: mechanizmus pre prácu s kurzormi / dátami v separátnom vlákne na pozadí
- dlhotrvajúca operácia beží na pozadí
- korektne sa prehadzujú dáta do hlavného vlákna
- nie je treba zložitú synchronizáciu vlákien
- správne implementovaný loader dodržiava životný cyklus aktivity a správne spravuje kurzory a uvoľnuje ich, ak ich už netreba
- zároveň správne notifikuje adaptéry a widgety sa tak korektne prekresľujú pri zmenách
Zdroje
Implementácia Loadera
- aktivita implementuje
LoaderCallbacks
- implementujeme
LoaderCallbacks<Cursor>
, keďže načítavame kurzory
- implementujeme
- bude notifikovaná o priebehu dlhotrvajúcej operácie:
onCreateLoader()
: na začiatku vytvoríme loader- obvykle sa kontaktuje content provider
onLoadFinished()
: načítavanie skončilo- obvykle sa asociuje kurzor z loadera s adaptérom
onLoaderReset()
: načítavanie skončilo- ak sa predošlý loader zrušil, a dáta sú zneplatnené.
- obvykle sa zruší kurzor z adaptéra
onCreateLoader()
- využijeme zabudovaný
CursorLoader
- zistíme URI pre content provider histórie volaní
- voliteľne vyplníme selekcie, projektcie, …
- parameter
loaderId
určuje, ktorý loader sa má v metóde inicializovať- loaderov môže byť v aktivite viacero
- sú odlíšené číselnými kódmi, ktoré definujeme sami
Kód:
@Override
public Loader<Cursor> onCreateLoader(int loaderId, Bundle bundle) {
if(loaderId == LOADER_ID_GRIDVIEW) {
CursorLoader cursorLoader = new CursorLoader(this);
cursorLoader.setUri(CallLog.Calls.CONTENT_URI);
cursorLoader.setSortOrder(CallLog.Calls.DATE + " DESC");
return cursorLoader;
}
return null;
}
Adaptér
- využijeme
CursorAdapter
s klasickým layoutom - použime ho ako inštančnú premennú aktivity, musíme ho vidieť z loadercallbackov
- na začiatku ho stvoríme s
null
ovým kurzorom
onLoadFinished()
nastavíme čersvý kurzor do adaptéra
@Override public void onLoadFinished(Loader<Cursor> loader, Cursor cursor) { adapter.changeCursor(cursor); }
onLoaderReset()
nastavíme
nullový kurzor
adaptéru@Override public void onLoaderReset(Loader<Cursor> cursor) { adapter.changeCursor(NO_CURSOR); }
Inicializácia loadera
- ak využívame podpornú knižnicu, aktivita musí dediť od
android.support.v4.app.FragmentActivity
inicializujeme loader cez:
getSupportLoaderManager().initLoader(LOADER_ID_GRIDVIEW, NO_BUNDLE, this);
- loaderId: viď prvý parameter v
onCreateLoader()
- bundle: pre prenos dát medzi aktivitou a loadercallbackom. Keďže loadercallback je vnútorná trieda, bundle nepoužívame a môže byť
null
- loadercallback: inštancia triedy s callbackmi: je ňou naša aktivita
- loaderId: viď prvý parameter v
Inicializácia loadera (čisté Android 4.x)
- ak kašleme na staré Androidy, môžeme ísť cez čisté 4.x API
- nemusíme dediť,
Activity
už má príslušné metódy inicializujeme loader cez:
getLoaderManager().initLoader(LOADER_ID_GRIDVIEW, NO_BUNDLE, this);
Zmena poslucháča na click na položku / Surové kurzory a content providery
- už máme normálny kurzorový adaptér: vidíme identifikátory položiek zo stĺpca
_id
. ako zistiť, ktorá položka bola vybraná?
- objekt rovno z adaptéra cez
getItem()
je divný (interný objekt zCursorAdaptera
) - miesto toho: spýtajme sa content providera!
- objekt rovno z adaptéra cez
Content Resolver: továreň pre získavanie content providerov
getContentResolver()
obsahuje štandardnú metódu
query()
, kde uvedieme názov tabuľky, selekcie, projekcie atď.- rovnako aj metódy pre vkladanie, mazanie a la databáza
dostaneme kurzor
Cursor cursor = getContentResolver().query(CallLog.Calls.CONTENT_URI, null, CallLog.Calls._ID + "=" + id, null, null);
- v tomto prípade sme postavili
WHERE
podmienku_id = ?
, kde?
je identifikátor vybranej položky
- v tomto prípade sme postavili
kurzor má vrátiť jeden riadok
- ak
moveToNext()
vrátitrue
, existuje riadok, v opačnom prípade sa záznam nenašiel (nie je sa posunúť za žiadny riadok) - z kurzora získame hodnoty stĺpcov cez
getXXX()
metódy, kdeXXX
je dátový typ - pozor, parameter je index stĺpcva
- ale index stĺpca podľa mena získame cez
cursor.getColumnIndex(*meno*)
- po skončení práce s kurzorom ho nezabudneme zatvoriť!
Kód
@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));
Intent callIntent = new Intent(Intent.ACTION_CALL, Uri.parse(SCHEME_TEL + phoneNumber));
startActivity(callIntent);
}
cursor.close();
}
Zdrojáky
package sk.upjs.ics.android.callgrid;
import static android.webkit.WebView.SCHEME_TEL;
import static sk.upjs.ics.android.callgrid.Constants.NO_BUNDLE;
import static sk.upjs.ics.android.callgrid.Constants.NO_CURSOR;
import android.content.Intent;
import android.database.Cursor;
import android.net.Uri;
import android.os.Bundle;
import android.provider.CallLog;
import android.support.v4.app.FragmentActivity;
import android.support.v4.app.LoaderManager.LoaderCallbacks;
import android.support.v4.content.CursorLoader;
import android.support.v4.content.Loader;
import android.view.View;
import android.widget.AdapterView;
import android.widget.AdapterView.OnItemClickListener;
import android.widget.GridView;
import android.widget.SimpleCursorAdapter;
public class MainActivity extends FragmentActivity implements LoaderCallbacks<Cursor>, OnItemClickListener {
private static final int LOADER_ID_GRIDVIEW = 0;
private SimpleCursorAdapter adapter;
private GridView callLogGridView;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
getSupportLoaderManager().initLoader(LOADER_ID_GRIDVIEW, NO_BUNDLE, this);
String[] from = { CallLog.Calls.NUMBER };
int[] to = { android.R.id.text1 } ;
adapter = new SimpleCursorAdapter(this, android.R.layout.simple_list_item_1, NO_CURSOR, from, to);
callLogGridView = (GridView) findViewById(R.id.callLogGridView);
callLogGridView.setAdapter(adapter);
callLogGridView.setOnItemClickListener(this);
}
@Override
public Loader<Cursor> onCreateLoader(int loaderId, Bundle bundle) {
if(loaderId == LOADER_ID_GRIDVIEW) {
CursorLoader cursorLoader = new CursorLoader(this);
cursorLoader.setUri(CallLog.Calls.CONTENT_URI);
cursorLoader.setSortOrder(CallLog.Calls.DATE + " DESC");
return cursorLoader;
}
return null;
}
@Override
public void onLoadFinished(Loader<Cursor> loader, Cursor cursor) {
adapter.changeCursor(cursor);
}
@Override
public void onLoaderReset(Loader<Cursor> cursor) {
adapter.changeCursor(NO_CURSOR);
}
@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));
Intent callIntent = new Intent(Intent.ACTION_CALL, Uri.parse(SCHEME_TEL + phoneNumber));
startActivity(callIntent);
}
cursor.close();
}
}
Level 3: vlastný layout položiek
- zatiaľ sme používali zabudovaný layout pre jednotlivé “bunky” položky
- každá bunka obsahuje jeden
TextView
(layout je systémový, zandroid.R.layout.simple_list_item_1
), ktorého identifikátor je systémovéandroid.R.id_text1
- ak chceme farebné a väčšie bunky, definujme vlastný layout:
Súbor res/layout/grid_item.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://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="match_parent"
android:layout_height="match_parent"
android:height="90dp"
android:background="@android:color/black"
android:paddingTop="20dp"
android:paddingBottom="20dp"
android:gravity="center"
/>
</LinearLayout>
- v bunke bude jeden
TextView
, pridelíme mu identifikátor, aby sme sa naň vedeli odkázať vSimpleCursorAdapter
i. - šírka a výška
match_parent
(podľa rodiča) - výšku nastavíme napevno: 90 dp (dp = pixel nezávislý od hustoty displeja)
- podrobnosti o merných jednotkách: http://stackoverflow.com/a/2025541
- zachováme tým štvorcovitosť bunky
- nastavíme vyšší padding hore i dole (v duchu box modelu v CSS)
- centrujeme text cez
gravity
(pozor, nemýliť si slayout_gravity
!)
Inicializácia adaptéra
int[] to = { R.id.grid_item_text } ;
adapter = new SimpleCursorAdapter(this, R.layout.grid_item, NO_CURSOR, from, to);
- identifikátor v
to
odvodíme od identifikátoraTextView
u v našom XML layoute - na layout sa odkážeme v konštruktore
Farby ako resource
- ak chceme definovať farby dynamicky, nie napevno v kóde, vieme ich definovať v resources
- vytvorme v Eclipse New Android XML Values File
- nech je typu color
nech vznikne súbor
res/values/color.xml
<?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>
Využitie farieb v GridView
- prispôsobenie výzoru položiek cez
ViewBinder
- viď separátny článok
- definujme vlastnú triedu pre viewbinder
- asociujme ju s gridviewom cez
setViewBinder()
Čo chceme?
- chceme rozličné farby pre rozličné typy hovorov v histórii
- z metódy stále vrátime
false
, lebo nerobíme skutočný binding hodnoty z kurzora na prvok, len meníme farby
Implementácia
- získame objekt pre
resources
z nich získame farbu na základe identifikátora, ktorý sme stanovili v
color.xml
Resources resources = view.getResources(); textView.setBackgroundColor(resources.getColor(R.color.incomingCallBackground));
pozor!
int
má viacero významov:- znamená identifikátor resourceu
- ale aj znamená farbu
- v moderných Eclipsoch dostaneme varovanie, ale treba dať pozor, aby sme nenastavovali podivnú farbu, ktorej číselná reprezentácia sa popletie s jej identifikátorom
Kód
private static class CallLogCellViewBinder implements 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));
Resources resources = view.getResources();
switch(type) {
case CallLog.Calls.INCOMING_TYPE:
textView.setBackgroundColor(resources.getColor(R.color.incomingCallBackground));
textView.setTextColor(resources.getColor(R.color.incomingCallForeground));
break;
case CallLog.Calls.OUTGOING_TYPE:
textView.setBackgroundColor(resources.getColor(R.color.outgoingCallBackground));
textView.setTextColor(resources.getColor(R.color.outgoingCallForeground));
break;
case CallLog.Calls.MISSED_TYPE:
textView.setBackgroundColor(resources.getColor(R.color.missedCallBackground));
textView.setTextColor(resources.getColor(R.color.missedCallForeground));
break;
}
}
return false;
}
}
Dolaďovačky
Ako sa zbaviť sivej lišty hore?
a zároveň:
Ako nastaviť čierne pozadie?
V manifeste nastavíme tému v elemente príslušnej aktivity:
<activity
android:name="sk.upjs.ics.android.callgrid.MainActivity"
android:theme="@android:style/Theme.Black.NoTitleBar"
Finálne slová
- všimnite si korektné prekresľovanie zoznamu v duchu aktivity
- ak kliknete na položku, spustí sa telefón (dialer), a po skončení hovoru sa automaticky zjaví v zozname novo vykonaný hovor (ako odchádzajúci)
Ostatné zdroje
- http://ics.upjs.sk/~novotnyr/blog/1019/android-zobrazovanie-historie-volani-content-providery-kurzory-a-simplelistadapter
- http://ics.upjs.sk/~novotnyr/blog/1028/android-zobrazovanie-historie-volani-prisposobenie-vyzoru-poloziek-cez-viewbinder