Nine-patch Androidissa

Graafinen käyttöliittymä vaatii paljon kuvaresursseja, ja mikäli on tarkoitus tukea erilaisia Android-laitteita, joudutaan tekemään samoista kuvista eriresoluutioiset versiot. Työ vähenee huomattavasti, jos kuvat venyvät sopivaan kokoon.

Nine-patch tarjoaa mahdollisuuden venyttää kuvia järkevästi, eli se venyttää vain määriteltyjä osia kuvasta. Muu osa kuvasta pysyy sellaisena, kuin se on kuvassa, eikä veny. Venyviä alueita voi kuvassa olla useita ja niiden kokojen suhteet pysyvät aina samoina.

Nine-patch-kuvaan on myös mahdollista määritellä alue, johon sisältö tulee. Tämä on erityisen käytännöllistä tehtäessä grafiikoita säiliöihin, kuten nappeihin. Kun napin kuvasta tekee pienen ja asettaa sen kasvamaan sisällön mukaan ja kun sisältö on määritelty napin keskelle, grafiikka tulee automaattisesti oikean kokoiseksi.

Kuinka tehdä 9-patch kuvia

Nine-patch kuvat ovat normaaleja png-kuvia, joissa on yhden kuvapisteen paksuinen reuna joka on joko täysin läpinäkyvä tai valkoinen. Tälle reunalle piirretään yhden kuvapisteen levyistä mustaa viivaa ja kahden eri sivuilla olevan viivan määrittämä leikkaava alue rajaa venyvän ja sisällön täyttävän alueen. Android laitteella nämä kuvat on talletettava muotoon “.9.png”, jotta Android tietää kuvien olevan nine-patchejä.

Vasemmalla ja ylhäällä olevat mustat viivat rajaavat venyvän alueen. Oikealla ja alhaalla olevat viivat taas rajaavat kuvan sisällön alueen.

Kuvat voi tehdä millä tahansa kuvankäsittelyohjelmalla, mutta helpoin tapa on käyttää Android SDK:n mukana tulevaa työkalua “draw9patch”. Ohjelma löytyy “tools” kansiosta.

Ohjelma lisää kuvaan yhden kuvapisteen kokoiset reunat ja antaa muokata kuvaa vain näiden reunojen alueelta. Ohjelma myös näyttää oikeassa laidassa miltä kuva näyttää eri tavoilla venytettynä.

Avattuasi ohjelman voit raahata haluamasi kuvan ohjelmaan. Tämän jälkeen näet kuvan venytyksen tuloksen oikealla ja kuvan muokattavana vasemmalla. Kuvassa kannattaa valita tasaisia – usein vain yhden pikselin mittaisia – alueita venyväksi, koska tällöin kuvan väritys ei muutu. On myös tapauksia, missä on järkevää venyttää väritystä niin että isommilla ko’oilla väritys muuttuu tasaisesti.

Voit asettaa kuvassa editorissa venyvän alueen näkyväksi “show patches” valinnalla. Venyvät alueet ovat violetit alueet, jossa vihreät viivat kohtaavat. Samalla tavalla saat sisällön alueen näkyviin “show content” valinnalla.

Huomaa miten kuvassa olevat yksityiskohdat pysyvät tarkkoina, vaikka muu kuva venyy täyttämään koko näytön.

Viime viikon animaatioartikkelissa on käytetty tässä esimerkkinä olevaa nine-patch kuvaa. Nine-patch kuvien käyttö onnistuu applikaatiossa täysin samalla tavalla kuin normaalien kuvien. Android itse hoitaa kuvan koon ja sisällön asettelun.

Esimerkkikuva ilman nine-patchiä.

Esimerkkikuva nine-patchin jälkeen.

Animaatiot Androidissa

Helppo tapa tehdä applikaatiostasi hienompi on lisätä animaatioita. Käymme tässä läpi yksinkertaisia animaatioita ja niiden käyttämistä applikaatiossasi. Tässä esimerkissä keskitymme elementtien sijainnin muuttamiseen animaatioilla.

