180 minút s Androidom: 5. stretnutie [história volaní v mriežke]

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:

callgrid2

Ú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 ako ListView, ú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ĺpca columnWidth, 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 na LinearLayoute a vypnime listSelector (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ýva onItemClick(), 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

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ádzame uses-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:

callgrid1

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

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
  • 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
  • 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 nullový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

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 z CursorAdaptera)
    • miesto toho: spýtajme sa content providera!
  • 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
  • kurzor má vrátiť jeden riadok

  • ak moveToNext() vráti true, 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, kde XXX 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ý, z android.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ť v SimpleCursorAdapteri.
  • šírka a výška match_parent (podľa rodiča)
  • výšku nastavíme napevno: 90 dp (dp = pixel nezávislý od hustoty displeja)
  • nastavíme vyšší padding hore i dole (v duchu box modelu v CSS)
  • centrujeme text cez gravity (pozor, nemýliť si s layout_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átora TextViewu 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

Pridaj komentár

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