Animaatioita voi tehdä kahdella tavalla. Joko suoraan koodissa, tai määritellä erikseen xml:ssä. Jos animaatiossa ei tarvitse laskea suoritusaikana mitään tulisi ne tehdä xml:ssä, että koodi on selkeämpää.

Voit ladata esimerkin lähdekoodin täältä.

Animaatio koodissa

Animaation luominen koodissa on hyvin suoraviivaista. Uusi animaatio olio luodaan halutunlaisesta animaatiotyypistä. Tässä tapauksessa:

TranslateAnimation (int fromXType, float fromXValue,
                    int toXType, float toXValue,
                    int fromYType, float fromYValue,
                    int toYType, float toYValue)

Type attribuutit kertovat onko animoitava matka absoluuttinen vai suhteellinen. Suhteellinen matka voi riippua joko itse animoitavasta näkymästä, tai sen vanhemmasta:

Animation.ABSOLUTE
Animation.RELATIVE_TO_SELF
Animation.RELATIVE_TO_PARENT

Koodissa olen käyttänyt absoluuttista siirtymätyyppiä, sillä haluan animoida valikon liikkumisen vain piiloon jäävältä osiolta, jonka olen asettanut 100dp:ksi. Huomaa että koodissa täytyy ottaa huomioon dp:t, eli näytön tiheydestä riippumattomat pikselit. Näytön tiheyden saat:

float scale = getResources().getDisplayMetrics().density;

Koska absoluuttisia siirtoja ei voi ilmoittaa dp:nä on annetut dp:t kerrottava näytön tiheydellä, jotta saat dp:n arvon pikseleinä.

Animaatiolle tulee asettaa kesto, minkä aikana animaatio suoriutuu loppuun. Kesto annetaan millisekuntteina.

animation.setDuration(500);

Animaation loputtua liikutettu näkymä ei suoraan jää sille asetettuun paikkaan, vaan sen paikka täytyy asettaa koodissa ja vasta, kun animaatio on päättynyt. Muuten kuva hyppää asettamaasi paikkaan suoraan.

Huomaa:

Huomaa, että animaatio lähtee aina animoidun näkymän sen hetkisestä asemasta. Aseman pystyy muuttamaan myös automaattisesti:

animation.setFillEnabled(true);
animation.setFillAfter(true);

Tätä tapaa voi käyttää sekä koodissa, että xml:ssä, mutta jos kuvasi reagoi painalluksiin sen oikea paikka on edelleen siellä, missä se on xml layoutissa asetettu. Eli kuvan tai napin paikka on oikeasti eri, kuin missä se näyttäisi olevan. Tästä syystä tätä tapaa ei voi käyttää tässä esimerkissä. Jos esimerkiksi valikossa olisi nappula se kyllä tulisi näkyviin, mutta sen painalluksen tunnistava osuus olisi edelleen näytön ulkopuolella, eikä nappulaa näin voisi painaa.

Lopuksi animaatio käynnistetään sille näkymälle, mitä halutaan liikuttaa:

slider.startAnimation(animation);

Animaatio XML:ssä

Animaatioiden määrittäminen XML:ssä on melko suoraviivaista. Tarvittavat ohjeet ja listat mahdollisista elementeistä löytyvät Googlen Android developer-sivustolta.

Animaatiot tallennetaan applikaatiosi resurssi kansioon res/anim/animaation_nimi.xml. Animaatio voi sisältää useita animaatioita, kuten liikkumista ja koon muutosta. Nämä animaatiot tulee asettaa <set>-elementin sisälle. Jos animaatiosi koostuu vain yhdestä osasta ei <set>-elementin käyttäminen ole pakollista. Yllä olevasta linkistä löytyy mahdolliset animaatiotyypit ja niiden parametrit.

Tähän esimerkkiin tein yksinkertaisen siirtymisanimaation, missä kuva tulee näytön vasemmasta laidasta sisään. Animaatiolle on asetettu kesto, lähtöpaikka ja lopullinen paikka. Tässä tapauksessa kuva on koko näytön kokoinen ja aloittaa -100%:sta ja siirtyy normaaliin paikkaansa. X-akselin negatiiviset arvot ovat ruudun vasemmalla puolella ja positiiviset oikealla puolella.

<?xml version="1.0" encoding="utf-8"?>
<translate xmlns:android="http://schemas.android.com/apk/res/android"
   android:interpolator="@android:anim/decelerate_interpolator"
   android:duration="500"
   android:fromXDelta="-100%"
   android:toXDelta="0"
   android:fillEnabled="true"
   android:fillAfter="true"
   />

Animaatio ladataan koodissa:

Animation slideFromLeft = AnimationUtils.loadAnimation(this, R.anim.slide_in_from_left);

Muista asettaa animoitu näkymä näkyväksi, ennen kuin aloitat animaation, jos näkymän näkyvyys on poissa. Tai jos viet näkymää pois ruudulta aseta näkymä pois animaation jälkeen. Muuten animaatio aloitetaan samalla tavalla, kuin koodiesimerkissä.

Vinkki

Mukava tapa sulkea valikoita, jotka eivät peitä koko näyttöä on tehdä tyhjän osan peittävä näkymä, jota painamalla valikko menee kiinni. Tässä esimerkissä teemme slidecontainer:ssä FrameLayoutin nimeltä hider, joka tekee saman asian, kuin välilehden painaminen. Kun valikko on ylhäällä piilotamme hiderin, niin ettei sitä pysty painamaan. Tämä lisää valikon käytettävyyttä.

Valikot olisi myös hyvä toteuttaa omina aktiviteetteinään, mutta tässä esimerkissä näin ei voi tehdä, sillä valikon alareuna on osittain näkyvissä koko ajan. Aktiviteetti voi olla vain osittain toisen päällä, mutta alempaa aktiviteettiä ei voi käyttää.

Android SQL-tietokannan käyttäminen

Jatkona viimeviikon tiedon tallentamiselle toteuttakaamme SQLite-tietokanta Androidissa.

Yksinkertaisuuden vuoksi luomme vain yhden taulun sisältävän tietokannan. Tietokantaan talletetaan tuotteita, joilla on nimi, määrä sekä kuvaus. Tämän artikkelin liitteenä on esimerkki ohjelma, jossa tietokantaa voi kokeilla.

Product
product_name
product_count
product_description


Lataa esimerkin lähdekoodi.

Jatkamme SQLiteOpenHelper-luokkaa omalla toteutuksellamme ja luomme taulut onCreate-metodissa. Taulujen sarakkeiden nimet voi kätevästi tallettaa finaaleina muuttujina erillisiin sisäluokkiin, jotta ne on määritelty vain yhdessä paikassa ja selkeästi jäsenneltyinä.

Taulujen luonti tapahtuu samoilla SQL-komennoilla, kuin normaalin tietokannan luonnissa. Komentojen kirjoittaminen koodissa vain on hiukan eri näköistä, jos sen tekee StringBuilderilla. Create table-lauseen voi antaa myös suoraan stringinä.

private static final class ProductTable {
    public static final String PRODUCT_NAME = "product_name";
    public static final String PRODUCT_COUNT = "product_count";
    public static final String PRODUCT_DESCRIPTION = "product_description";
}
@Override
public void onCreate(SQLiteDatabase db) {
    StringBuilder sql = new StringBuilder();

    sql.append("CREATE TABLE ").append(PRODUCT_TABLE_NAME).append(" (");

    sql.append(ProductTable.PRODUCT_NAME).append(" TEXT,");
    sql.append(ProductTable.PRODUCT_COUNT).append(" INTEGER,");
    sql.append(ProductTable.PRODUCT_DESCRIPTION).append(" TEXT,");
    sql.append("PRIMARY KEY(").append(ProductTable.PRODUCT_NAME).append(")");
    sql.append(")");

    db.execSQL(sql.toString());
}

Tiedon tallentaminen tietokantaan

Tieto tallennetaan tietokantaan SQLiteDatabase-luokan instanssilla, joka saadaan SQLiteOpenHelper:ltä perityllä getWritableDatabase()-metodilla. Näin saatuun tietokantaolioon voi suorittaa kyselyitä tai lisäyksiä.

Lisättävät arvot on asetettava ContentValues-luokan ilmentymällä, joka pitää sisällään avain-arvo pareja, jossa avaimet ovat tietokannan sarakkeen nimiä ja arvo tietokantariville tulevan sarakkeen arvo. Jokaiselle tietokantariville täytyy luoda oma ContentValues-joukkonsa.

Hyvän ohjelmointitavan mukaista on, että tietokantametodit palauttavat boolean arvon riippuen onnistuiko metodi vai ei. Tietokantakyselyt luonnollisesti palauttavat kyselyn tuloksen.

boolean addProduct(Product product) {
    boolean result = true;
    SQLiteDatabase db = this.getWritableDatabase();

    ContentValues values;
    values = new ContentValues();
    values.put(ProductTable.PRODUCT_NAME, product.getName());
    values.put(ProductTable.PRODUCT_DESCRIPTION, product.getDescription());
    values.put(ProductTable.PRODUCT_COUNT, product.getQuantity());

    long id = db.insert(PRODUCT_TABLE_NAME, null, values);

    if(id == -1) {
        Log.e(TAG, "Could not add product");
        result = false;
    }
    return result;
}

Tiedon hakeminen tietokannasta

Tietokannan lukemiseen riittää vain lukemiseen tarkoitettu SQLiteDatabase-olio, jonka saa getReadableDatabase()-metodilla. Kyselyn voi toteuttaa yhteen tauluun query()-metodilla, joka tarvitsee useanlaisia parametrejä. Tietokantaan voi myös suorittaa kyselyitä rawQuery()-metodilla, mille voi argumenttinä antaa sql-kyselyn suoraan stringinä.

Metodeista saadaan paluuarvona Cursor-luokan ilmentymä. Kursori antaa pääsyn haettuun tietokantatauluun. Tässä esimerkissä käymme kursorin jokaisen rivin läpi ja otamme sarakkeiden tiedot getString()- ja getInt()-metodeilla, missä parametreinä on sarakkeen numero alkaen nollasta. Sarakkeet ovat samassa järjestyksessä, kuin kyselyssä annetussa argumentissä.

Kursori tulisi aina muistaa sulkea sen käyttämisen jälkeen.

List<Product> getProducts() {

    SQLiteDatabase db = this.getReadableDatabase();

    String[] colums = new String[] { ProductTable.PRODUCT_NAME, ProductTable.PRODUCT_COUNT, ProductTable.PRODUCT_DESCRIPTION };

    Cursor cursor = db.query(PRODUCT_TABLE_NAME, colums, null, null, ProductTable.PRODUCT_NAME, null, null);

    cursor.moveToFirst();

    List<Product> products = new ArrayList<Product>();
    while (cursor.isAfterLast() == false) {
        String name = cursor.getString(0);

        Log.v(TAG, "Found product: "+name);
        Product product = new Product(name);

        product.setDescription(cursor.getString(2));
        product.setQuantity(cursor.getInt(1));

        products.add(product);

        cursor.moveToNext();
    }
    cursor.close();
    return products;
}

Tiedon poistaminen

Tiedon poistaminen tietokannasta tapahtuu hyvin samalla tavalla, kuin lisääminen. Metodin onnistumisen seuraaminen onnistuu helpoiten, kun se palauttaa boolean arvon. SQLiteDatabase tarjoaa metodin delete(taulun_nimi, where_lause, where_parametrit), joka palauttaa poistettujen rivien lukumäärän.

boolean deleteProduct(Product product) {
    SQLiteDatabase db = this.getWritableDatabase();
    String[] params = new String[] { product.getName() };

    int numOfRows = db.delete(PRODUCT_TABLE_NAME, ProductTable.PRODUCT_NAME + " = ?", params);

    if (numOfRows >= 1) {
        return true;
    }
    return false;
}

Tallentaminen Andoirissa

Android tarjoaa useita mahdollisuuksia tallentaa tietoa applikaatioissa. Käymme tässä nopeasti läpi yleisimmät menetelmät. Kolme ensimmäistä menetelmää ovat kaikki tavalla tai toisella tiedostoon kirjoittamista ja vaativat hieman enemmän aikaa IO-operaatioiden takia. Tämä tulisi ottaa huomioon applikaatiossasi ja suorittaa kaikki IO-operaatiot erillisessä säikeissä. Säikeistä ja AssyncTaskia käsitellään erillisessä artikkelissa.

1. Tiedostoon kirjoittaminen

Tämä on suoraviivaista tiedostoon kirjoittamista. Oletuksena Androidissa kirjoitetut tiedostot ovat applikaatiolle yksityisiä, eikä toiset applikaatiot tai käyttäjä itse pysty niitä lukemaan. Tätä voi kuitenkin vaihtaa kohdassa 4 käsiteltävillä moodeilla.

String tiedostoNimi = “tiedoston_nimi”;
String data = “Tallennettava data”;

FileOutputStream output = openFileOutput(Tiedostonimi, context.MODE_PRIVATE);
output.write(data.getBytes());
output.close();

2. Serializable object

Tämä on vain erikoistapaus kohdasta yksi. Serialisoidut objektit voi kirjoittaa tiedostoon suoraan ja Java teoriassa hoitaa kirjoittamisen puolestasi. Mutta jos haluat tukea versiointia ohjelmassasi on sinun itse toteutettava luku ja kirjoitus metodit, eli lopputulos olisi sama kuin tiedostoon kirjoittamisessa.

3. SQL-tietokanta

Androidissa tulee suoraan valmiina SQLite tietokanta, jota voi käyttää tallentamaan tietoa ohjelmasta. Luomasi tietokannat ovat applikaatiolle yksityisiä, eikä niihin pääse käsiksi muista applikaatioista.

Android tukee neljää tietomuotoa tietokannassa.
1) TEXT: tämä on rinnastettavissa Javan Stringiin
2) INTEGER: sama kuin Javan Long
3) REAL: sama kuin Javan Double
4) BLOB: tallentaa tietoa bittitaulukkona

Muuta tietoa tallennettaessa täytyy se ensin muuntaa johonkin näistä muodoista.

Suositeltu tapa käyttää SQLite-tietokantaa on tehdä luokka, mikä jatkaa SQLiteOpenHelper-luokkaa. Tässä luokassa sinun on toteutettava omat versiot onCreate ja onUpgrade()-metodeista. Käsittelen SQLite-tietokannan tekemistä yksityiskohtaisesti ja esimerkkien kanssa seuraavan viikon androidkehitys.fi-blogissa.

4. Preferenssit

SharedPreferences luokka Androidissa mahdollistaa avain – arvo parien tallentamisen yksinkertaisista tietotyypeistä: boolean, float, int, long, string. Preferenssit ovat joko aktiviteetti- tai puhelinkohtaisia.

Aktiviteettikohtaiset preferenssit saa getPreferences()-metodilla ja applikaatiokohtaiset preferenssit getSharedPreferences(“preferenssiNimi”, moodi), missä moodi on joku seuraavista:

  • MODE_PRIVATE: Tämä on oletus moodi ja antaa vain kutsuvalle applikaatiolle oikeuden käsitellä tiedostoa.
  • MODE_WORLD_READABLE: Mikä tahansa applikaatio saa lukuoikeuden tiedostoon.
  • MODE_WORLD_WRITABLE: Mikä tahansa applikaatio saa kirjoitusoikeuden  tiedostoon.
  • MODE_MULTI_PROCESS: Tämä moodi automaattisesti päivittää preferenssit, vaikka ne olisi jo ladattu prosessille. Näin muiden prosessien tekemät muutokset samaan tiedostoon näkyvät heti ja automaattisesti. On kuitenkin parempia tapoja siirtää tietoa prosessista toiseen.

Preferenssien muokkaaminen vaatii Editor-olion käyttöä. Preferenssejä voi käyttää esimerkiksi seuraavasti:

private final String PREFERENSSI_NIMI = “preferenssiNimi”;
private final String NIMI = “nimi”

SharedPreferences preferenssit = getSharedPreferences(PREFERENSSI_NIMI, Context.MODE_PRIVATE);

Editor editori = preferenssit.edit();
editori.putString(NIMI, ”username”);
editori.commit();

Preferenssien lukeminen onnistuu helposti tietotyypin get-metodilla, jossa annetaan preferenssin nimi ja oletusarvo, jos preferenssiä ei löydy:

preferenssit.getString(NIMI, null);

Androidin resurssit, orientaatio ja kuvan tarkkuus

Mobiililaitteille koodatessa tarvitsee usein ottaa huomioon, että käyttäjä saattaa käyttää laitettaan muussakin, kuin pystysuunnassa. Ohjelmiston ulkonäköä eri asennoissa tulisi miettiä ja vain harvoin orientaation lukitseminen yhteen asentoon on toimiva ratkaisu.

Androidissa pystyy hyvin pienellä vaivalla tekemään uusia pohjia (layout) tai käyttämään eri kuvia riippuen laitteen orientaatiosta. Android ottaa nämä pohjat ja kuvat automaattisesti käyttöön orientaatiosta riippuen, jos ne on asetettu oikeisiin kansioihin ja nimetty oikein.

Projektin resurssien peruskansiot ovat drawable ja layout. Näihin kansioihin asetetut resurssit tulevat köyttöön näytön tarkkuudesta tai orientaatiosta riippumatta. Android kuitenkin hakee resursseja ensisijaisesti kansiosta, joka vastaa nöytön tarkkuutta ja/tai orientaatiota. Mikäli pohja main.xml löytyy kansiosta “layout-land” sekä “layout”, valitaan tiedosto “layout-land”-kansiosta, kun laitetta pidetään vaakatasossa ja layout-kansiosta muissa tapauksissa.

Uutta projektia luotaessa Eclipse ei luo kaikkia kansioita suoraan, vaan joudut itse lisäämään ne. Kansioiden nimissä on aina joko “drawable” tai “layout” alussa. Alun jälkeen voi kansioille antaa viivalla eroteltuna sääntöjä siitä millaisessa tilanteessa tämän kansion resursseja tulisi käyttää. Täydellinen lista käytettävissä olevista parametreista löytyy osoitteesta:
http://developer.android.com/guide/topics/resources/providing-resources.html#AlternativeResources

Kuvien kanssa voi helposti tulla ongelmia, jos xml pohjat vaativat erilaisia kuvia eri orientaatioissa. Huono ratkaisu olisi tehdä uusi pohja haluttuun orientaatioon ja uudet kuvat, mitkä nimettäisiin jokainen yksilöllisesti. Xml:n osista, kuten kuvista on kuitenkin mahdollista tehdä uudet versiot samalla nimellä oikeisiin kansioihin. Näin Android osaa hakea oikean kuvaresurssin orientaatiosta tai resoluutiosta riippuen ilman xml-pohjan muuttamista.

Kuvien tarkkuuksien kanssa kannattaa olla varovainen. Mikäli Android löytää kuvan drawable-hdpi kansiosta, mutta ei drawable-mdpi kansiosta se automaattisesti skaalaa kuvan pienemmäksi. Tämä johtaa usein suttuisiin kuviin. Android 3.2 on tuonut lisää mahdollisuuksia näyttöjen koon ja tarkkuuden erittelyyn ja näihin perehdytään lähemmin erillisessä artikkelissa.

Jos kuva on vain drawable-kansiossa sitä ei skaalata vaan näytetään sellaisenaan. Drawable-kansioon ei tulisi laittaa kuvia, vaan täällä on suositeltavaa pitää vain xml:llä luotuja bittikarttoja tai kuvaresursseja, joiden luomista käsittelen myöhemmässä artikkeliss

Mikäli samaa kuvaa käytetään eri näyttötarkkuuden laitteilla tulevat kuvat erittäin isoiksi huonoilla tarkkuuksilla tai erittäin pieniksi suurilla tarkkuuksilla. Tarkempi kuvaus kuvien tarkuuksien käytöstä löytyy:
http://developer.android.com/guide/practices/screens_support.html#density-independence