Category Archives: Sovelluskehitys

Android-sovellusten kehitys

OUYA, Android-konsoli

Kesälomalta paluu sorvin ääreen voi olla raskasta tai hauskaa… Kesälomalla hankittujen lelujen kanssa siitä onneksi saa hauskaa.

OUYA on Kickstarter-rahoituksen avulla luotu Android 4.1 -konsoli, jossa pyörivät kaikki normaalit Android-applikaatiot ilman suurempaa vaivaa. Konsoliin saa helposti ladattua SDK:n, jonka avulla ohjainta voi käyttää applikaatioissaan, mutta hiirellä ja näppäimistölläkin voi konsolia suoraan käyttää.

OUYA ei ole kovin tehokas konsoli eikä pelitarjonta ole vielä kovin laaja, tätä artikkelia kirjoitettaessa vain 384 peliä, mutta 99$ hintansa puolesta se on hyvä hankinta arcade pelien pelaamiseen olohuoneen sohvalla kavereiden kanssa.

Yrityksille OUYA tarjoaa halvan ja helpon ratkaisun tarjota interaktiivista sisältöä asiakasnäyttöihin. Valmiin applikaation kääntäminen ja asentaminen OUYA:lle on hyvin helppoa. Kehittäminen OUYA-konsolille ei ole yhtään sen vaikeampaa kuin mille tahansa muulle Android-laitteelle. Toki pieniä muutoksia joudutaan tekemään käyttöliittymään, jotta normaalisti kosketusnäytölle tarkoitettua applikaatiota voi käyttää peliohjaimella tai hiirellä. Kosketusnäytön korvaaminen hiirellä tai muulla ohjaimella johtaa helposti hyvin heikkoon käyttökokemukseen.

 

Asentaaksesi minkä tahansa applikaation OUYA:lle pitää sinun voida ladata applikaation .apk-tiedosto Internetistä. Mene OUYA:n kotinäkymän MAKE-valikkoon ja sisäänrakennetulla Internet-selaimella lataa APK-tiedosto. Tämän jälkeen palaa päävalikkoon, mene MANAGE-valikkoon, valitse SYSTEM ja siellä valitse ADVANCED. Tämä on jokaiselle Android-käyttäjälle tuttu asetukset-valikko Androidissa. Mene listassa alaspäin ja valitse Storage, jonka sisällä valitse Download. Täältä löydät juuri lataamasi APK-paketin ja asentaminen onnistuu ohjaimen O-napilla. Tämän jälkeen applikaatio löytyy samasta MAKE-valikosta kuin OUYA:n Internet-selain.

Samaan MAKE-valikkoon tulevat myös applikaatiot, jotka itse kehität ja asennat tietokoneen ADB:n avulla microUSB:n kautta.

Kehittäminen OUYA:lla

Kaikki OUYA-konsolit toimivat applikaatioiden kehittämiseen ilman maksuja tai muutoksia konsoliin. Jotta saat OUYA:n toimimaan normaalissa Android-kehitysympäristössäsi Macilla sinun täytyy vain lisätä kolmannen osapuolen USB Hex ID (0x2836) adb_usb.ini -tiedostoon .android-hakemistossa ja käynnistää ADB uudelleen. Tarkemmat ohjeet myös Windows-koneille on katsottavissa videona OUYA developers -sivustolta. Tämän jälkeen OUYA-konsolisi näkyy Eclipsessä ja ADB:ssä samalla tavalla kuin mikä tahansa muu Android-laite.

Seuraavaksi lataa OUYA Development kit (ODK) osoitteesta https://devs.ouya.tv/developers/odk, kopioi libs\-hakemistosta ouya-sdk.jar ja lisää se omaan Android-projektiisi. Tämän jälkeen ohjaimen komennot saa kiinni oman Activity -luokkasi onKeyDown() ja onGenericMotionEvent() -metodeista.

Tein tätä artikkelia varten yksinkertaisen applikaation, joka käyttää ODK:ta siirtelemään ImageView:tä FrameLayoutin sisällä.

Ota ohjain käyttöön Activity-luokan onCreate()-metodissa:

@Override
protected void onCreate(Bundle savedInstanceState) {
	super.onCreate(savedInstanceState);
	OuyaController.init(this);
}

Tunnista ohjaimen komennot:

@Override
public boolean onKeyDown(final int keyCode, KeyEvent event) {
	switch (keyCode) {
		case OuyaController.BUTTON_O:
			Log.d(TAG, "OUYA button O");
			break;
		case OuyaController.BUTTON_U:
			Log.d(TAG, "OUYA button U");
			break;
		case OuyaController.BUTTON_Y:
			Log.d(TAG, "OUYA button Y");
			break;
		case OuyaController.BUTTON_A:
			Log.d(TAG, "OUYA button A");
			break;
	}
	return true;
}
@Override
public boolean onGenericMotionEvent(final MotionEvent event) {
// Joystick
	if ((event.getSource() & InputDevice.SOURCE_CLASS_JOYSTICK) != 0) {
		float LS_X = event.getAxisValue(OuyaController.AXIS_LS_X);
		float LS_Y = event.getAxisValue(OuyaController.AXIS_LS_Y);
	}
	return true;
}

Tämän jälkeen voit tehdä ohjaimen antamilla komennoilla mitä itse haluat.
Koko koodin ja Eclipse projektin saat ladattua Githubista:
https://github.com/Androidkehitys/OUYA

Onnea pelikehitykseen! Palataan leikkimään telkkujen kanssa, kun Cromecast saapuu postissa.

Porttautuva koodi mobiilissa

“Write once, run anywhere” oli Javan silver bullet -mantra aikanaan. Eli kirjoitat koodin kerran, käännät sen kerran ja ajat missä vaan tuetussa ympäristössä ilman että ohjelmasi tarvitsee liiemmin tietää ajonaikaisesti mikä käyttöjärjestelmä / rauta ympäristössä on käytössä. No, vaikkei tuohon ole vieläkään – 18 vuotta myöhemmin – ole ihan päästy, on Javan ja vaikkapa Pythonin kaltaisten tulkattujen/scriptauskielten kanssa useimmiten paljon helpompaa elää monialustaympäristössä kuin natiivikoodin. Mutta entä jos halutaankin kehittää natiivisovellus jossa uudelleenkäytetään mahdollisimman suuri osa koodista sen sijaan että N alustaa varten tehdään N toteutusta? Vaikka nykyisten mobiililaitteiden rautaspeksejä (kotitehtävä: montako 25MHz i386 CPU:ta tarvitaan tuottamaan Samsung Galaxy S4:n CPU-teho?) ihmetellessä ei parhaalla omallatunnollakaan voi sanoa resurssien puutteen pakottavan natiivikoodiin, on sen käyttö esim. Android-alustallakin perusteltua sovelluksissa joiden on puristettava se viimeinenkin pisara suorituskykyä irti laitteesta.

Luonnollisesti kaikenlaisiin sovellustyyppeihin porttautuvaa koodia on turha yrittää ujuttaa; jos sovellus on 80%:sti käyttöliittymää eikä ole tarjolla/ei haluta käyttää kaikilla kohdealustoilla toimivaa porttautuvaa UI-kirjastoa, ei kannata väkisin lyödä päätään seinään vaan kirjoittaa toteutukset kiltisti alustan natiivi-API:a käyttäen – tai valita teknologiaksi HTML5 joka sopii webmäisiin lomakesovelluksiin kuin nyrkki silmään. Ym. UI-kirjastoa käyttämällä saatetaan myös – sen toteutuksesta toki riippuen – menettää alustan oma look-and-feel. Ihan hihasta ravistettuna porttautuvan arkkitehtuurin puolesta / vastaan listat voisivat näyttää vaikkapa tältä:

Vastaan:

  • Väkisin yritetty porttautuvuus tekee arkkitehtuurista kömpelön ja lisää turhaa kompleksisuutta
  • Alustan natiiveja ominaisuuksia saatetaan menettää (hyvässä ja pahassa)
  • Käytettäväksi valittu yhteinen ohjelmointikieli ei välttämättä ole tuttu kaikille projektin jäsenille

Puolesta:

  • Koodin joutuu kirjoittamaan vain kerran eli kehityskustannus pienenee
  • Bugit joutuu korjaamaan vain kerran
  • Koodipohjasta tulee merkittävästi pienempi jolloin sen ylläpidettävyys paranee ja siihen on nopeampaa perehdyttää uusia tekijöitä
  • Ohjelmiston luonteen salliessa paljon koodia kierrättävä arkkitehtuuri on hyvin elegantti

Esimerkkiprojekti: MMark13 – mobiilia suorituskykytestausta

Käytän esimerkkinä kuluneen vuoden aikana hiljalleen syntynyttä harrasteprojektiani MMark13, joka – hyvin tiivistetysti – on mobiililaitteiden CPU/GPU suorituskyvyn mittaamiseen tarkoitettu työkalu. Vastaavalla idealla (joskin merkittävästi isommalla tiimillä & budjetilla :)) rakennettu sovellus löytyy Desktop-puolelta, monelle tuttu suomalaista tekoa oleva Futuremark:in 3DMark. Ja ei, en ole suoraan rinnastamassa harrastelijatuotostani PC-maailman (Ja Futuremark:han puuhaa parasta aikaa myös mobiilibenchmarkingia, Androidille se jo ilmestyikin! -toim.) uraauurtavaan tuotteeseen.. no joka tapauksessa, projekti lähti liikkeelle halusta hioa/päivittää OpenGL ES 2.0 taitoja ja havaittuani ettei tuossa vaiheessa (alkuvuosi 2012) juuri vastaavia sovelluksia ollut tarjolla (paitsi kaupallinen ja maksullinen GLBenchmark 2.5 – joskin tuolloin vain iOS:lle), valitsin kyseisen sovellusalueen jo siksikin että porttautuvan arkkitehtuurin hyväksikäyttäminen laajalti tarjosi ainutlaatuisen edun: kun jokaisella alustalla ajettava koodi on 100% sama niiltä osin jotka grafiikan piirtoon / mittauksiin vaikuttavat, voidaan tuloksia verrata 1:1 toisiinsa alustojen välillä ilman että tarvitsee napista Javan tehottomuudesta Androidilla.

 

MMark13 koodin rakenne

 

Koodin uudelleenkäytön suunnittelussa on hyvä lähteä liikkeelle siitä että kartoittaa tarkasti kohdealustansa ja niiden tarjoamat asiat ja sitten hahmottelee niiden pohjalta suurimman yhteisen nimittäjän. Valitsin kohdealustoikseni iOS (Applen laitteet) ja Qt (Android, kaikki merkittävät desktop-käyttöjärjestelmät, Meego, Blackberry10, Symbian, Sailfish OS, Ubuntu Phone, luultavasti myös Tizen ja Firefox OS). Kun alustat ovat tiedossa, aloittaa voi vaikka ohjelmointikielestä; se on valinta jota alemmalle tasolle ei juuri pääse. C olisi aina varma valinta, mutta puhtaalla C:llä syntyy tulosta kovin hitaasti ja dynaamisten tietorakenteiden / merkkijonojen / jne käsittely on melko kömpelöä. Valitsinkin siksi pykälää korkeamman abstraktiotason eli C++:n. Ohjelmointikielen version valintaan kannattaa paneutua myös hetken eikä sännätä käyttämään heti uusinta; itse en uskaltanut vielä tähän projektiin valita tuoreinta C++11 versiota vaikka se oliskin tarjonnut muutamat elämää helpottavat asiat kuten “auto” tyypin muuttujille, sisäänrakennetun säiekirjaston ja paljon muuta. Sittemmin olen perehtynyt tarkemmin C++11 feature support matriisiin merkittävillä kääntäjillä (GCC, clang, MSVC++) ja vaikuttaa että aika on kypsä sen käyttöönotolle.

Ohjelmointikielen valinnan jälkeen seuraava looginen askel on kartoittaa APIen/ulkoisten kirjastojen tarve ja koettaa vaikkapa proof-of-concept -hengessä varmistaa että kirjastovalinnat täyttävät tarpeet. MMark13 -projektissa tarvitsin säikeistystä, joten käytin hyväkseni tietoa että kaikki kohdealustani ovat UNIX-perillisiä ja siten tarjoavat POSIX threads (pthreads) paketit. Tarvittiin myös JSON kirjastoa; pienellä googlauksella löysin “jsoncpp” paketin jonka koodi/dokumentaatio oli poikkeuksellisen laadukkaan oloista ja joka istui suoraan tarpeisiini. Kenties tuosta löydöksestä riemuitessani kirjoitin seuraavan askeleen eli tuloslaskelma-JSON lähetyksen palvelimelle abstraktion läpi siten että kirjoitin alustoille omat toteutuksensa sen sijaan että olisin käyttänyt ilmiselvää ratkaisua libcurlia. Tajusin mokani vasta projektin loppumetreillä enkä rupea yleensä korjaamaan jotain joka toimii, joten saivat jäädä. Lisäksi otin mukaan simppelin C++ MD5-toteutuksen. Halusin toteuttaa softani käyttöliittymän alustariippumattomasti siten että se käyttäytyy kaikilla laitteilla samalla tavalla ja myös näyttää samalta. Etsinkin melko laajalti OpenGL-pohjaista Widget-kirjastoa mutta löytämäni paketit olivat joko aivan järjettömiä bloatteja tai sitten erittäin huonolaatuisen näköistä koodia (usein molempia) että päädyin kirjoittamaan hyvin lightweight toteutuksen aivan alusta erään intensiivisen viikonlopun aikana. Eli ainakin yksi pyörä tuli keksittyä uusiksi – toisaalta aikaa ei tuohon uponnut paljoa, lopputulos miellyttää silmää, homma oli erittäin hauskaa ja lisäksi vieläpä opin paljon.

Lisäksi tarvittiin seuraavat asiat alustariippuvasti: softan bootstrapping, softa/hardisinformaation penkomista laitteelta, joitain sekalaisia pikkujuttuja sekä resurssifilejen lukeminen systeemistä – tähän olisi voinut käyttää ihan fread():ia mutta halusin toisaalta käyttää Qt:n mainiota resource systeemiä jonka kanssa tuo taas ei toimi. Alla muutama konkreettinen koodiesimerkki alustariippuvasta koodin jaosta:

CommonFunctionsQT.cpp:

std::string RandomUuid()
{
    // .mid(1,36) strips the curly braces added by toString(), duh
    return std::string(QUuid::createUuid().toString().mid(1,36).toUtf8());
}

CommonFunctionsIOS.cpp:

std::string RandomUuid()
{
    CFUUIDRef theUUID = CFUUIDCreate(NULL);
    CFStringRef cfuuid = CFUUIDCreateString(NULL, theUUID);
    CFRelease(theUUID);
    NSString* uuid = (NSString*)cfuuid;
    [uuid autorelease];

    return std::string([uuid UTF8String]);
}

Kurkistetaan vielä korkealla tasolla ohjelmiston toimintaa ja sitä miten alustariippuva ja porttautuva osa koodia pelaavat yhteen. iOS-buildissa tarvittava wrapperkoodi on luonnollisesti Objective-C(++):aa; kokemus osoitti ettei sen ja C++ -maailman sekoittaminen keskenään ole triviaalia tai kovin eleganttia, ja kehotankin eristämään kyseisen rajapinnan mahdollisimman pienelle alalle. Käytännössä toimiva pattern on antaa Objective-C++ koodille yksisuuntainen has-a -patternin mukainen suhde porttautuvaan C++ koodiin ja rakentaa iOS-specific toteutus C++ luokalle joka hoitaa callbackien tarvitseman alustariippuvan koodin. Qt maailma on natiivisti C++:aa, joten siellä ei tarvittu taikatemppuja. Ohjelman toimintalogiikka pääpiirteittäin:

  1. Alustariippuvainen osa ottaa vastaan inputit, muuttaa niiden datatyypit alustariippumattomaan muotoon ja välittää ne eteenpäin allaolevalle porttautuvalle osalle. Käytännössä inputit ovat:
    • Redraw timer. iOS:ssa CADisplayLink, Qt:ssä QTimer.
    • Käyttäjän kosketukset. iOS:ssa UITouch*, Qt:ssa QTouchEvent.
  2. Alustariippumaton osa päivittää tilan / menussa ollessa UI:n tilan touchien / kuluneen ajan avulla
  3. Uuden tilan perusteella piirretään uusi frame sekä mm. aloitetaan fysiikkalaskenta seuraavaa framea varten
  4. Tarvittaessa kutsutaan abstraktoituja callbackeja kun tarvitaan alustariippuvaa toiminnallisuutta; esimerkiksi kun ladataan testin vaatimia tekstuureita levyltä tai halutaan lähettää tulokset serverille

Jollekulle on saattanut tätä lukiessa tulla mieleen että mitenkäs se Windows Phone sitten? WP8 pois jättämiseen kohdealustojen joukosta on oikeastaan vain yksi syy – OpenGL APIn puute. Vaikka ymmärtääkseni WP8 hardis on OpenGL ES 2.0 yhteensopivaa, Microsoft ei tapansa mukaan tarjoa standardia rajapintaa vaan pakottaa kehittäjät porttaamaan koodinsa Direct3D:lle. Ja koska softani tärkeimpiä ominaisuuksia on olla suoraan vertailukelpoinen ympäristöjen välillä, ei tuo oikein ollut optio. Jos Googlen ANGLE projektista tulee ikina luotettava port WP:lle *tai* jostain taivaasta putoaa OpenGL ajuri, teen ilomielin myös WP-buildin. Tämän vuoden aikana on tarkoitus suoltaa ulos buildit ainakin Blackberry10:lle, Sailfish OS:lle sekä Ubuntu Phonelle riippuen siitä saadaanko moisia laitteita käsiimme.

Hyvää kevättä kaikille!

Linkkejä

Lisää luettavaa:

Ehta modaalidialogi iOS:lla


Muista kehitysympäristöistä iOS-maailmaan tulevasta voi tuntua oudolta Cocoa Touchin modaalidialogien puute. Vaikkakin on mahdollista rakentaa kaiken muun päälle piirtyvä dialogi joka oletukselta (ja tämä on ohitettavissa) saa kaiken syötteen, dialogin näyttävä kutsu ei blokkaa vaan palaa välittömästi ja dialogin sulkeutuminen on otettava kiinni asynkronisella callbackilla (‘delegate’). Edes perustyökalu UIAlertView ei käyttäydy kuin muiden alustojen – vaikkapa MFC, Javascript, Symbian Series60/Series80, Java AWT/Swing, Qt, Android, GTK, jne.. – stock komponentit MessageBoxit ja InputDialogit.

Tämänkaltaista UI paradigmaa voi kuitenkin paitsi kaivata, myös tarvita; oletetaan etta ollaan rakentamassa iOS -toteutusta cross-platform sovellukseen jossa täytyy toteuttaa metodi vaikkapa seuraavasti:

std::string AskUserInput()
{
  // Semantiikka: kysy käyttäjältä syöte popup dialogilla 
  //   ja palauta annettu teksti
}

Miten tämä toteutetaan iOS:lla? Nostetaan kädet pystyyn, otetaan projektin arkkitehti Skypen päähän ja anotaan API:in muutosta joka mahdollistaisi syötteen kyselyn asynkronisen käsittelyn? Miksei, mutta jos yo. mekanismi on jo toteutettu viidelle muulle alustalle, on edessa iso muutostyö ja paljon hampaita kiristeleviä ihmisiä. Turha asynkronia näin yksinkertaisessa asiassa hankaloittaa sekä pirstaloittaa arkkitehtuuria.

Ongelman ratkaisu lähtee UI frameworkin toiminnan ymmärtämisestä. Käytännössä kaikissa moderneissa käyttöliittymäkirjastoissa on 1-N UI säiettä jotka palvelevat käyttöliittymää ns. event loopin [tapahtumakieriö?! -toim.] kautta. Event loop ottaa inputteja komponenteilta, timereilta tai vaikkapa muilta säikeiltä alustakohtaisten viestinvälitysmekanismien välityksellä ja tarjoilee niitä kiinnostuneille kuuntelijoille. Käytännössä jokaisella säikeellä on tasan yksi (tai nolla, jos kyseessä ei ole UI:ta palveleva säie) event loop, ja yleisesti mobiilikäyttöjärjestelmissä UI:ta palvelee vain yksi säie kerrallaan. Käyttöliittymä kumminkin toimii jouhevasti niin kauan kuin tätä säiettä ei varata kerralla liian pitkäksi aikaa; pitkäkestoinen laskenta täytyy joka jakaa pieniin osiin tai tehdä muissa säikeissä.

Event loopiin perustuvat järjestelmät sallivat yleensä event looppien sisäkkäisen ajamisen (nesting); tällöin vanha event loop aktivaatio pysähtyy eikä jatka ennenkuin sisempi loop aktivaatio loppuu. Tätä voidaan hyväksikäyttää asynkronian poistamiseksi; API-metodin sisällä ajetaan uusi aktivaatio, kuunnellaan asynkronista vastausta ja lopetataan aktivaatio kun vastaus on saatu. Tällöin APIa kutsuva metodi blokkaa koko suorituksen ajan. On kuitenkin erotettava tämä semaforin odottamisesta; säiehän ei ole idlena vaan suorittaa sisempää event loop aktivaatiota.

Rakennetaan esimerkkitoteutus iOS:lle. Halutaan tehda seuraavanlainen kutsu:

-(void) requestAndLogInput {
  InputDialog* dlg = [InputDialog dialog];
  [dlg showModal];
  NSLog(@"Dialog input was = %@", [dlg getInput]);
}

Aloitetaan dialogin luomisesta. Tähän käytetään normaalia Cocoa Touch boilerplatea:

+(InputDialog*) dialog {
    NSArray* nibViews = [[NSBundle mainBundle] loadNibNamed:@"InputDialog"
                                                      owner:nil
                                                    options:nil];
    InputDialog* dlg = [[[nibViews objectAtIndex:0] retain] autorelease];

    return dlg;
}

Määritellään metodi jolla dialogi näytetään modaalisti – se asettuu päällimmäiseksi ja blokkaa kunnes dialog on suljettu. iOS:lla event loop tunnetaan nimellä Run loop, ja sitä vastaavat APIt ovat NSRunLoop sekä Core Foundationin CFRunLoop*:

-(void) showModal {
    // Lisätään dialogimme keywindow'n ylimmän lapsen ylimmäksi lapseksi,
    // jolloin dialogi päätyy päällimmäiseksi view stackiin
    UIWindow* keyWindow = [[UIApplication sharedApplication] keyWindow];
    UIView* topmostView = [keyWindow.subviews objectAtIndex:0];
    [topmostView addSubview:self];

    // Sijoitetaan dialogi suunnilleen keskelle ruutua    
    CGSize s = topmostView.bounds.size;
    self.center = CGPointMake(s.width / 2, (s.height / 2) - 70);

    // Ajetaan nested run loop aktivaatio jolloin showModal kutsu blokkaa
    CFRunLoopRun();
}

Lopuksi määritellään dialogin sulkeva IBAction:

-(IBAction) okPressed {
    // Poistetaan dialogi näkyvistä
    [self removeFromSuperview];

    // Pysäytetään sisempi run loop aktivaatio; kontrolli palaa 
    // ulommalle ja showModal -kutsu palaa
    CFRunLoopStop(CFRunLoopGetCurrent());
}

Valmista! Ja koska kyseessä  ovat erilliset run loop aktivaatiot eivätkä erilliset run loopit, esimerkiksi timerit jatkavat eloaan ja autorelease pool on jaettu aktivaatioiden välillä.

 Esimerkkiprojekti Xcode 4.5 -projektina. 40kB zip

 

Lua mobiilisoftassa

Käyttämällä skriptikieliä mobiiliohjelmoinnissa voidaan mm. edistää koodin jakamista eri alustojen kesken. Varsinkin peliohjelmoinnissa skriptien käyttäminen on suosittua: Pelilogiikkaa on helposti paljon ja sen kaiken kirjoittaminen erikseen eri ohjelmointikielillä eri alustoille olisi aikaavievää ja virhealtista. Eräs varsin suosittu skriptikieli on Lua. Lua on suhteellisen helppo yhdistää toiseen projektiin ja siihen on helppo luoda applikaatiospesifisiä laajennuksia. Lua on lähtökohtaisestikin suuniteltu laajennuskieleksi ja verrattuna esim. Pythoniin Lua on hyvin kevyt. Tässä artikkelissa opastetaan, miten Luaa voi käyttää iOS- ja Android-projekteissa.

Vaihe 0 — Luan lähdekoodi

Hae Luan lähdekoodi http://www.lua.org/ftp/.

Huomaa, että Lua on muuttunut jonkin verran versioiden välillä. Tämä ohje on kirjoitettu versiolle 5.2.1. Jotkin aiemmalle versiolle kirjoitetut Lua-ohjelmat eivät välttämättä toimi suoraan uusimmalla versiolla. Näitä ohjeita voi soveltaa myös muihin versioihin, mutta ne eivät välttämättä toimi sellaisenaan.


iOS

Vaihe 1 — Lua-kirjasto

  1. Luo uusi kirjastoprojekti. Tässä esimerkissä olen antanut projektille nimeksi Lua521 Luan versionumeron mukaan.Luo uusi kirjastoprojekti
  2. Poista projektiin automaattisesti luodut ProjektinNimi.m ja .h -tiedostot.
  3. Lisää projektiin Luan lähdekoodipaketista src-hakemiston sisältö lukuunottamatta tiedostoja lua.c, luac.c ja Makefile.
  4. Mene projektin Build Phases -ikkunaan ja lisää Copy Headers -vaihe.
    Build Phases
    Lisää vaihe
  5. Lisää Copy Headers -vaiheeseen projektin tiedostot lauxlib.h, lua.h, lua.hpp, luaconf.h ja lualib.h ja siirrä ne Public-osaan.
    Copy Headers -vaihe
  6. Lua-kirjasto on nyt valmis käytettäväksi.

Vaihe 2 — Lua-kirjaston käyttäminen toisessa projektissa

Esimerkkinä käytän kokonaan uutta projektia, mutta Luan lisääminen olemassaolevaan projektiin tapahtuu samoin.

  1. Lisää projektiin viittaus Lua-kirjastoprojektiin. Tämä tapahtuu kuten minkä tahansa muunkin olemassaolevan tiedoston lisääminen. Tässä tapauksessa lisättävä tiedosto on kirjastoprojektin .xcodeproj-projektitiedosto.
    Lisätään viittaus kirjastoon
    Lisättävä tiedosto on kirjastoprojektin .xcodeproj-tiedosto
  2. Aseta projektin targetti riippuvaiseksi Lua-kirjaston targetista.
  3. Lisää Lua-kirjasto linkitettäviin kirjastoihin.
    Appin Build Phase konffit
  4. Lisää Header Search Pathsiin polku $(BUILT_PRODUCTS_DIR)/usr/local/include.
    Header Search Paths
  5. Nyt projektissa voidaan käyttää Luaa.

Android

Vaihe 1 — Lua-kirjasto

Lua on kirjoitettu C-kielellä, joten Androidissa tarvitsemme NDK:ta sen kääntämiseen. Kätevintä on luoda siitä NDK-moduli. Android NDK:n ja NDK-modulien käytön opastaminen tässä olisi hieman liian työlästä, ja NDK:ssa on varsin hyvät dokumentaatiot ja esimerkit. Mainittakoon joka tapauksessa, että jos et ole aiemmin käyttänyt ja/tai luonut NDK-moduleita, helpointa on luoda yksi hakemisto jonka alle sijoitat kaikki NDK-modulit. Tämän hakemiston polku pitää olla NDK_MODULE_PATH-ympäristömuuttujassa.

  1. Luo uusi hakemisto NDK-modulihakemiston alle (tämän hakemiston nimi on luotavan modulin nimi).
  2. Kopioi tähän hakemistoon Lua-lähdekoodipaketin src-hakemiston sisältö lukuunottamatta tiedostoja lua.c, luac.c ja Makefile.
  3. Luo tähän hakemistoon seuraavansisältöinen Android.mk-tiedosto:
    LOCAL_PATH := $(call my-dir)
    
    include $(CLEAR_VARS)
    
    LOCAL_MODULE := liblua521
    LOCAL_CFLAGS := -D"getlocaledecpoint()='.'"
    LOCAL_SRC_FILES := \
    	lapi.c \
    	lauxlib.c \
    	lbaselib.c \
    	lbitlib.c \
    	lcode.c \
    	lcorolib.c \
    	lctype.c \
    	ldblib.c \
    	ldebug.c \
    	ldo.c \
    	ldump.c \
    	lfunc.c \
    	lgc.c \
    	linit.c \
    	liolib.c \
    	llex.c \
    	lmathlib.c \
    	lmem.c \
    	loadlib.c \
    	lobject.c \
    	lopcodes.c \
    	loslib.c \
    	lparser.c \
    	lstate.c \
    	lstring.c \
    	lstrlib.c \
    	ltable.c \
    	ltablib.c \
    	ltm.c \
    	lundump.c \
    	lvm.c \
    	lzio.c
    
    LOCAL_EXPORT_C_INCLUDES := $(LOCAL_PATH)
    include $(BUILD_STATIC_LIBRARY)

    Android.mk-tiedoston rivi LOCAL_MODULE := liblua521 määrää luotavan kirjaston nimen — sen voi muokata haluamakseen.

  4. Lua-kirjastomoduli on valmis käytettäväksi

Vaihe 2 — Lua-kirjaston käyttäminen toisessa projektissa

Koska itse Lua-kirjasto on NDK-moduli, pitää sitä myös käyttää NDK:lla käännettävän koodin kautta. Ohjeita NDK:n käyttöön saa muualta. Lua-kirjastomodulia käytetään kuten mitä tahansa NDK-modulia: Projektin Android.mk-tiedostoon lisätään tieto linkitettävästä kirjastosta, ja tiedoston lopussa kutsutaan import-module-makroa. Esimerkkiprojektini Android.mk-tiedosto näyttää seuraavanlaiselta:

LOCAL_PATH := $(call my-dir)

include $(CLEAR_VARS)

LOCAL_MODULE  := luatest
LOCAL_CFLAGS := -std=c99 -Wall -Werror
LOCAL_SRC_FILES := luatest.c
LOCAL_STATIC_LIBRARIES := lua521
LOCAL_LDLIBS := -llog

include $(BUILD_SHARED_LIBRARY)

$(call import-module,lua521)

Kiinnostavat rivit ovat siis LOCAL_STATIC_LIBRARIES := lua521, jossa linkitetään Lua-modulin luoma liblua521-kirjasto, sekä $(call import-module,lua521), jolla sisällytetään projektiin lua521-niminen Lua-moduli.


Vaihe 3 — Lua-koodin ajaminen

Jokaiselle sopivia tyhjentäviä ohjeita on hankala kirjoittaa, joten valaisen asiaa muutamalla yksinkertaisella esimerkillä. Näitä esimerkkejä soveltamalla päässee yllättävän pitkälle. Kaikki esimerkkikoodi on C:tä (ei Objective- tai -++); koodiesimerkkien yhdistäminen muuhun koodiin, oli kyse sitten Android- tai iOS-koodista, jätetään harjoitustehtäväksi.

Yksinkertaisen skriptin ajaminen

Lua-kirjaston funktiot on esitelty kolmessa .h-tiedostossa: lua.h, lualib.h sekä lauxlib.h. Koodiesimerkit on kirjoitettu olettaen että kaikki kolme otsikkotiedostoa on sisällytetty lähdekooditiedostoon. Huom! Nämä ovat nimeomaan C-headereita; jos Luaa käytetään C++-koodissa, sisällytettävä tiedosto on lua.hpp (ei muita).

Seuraava koodinpätkä ajaa lyhyen Lua-skriptin:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
static void luaExample() {
     lua_State* lua = luaL_newstate();
     luaL_openlibs(lua);
     const char* script = “print(\”Printing from Lua!\”)\n”;
     luaL_loadstring(lua, script);
     int err = lua_pcall(lua, 0, 0, 0);
     if(err != LUA_OK) {
          const char* errMsg = lua_tostring(lua, -1);
          printf(“Error while calling Lua function: %s”, errMsg);
     }
     lua_close(lua);
}

Rivillä 2 luodaan uusi lua_State-olio. Tämä olio on käytännössä kokonainen Lua-tulkki. Tapauksesta riippuen appissa voisi olla yksi lua_State koko sen elinkaaren ajan, tai siinä voidaan vaikka luoda ja tuhota lua_Stateja tarpeen mukaan. Kutsun palauttama olio on periaatteessa käytettävissä suoraan, mutta käytännössä aina pitää tehdä myös rivillä 3 oleva kutsu, joka lataa Lua-tulkkiin Luan standardikirjastot.

Rivillä 5 ladataan merkkijonosta Lua-funktio. Tässä kohtaa lienee parasta paneutua hieman Lua-tulkin C-APIin. Lua-tulkilla on oma operandipino. Suurin osa C-API-kutsuista käsittelee tätä pinoa. Esimerkiksi Lua-funktion kutsuminen tapahtuu siten, että pinoon työnnetään ensiksi kutsuttava funktio ja sitten funktion kaikki argumentit ensimmäisestä aloittaen. Tämän jälkeen kutsutaan lua_call()- tai lua_pcall()-funktiota. Tämä kutsu poistaa funktion ja argumentit pinosta, kutsuu funktiota ja funktion palattua työntää funktion mahdolliset paluuarvot pinoon ensimmäisestä aloittaen. Kun uusi Lua-tulkki luodaan, pino on tyhjä. Kutsu luaL_loadstring()-funktioon lukee annetusta merkkijonosta Lua-funktion ja työntää sen pinon päällimäiseksi. Tämän kutsun jälkeen siis pinon päällimmäinen (ja ainoa) olio on tuo Lua-funktio (tai virheilmoitus, jos funktion lataaminen epäonnistui).

Rivillä 6 kutsutaan edellisellä rivillä ladattua funktiota. Käytettävä C-funktio on lua_pcall(). Ero lua_call()- ja lua_pcall()-funktioiden välillä on, että lua_pcall() (p = protected) palauttaa virhekoodin jos kutsu epäonnistuu. Funktio lua_call() sen sijaan käyttää Lua-tulkin virheenhallintajärjestelmää, joka lähtökohtaisesti tulostaa virheilmoituksen ja kaataa koko appin. Kannattaa siis lähes poikkeuksetta käyttää lua_pcall():ia. Lua_pcall():lle annettavat argumentit ovat: lua_State, argumenttien määrä, paluuarvojen enimmäismäärä, ja message handlerin indeksi pinossa (0 ei ole validi indeksi, vaan se merkitsee, että message handleria ei ole; pinon indeksit alkavat 1:stä). Tässä tapauksesssa emme välitä yhtään argumenttia funktiollemme, emmekä ole kiinnostuneita sen mahdollisista paluuarvoista.

Rivillä 7 tarkistetaan, onnistuiko funktiokutsu. Jos paluuarvo on LUA_OK (0), funktiokutsu onnistui, ja pinossa on nyt funktion mahdolliset paluuarvot, kuitenkin enintään niin monta arvoa kuin lua_pcall()-kutsussa oli määrätty. Tässä tapauksessa pino on siis tyhjä. Muu paluuarvo merkitsee, että funktiota suoritettaessa tapahtui virhe. Paluuarvo kertoo virheen laadusta jotain (katso tarkempaa tietoa Lua Reference Manualista), mutta tässä tapauksessa tarkastelemme vain virheilmoitusta. Jos kutsu epäonnistuu, pinoon työnnetään (funktion paluuarvojen sijaan) virheilmoitus. Rivillä 8 haemme virheilmoituksen pinosta. Indeksi -1 tarkoittaa pinon päällimmäisintä alkiota (negatiiviset indeksit lasketaan pinon päältä alkaen). Tässä esimerkissä tänne ei koskaan tulla, mutta voit kokeilla virheenhallintaa muokkaamalla Lua-skriptin virheelliseksi.

Rivillä 11 lua_State tuhotaan. Lua_close()-kutsun jälkeen lua_Statea ei voi enää käyttää. Jos appissa luodaan Lua-tulkkeja tarpeen mukaan, on tärkeää muistaa tuhota ne lua_close()-kutsulla kun niitä ei enää tarvita, etteivät ne jää varaamaan muistia.

Argumentit ja paluuarvot

Seuraavassa on esimerkki Lua-funktiosta joka ottaa argumentteja ja palauttaa arvoja:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
static void argsAndRetsExample() {
    lua_State* lua = luaL_newstate();
    luaL_openlibs(lua);

    const char* script = "\
local arg1, arg2 = ...\n\
print(\"Lua function was called with \" .. arg1 .. \" and \" .. arg2)\n\
local ret1 = arg1 + arg2\n\
local ret2 = arg1 * arg2\n\
return ret1, ret2\n\
";
    luaL_loadstring(lua, script);

    lua_Number arg1 = 3.7, arg2 = -.1;
    printf("calling Lua function with %f and %f\n", arg1, arg2);
    lua_pushnumber(lua, arg1);
    lua_pushnumber(lua, arg2);
    int err = lua_pcall(lua, 2, 2, 0);
    if(err == LUA_OK) {
        lua_Number ret1 = lua_tonumber(lua, 1);
        lua_Number ret2 = lua_tonumber(lua, 2);
        printf("Lua function returned %f and %f\n", ret1, ret2);
    } else {
        const char* errMsg = lua_tostring(lua, -1);
        printf("Error while calling Lua function: %s\n", errMsg);
    }

    lua_close(lua);
}

Ajettaessa tämä tulostaa seuraavaa:

calling Lua function with 3.700000 and -0.100000
Lua function was called with 3.7 and -0.1
Lua function returned 3.600000 and -0.370000

Tarkastellaan ensin Lua-koodia. LuaL_loadstring()-funktiolla ladattu Lua-funktio on tyypiltään nk. vararg-funktio. Kaikki sen saamat argumentit on luettavissa vararg-lausekkeesta joka on muodoltaan kolme peräkkäistä pistettä (...). Lause local arg1, arg2 = ... rivillä 6 asettaa (paikallisen) muuttujan arg1 arvoksi funktion ensimmäisen argumentin ja muuttujan arg2 arvoksi toisen argumentin. Jos argumentteja ei ole tarpeeksi, muuttujan arvoksi tulee nil. Rivillä 10 Lua-funktio palauttaa arvoja. Lua-funktio voi palauttaa useita arvoja, tässä tapauksessa palautetaan kaksi arvoa.

Rivillä 12 ladataan funktio kuten edellä. Funktion argumentit työnnetään pinoon funktion jälkeen. Tässä kutsumme funktiota kahdella argumentilla. Riveillä 16 ja 17 työnnetään argumentit pinoon. Rivillä 18 suoritetaan kutsu. Argumenttien määrä on 2, ja haluamme enintään 2 paluuarvoa. Kutsun onnistuttua lukemme riveillä 20 ja 21 paluuarvot pinon pohjalta (funktio ja argumentit ovat poistuneet pinosta).

C-funktioiden kutsuminen Lua-koodista

Esimerkki C-funktion kutsumisesta:

1
2
3
4
static int myCFunction(lua_State* lua) {
    int numArgs = lua_gettop(lua);
    printf("myCFunction called with %d args\n", numArgs);
    for(int i = 1; i

Ajettaessa tämä tulostaa seuraavaa:

myCFunction called with 2 args
arg 1: 5
arg 2: bacon
myCFuntion returned 3.141592 and second retvalue

Lua-koodista kutsuttava C-funktio on muuten samankaltainen kuin mikä tahansa C-funktio, mutta se saa argumenttinsa ja palauttaa arvonsa Lua-pinon kautta. Kaikkien Luasta kutsuttavien funktioiden on otettava yksi argumentti tyypiltään lua_State* ja palautettava int. Kun funktiota kutsutaan, annetussa lua_Statessa on pinossa funktion saamat argumentit (ensimmäinen pinon pohjimmaisena jne.), eikä mitään muuta. Argumenttien määrä saadaan tietää pinon korkeudesta. Argumenttien tyypit voidaan myös selvittää. Funktio palauttaa arvoja siten, että kaikki paluuarvot työnnetään pinoon ensimmäisestä aloittaen ja funktion varsinainen paluuarvo (C-mielessä) kertoo Lua-paluuarvojen määrän. Pinoa ei tarvitse erikseen “puhdistaa” ennen palaamista.

Rivillä 2 selvitetään argumenttien määrä. Riveillä 4-6 oleva silmukka tulostaa kaikki funktion saamat argumentit (muutettuaan ne ensin merkkijonomuotoon). Funktio palauttaa kolme arvoa; ne työnnetään järjestyksessä pinoon riveillä 8-10. Rivillä 11 palautetaan paluuarvojen määärä.

Riveillä 18-21 on taulukko, jonka avulla C-funktio liitetään Luaan. Taulukossa on tietueita, joissa ensimmäinen arvo on funktion nimi Luassa ja toinen arvo on pointteri varsinaiseen funktioon. Viimeisenä on on tietue, jossa molemmat arvot ovat NULL. Tässä liitetään vain yksi funktio, mutta samalla voisi liittää useammankin funktion. Rivi 23 lukee em. taulukon ja luo Lua-taulukon, jossa on annetuilla nimillä viittaukset annettuihin C-funktioihin. Tässä tapauksessa siis (Lua-)taulukko, jossa on arvolla "myCFunction" viittaus rivin 1 funktioon. Rivillä 24 tämä taulukko annetaan globaalin muuttujan "myCFunctions"-arvoksi.

Lua-koodissa rivillä 27 kutsutaan "myCFunctions"-taulukon "myCFunction"-jäsentä, ts. edellä määriteltyä C-funktiota. Funktiokutsu tehdään kuten muutkin Lua-funktiokutsut. Tässä funktiolle annetaan 2 argumenttia. Funktion 2 ensimmäistä paluuarvoa asetetaan paikallisten muuttujien a ja b arvoiksi. Kuten tulosteesta huomataan, funktion kolmas paluuarvo jää huomiotta.

Voit kokeilla panna breakpointin myCFunction()-funktioon. Ohjelman pysähtyessä ko. breakpointiin, voit tarkastella kutsupinoa ja huomata, että kutsu on tullut Lua-tulkin sisältä.

Windows Phone -sovelluskehitys Androidin ja iPhonen rinnalla

Windows Phonen astuminen iOS:n ja Androidin rinnalle on saanut aikaan hieman hämmennystä joidenkin loppukäyttäjien ja sovellusten tilaajienkin mielissä. Siinä missä iOS (iPhone/iPad), Android ja jopa poistuva Symbian muistuttavat toisiaan yleisasultaan – kukin tietenkin erottautuen omilla pienillä eroavaisuuksillaan – Windows Phone on tietoisesti suuntautunut erilleen muista omalla Metro-suunnittelukielellään.

Ohje: maassa maan tavalla

Suunnitellessa uutta sovellusta mille tahansa alustalle, yleispätevänä ohjenuorana voitaneen antaa:

Kunkin tyyppisen mobiililaitteen käyttäjä haluaa systemaattisen käyttäjäkokemuksen ja käyttöjärjestelmälle tunnusomaisen sekä varsin yhdenmukaisen ulkoasun sovelluksille. Sovelluksen käyttöönotto on tällöin mahdollisimman luontevaa eikä vaadi syvällistä perehdytystä.

Tämä halu kumpuaa usein niin käyttäjien puolelta kuin myös käyttöjärjestelmien toimittajilta! Jotkut palvelujen kehittävät mieltävät ajattelevansa käyttäjän parasta pyrkimällä mahdollisimman yhdenmukaiseen käyttöliittymään laitteen käyttöjärjestelmästä riippumatta, mutta päätyvät tekemään käyttäjälle karhunpalveluksen. On poikkeustilanne, että ihmiset käyttävät samaa sovellusta eri käyttöjärjestelmillä, mutta varmaa, että he käyttävät sitä muiden saman käyttöjärjestelmän sovellusten lomassa.

Suunniteltaessa sovellusta usealle alustalle täysin samaa konseptia ei siis voi aina käyttää. Vaikka iPhone ja Android-sovellukset voivat muistuttaa toisiaan hämmentävän paljon, niillä on eronsa. WP:llä erot ovat muihin alustoihin verrattuna huomattavat.

Esimerkki

Alla käytännön vertailuesimerkki IMDB-sovelluksesta Applen iPhonelle ja Windows Phonelle.

Kuva 1. iPhone versio IMDB:n sovelluksesta.

Kuva 2. WP-versio IMDB-sovelluksesta.

Edellä esitetyt kuvat osoittavat selvästi, että iPhonen käyttäjät eivät pitäisi WP:n tyylisestä sovelluksesta puhelimessaan (mikäli se edes pääsisi Applen App Storeen), eivätkä vastaavasti Windows Phone -käyttäjät halua iPhone/Android-tyylistä sovellusta laitteilleen. Toisilleen vieraat esitystavat vaativat sovelluksen arkkitehdilta jo konseptin varhaisessa vaiheessa ydinidean hahmottamiskykyä ja näkemystä mitä kukin loppukäyttäjä haluaa siltä.

Microsoftin sivuilta löytyy (englanniksi) esimerkkejä erilaisista sovellustyypeistä Windows Phonella.

Päätelmä

Metro-tyyliohjeita noudattavat Windows Phone -sovellukset ovat niin toimintalogiikaltaan kuin ulkoasultaan selvästi erilaisia kuin iOS/Android-taustaiset sovellukset, vaikka tarjottava toiminnallisuus olisi sama.

Windows Phonen tarjoaman visuaalisesti rikkaan Panorama-näkymän avulla yritysten brändäys ja yleensäkin halutut teemat sovelluksissa voidaan saada muita alustoja vahvemmin esille (kuva 2). Tämä toki vaatii ylimääräisen graafisen aineiston luomista.

Vaikka Windows Phonella vältetään käyttöliittymäelementtien ylimääräisiä koristeita, kuten liukuvärejä, tekstuureita, varjostuksia ja kiiltoefektejä, sisältö pyritään vastaavasti tuomaan esiin niin, että lopputulos voi hyvin olla kauniimpi kuin muilla alustoilla.

 

 

Apuohjelmia iOS-designereille

Xcode on asennettu, mitä nyt? Tässä kokoelma apuohjelmia ja muita resursseja, jotka helpottavat iOS-sovellusten kehitystä ja erityisesti käyttöliittymäsuunnittelua.

Templaattikokoelmat

Omnigraffle on suosikkityökaluni rautalankojen laatimiseen. Da5id:n kokoelma on paras tietämäni iOS-templaatti OmniGrafflelle.

Tuotantokelpoista jälkeä tarvittaessa Teehan & Lax:n Photoshop-kokoelma on mainio. Kannattaa malttaa nähdä sen verran vaivaa, että tekee 1x-grafiikat sille tarkoitetun tyylipohjan avulla, niin kuvat pysyvät mahdollisimman tarkkoina.

Liveview (ilmainen)

Liveview näyttää iPhone tai iPadin ruudulla valitun alueen tietokoneen työpöydältä. Tämä on hyödyllistä, jos haluaa tarkistaa, miltä grafiikat näyttävät retina-näytöllä ja että tekstit ovat varmasti luettavissa.

Sovelluksen avulla voi toteuttaa myös yksinkertaisen wizard of oz -demon operoimalla näkymiä tietokoneelta ja antamalla puhelimen käyttäjän käteen.

iExplorer (ilmainen)

 iOS-laitteissa ei ole tiedostojenhallintaa. Joskus on kuitenkin hyödyllistä nähdä, mitä tiedostoja minnekin kertyy. iExplorer on Mac-ohjelma, jolla pääsee selaamaan puhelimensa tiedostorakennetta.

xScope (29,99 $)

xScope tarjoaa vastaavan toiminnallisuuden kuin Liveview. Lisäksi sen avulla voi mittailla eri elementtien kokoja tietokoneen näytöltä ja lisätä apuviivoja. Tuleepa mukana virtuaalinen viivoitinkin. Apuviivoista on apua esim. Xcodessa, jonka omat viivat toimivat vähän kankeasti.

Ostin sovelluksen aikoinaan alennusmyynistä. Täysi hinta on hieman tyyris. Jos tarvitset vain viivoitinta, katso Free Ruler. Se on ilmainen, mutta toimii vähän bugisesti uusilla käyttöjärjestelmillä.

Mittaamisesta puheen ollen: itse käytän jatkuvasti kuvakaappausnäppäinkomentoa eri asioiden koon mittaamiseen. Painamalla komento + shift + 4 voi ottaa kuvakaappauksen halutusta näytön alueesta. Ominaisuutta voi käyttää myös elementtien kokojen selvittämiseen – ja kun painaa esciä ennen kuin päästää hiireen irti, säästää maailman turhalta kuvaruutukaappaukselta.

Hex color picker (ilmainen)

 Kuvakaapparin lisäksi Mac OS X:ssä on mainio pipettityökalu värien poimimiseen. Sen käyttöliittymä on peruja Next-ajoilta, ja pelkään pahoin, että moni ei edes tiedä sen olemassaolosta. Suurennuslasia klikkaamalla voi valita haluamansa värin kuin Photoshopin pipetillä. Ja mikä parasta, tämä toimii myös ohjelmien välillä.

Kätevää on myös siirtää värejä sovellusten välillä raahaamalla värin valitsimen alalaitaan.

Mac OS X:n värivalitsinta voi tehostaa asentamalla Hex color picker -laajennuksen. Sen avulla saa näkyviin suoraan värin heksa-arvon, mikä on hyödyksi myös web-suunnittelussa.

Skitch (ilmainen)

 Vakavamielisempään kuvakaappausten ottamiseen kannattaa hankkia Skitch. Se on yksi ohjelmista, joita ei voi suositella kylliksi. Kuvakaappausten ottamisen lisäksi Skitchillä voi tehdä niihin kätevästi merkintöjä: tekstiä, nuolia, vapaita tuherruksia. Paras puoli on kuvien jakaminen. Jos kuvan raahaa pois Skitchistä, siitä luodaan automaattisesti tiedosto kovalevylle ilman että kuvaa joutuisi itse erikseen tallentamaan.

Jos haluaa vaikkapa lisätä Jiraan kuvakaappauksen löytämästään bugista, ei tarvitse ensin tallentaa tiedostoa ennen kuvan lataamista selaimeen ja sitten etsiä uudelleen avaa-dialogissa, vaan riittää, että raahaa sen selaimeen suoraan Skitchistä. Kuvien lataaminen kaikkien nähtäville skitch.comiin käy sekin yhdellä klikkauksella.

Screenshot Journal (1,59 €)

Tietokoneen lisäksi kuvakaappauksia tulee helposti ottaneeksi valtavat määrät puhelimelta. Valitettavasti Apple on katsonut viisaaksi näyttää kuvakaappaukset samassa läjässä puhelimella otettujen valokuvien kanssa. Apuun tulee Screenshot Journal, joka poimii kuvakaappaukset erilleen kuvavirrasta ja tarjoaa keinoja kuvien hallintaan.

Jos tykkää nahkaefekteistä ja tikkauksista, sovellukseen kannattaa tutustua jo niiden vuoksi. Ovat keskimääräistä hienommat.

Kuvansiirtäjä

Mac OS X:n mukana tuleva kuvansiirtäjä ei ole tainnut saada ainuttakaan päivitystä sitten ensimmäisen Mac OS X -version, ja käyttöliittymä on muutamilta osin rasittava. Jos puhelimelta haluaa noutaa kuvakaappauksia, se on kuitenkin kevyempi vaihtoehto kuin iPhoto. Jos tiedät Kuvansiirtäjää paremman vaihtoehdon, kerro toki kommenteissa!

iOS-Simulator Cropper (ilmainen)

Laitteella kuvakaappauksien ottaminen on kaikesta huolimatta vähän jähmeää. Käyttöohjeita, promokuvia yms. varten on usein kätevämpää  ottaa kuvakaappaukset suoraan Xcoden Simulator-ohjelmasta. iOS-Simulator Cropper ottaa kuvakaappauksen pelkästään simulaattorissa pyörivästä ohjelmasta, nimeää sen halutulla tavalla ja lisää haluttaessa kuvan ympärille iPhone-kuoret kiiltoineen kaikkineen.

Myös status barin poistaminen kuvan ylälaidasta onnistuu helposti, jos vaikka haluaa lisätä kuvakaappaukset App Storeen Applen ohjeiden mukaan ilman palkkia.

iPhone Configuration Utility (ilmainen)

Sovellusten ja provisiointiprofiilien asentaminen onnistuu periaatteessa iTunesilla, mutta sillä on paha tapa synkronoida valtavia datamassoja joka välissä heti kun se löytää koneeseen kytketyn puhelimen. Kätevämpi työkalu on Applen tarjoama iPhone Configuration Utilyty. Sen avulla voi mm. asentaa .IPA-muotoisia sovelluksia laitteelleen, kun sellainen tarve tulee. Saatavilla myös Windowsille.

Quicklook plugin for provisioning profile files (ilmainen)

Provisiointiprofiilien kanssa tappelu on iOS-kehityksessä väistämättä vastaantuleva hupi. Tuskaa vähentää, jos voi kätevästi varmistua, että sovelluksen pitäisi teoriassa asentua halutulle laitteelle. Tämä kätevä lisäpalikka antaa Finderille kyvyn näyttää suoraan, mitkä testilaitteet sovelluksen provisiointiprofiiliin on lisätty.

UDID (ilmainen)

Jotta voisi varmistua, että oma puhelin on tuettu, pitäisi tietää puhelimen UDID-koodi. Sen saa selvitettyä iTunesilla, mutta Apple on tehnyt sen siinä määrin hankalaksi, että on usein helpompaa neuvoa ihmisiä asentamaan puhelimeen sovellus tätä varten. UDID on yksi näistä. Parempiakin saattaa olla.

Picturesque (11,99 €)

Törmäsin Picturesquehun vasta, mutta se näyttää lupaavalta. iPhone-sovelluksia tykätään usein esitellä heijastuksin je perspektiivitempuin kuorrutettuina. Picturesque tekee nämä yleisimmät efektit helposti ja antaa tallentaa kuvat myös läpinäkyvällä taustalla. Kätevä, jos kaipaat vapautusta Photoshopista.

 

CGAffineTransformation ja UI elementit

Affiinit kuvaukset ovat käytännössä pakon sanelemina tuttuja kaikille 3D grafiikkaohjelmointia harrastaneille mutta ne ovat aivan yhtä hyodyllisiä (joskaan eivät samalla tavalla edellytys) tasossa toimiessa. iOS:n Cocoa Touch tarjoaa useita tapoja manipuloida UIView:ien (jokaisen näkyvän UI widgetin kantaluokka) paikkaa; framen sijainnin muuttaminen taitaa olla päivittäisessa käytössä yleisin tapa ja se onkin riittävä ja oikea valinta suurimpaan osaan tapauksia.Mutta jos elementtejä halutaan skaalata tai kiertää (rotate), tarvitaan muita konsteja.

CGAffineTransformation esittää 3×3 matriisia jolla tason (2D) affiinit kuvaukset voidaan esittää. UIView luokalla on transformation niminen property, jonka animoitavuus tekee affiineista kuvauksista erittäin voimakkaan työkalun. Esimerkiksi UIView:n siirtäminen koordinaatteihin (tx,ty) tapahtuu CGAffineTransformation:n avulla seuraavasti:

CGAffineTransform transform = CGAffineTransformMakeTranslation(tx, ty);
view.transform = transform;

Jos on tottunut siirtelemään elementtejä muokkaamalla framen sijaintia, kannattaa huomioida että CGAffineTransformation:lla tehtävät siirrot (translation) kohdistuvat elementin keskipisteeseen eivätkä esim. sen vasempaan yläkulmaan.

Kuvauksia voi (ja pitää) yhdistää; usean operaation lopputulos mahtuu yhteen samaan kuvausmatriisiin sen ominaisuuksien vuoksi. Eri kuvausmatriisit kerrotaan yhteen oikeassa järjestyksessä ja lopputuloksella “kuvataan” (transform, v.)  elementti uuteen sijaintiin, skaalaan ja/tai orientaatioon. Esimerkiksi voidaan yhdistää translate – scale – rotate:

CGAffineTransform transform = CGAffineTransformIdentity;
transform = CGAffineTransformTranslate(transform, tx, ty);
transform = CGAffineTransformScale(transform, kScaleFactor, kScaleFactor);
transform = CGAffineTransformRotate(transform, kRotationAngle * M_PI / 180.0);
view.transform = transform;

Kuten aina affiinien kuvausten kanssa, on huomioitava operaatioiden järjestys; translaatio ensin, rotaatio sitten -järjestys siirtää kappaletta origosta ja sen jälkeen kiertää sitä kehää pitkin; rotaatio ensin, translaatio sitten -järjestys kiertää kappaletta oman keskipisteensä ympäri ja sen jälkeen siirtää kappaletta uuteen paikkaansa.

Esimerkkitoteutus

Ohessa yksinkertainen single-view sovellus joka pyrkii esittelemään yllä läpikäydyn asian joustavuuden ja voiman visualisoimalla ristinollaruudukon ja 5 kpl kumpaakin pelimerkkia O / X; koskettamalla pelimerkkiä kyseinen merkki tarttuu mukaan ja sille tehdään pieni skaalaus / rotaatio ihan vain demonstraation takia. Kaikki kappaleen manipulaatio tehdään CGAffineTransformation:lla.

Esimerkkisovelluksen ruutukaappaus
Esimerkkisovelluksen ruutukaappaus

Esimerkkisovelluksen lähdekoodi Xcode projektina (42kB)

 

 

 

 

 

 

ps. jos termi “affiini kuvaus” tuntuu kryptiseltä niin käytännössä sen määritelmä on että kys. operaatiossa “kuvattava” eli transformoitava kappale ei muuta mittasuhteitaan tai muotoaan; neliö pysyy neliönä niin sitä siirrettäessä, skaalatessa kuin kierrettäessäkin.

Lisää aiheesta

 

Mobiiliselaimen tunnistaminen CSS:llä

Selainten tunnistaminen tulee monesti kuvioihin silloin, kun joko sivun tyylejä, logiikkaa tai sisältöä muokataan sopimaan erikokoisille näytöille tai mobiiliselaimelle. Tähän on olemassa monenlaisia tekniikoita, ja tässä blogipostauksessa esittelen tavan tehdä tämä CSS-sääntöjen avustuksella.

Media Queryn käyttö

CSS:ssä pystyy määrittämään CSS-tyylien säännöt sen perusteella, minkä levyinen ikkuna tai laite on.Tämä onnistuu CSS:n media query -ominaisuuden avulla alla kerrotulla tavalla. Parametreina voidaan käyttää esimerkiksi laitteen leveyttä ja korkeutta sekä pikselitiheyttä.

1. CSS-tyylitiedoston aktivoiminen headissa ehtojen perusteella

<link rel="stylesheet" media="screen and (min-device-width: 320px) and (min-device-pixel-ratio:2)" href="retina-styles.css"/>

2. CSS-säännön aktivointi ehtojen perusteella

@media screen and (min-device-width: 320px) and (max-device-width: 480px) and (min-device-pixel-ratio:2) and (-webkit-min-device-pixel-ratio:2) and (-moz-min-device-pixel-ratio:2) {
display:block;
}

3. CSS-tiedoston sisällyttäminen toiseen CSS-tiedostoon

@import url(styles-retina-iphone.css) screen and (min-device-width: 320px) and (max-device-width: 480px) and (min-device-pixel-ratio:2) and (-webkit-min-device-pixel-ratio:2) and (-moz-min-device-pixel-ratio:2)

Mobiilikehityksen kannalta tärkeimpiä parametrejä ovat:
min-device-width ja max-device-width, jotka rajoittavat tyylin käyttöä tietyn levyisille näytöille. Näillä saadaan sivuston tyylit mukautumaan mobiilinäytölle.

min-device-pixel-ratio tarkoittaa näytön “pikselitiheyttä”. Tyypillisen tietokoneen näytön ja peruspuhelimen arvo on 1, mutta esimerkiksi iPhone 4:llä ja iPhone 4S:llä tämä arvo on 2. Selitys tälle on ns. retina-näytössä: näytön koko on iPhone 3G:ssä ja iPhone 4:ssä on sama, mutta näytön leveys ja korkeus on kaksinkertainen. Tällöin sivustolla olevat kuvat näkyvät iPhone 4:n tarkemmalla näytöllä pikselöityneenä ja epätarkkoina, sillä käytetyn grafiikan resoluutio on liian pieni. Tämän säännön avulla voidaankin saada käyttöön tarkemmat grafiikat siihen pystyville näytöille.

@media handheld on kiinnostava parametri, mutta sen selaintuki on valitettavan rajallinen. Esimerkiksi Safari jättää tämän parametrin kokonaan huomiotta, joten parametri ei tunnista iPhonea. Tätä parametria en siis suosittele käytettävän.

orientation tarkoittaa puhelimen asentoa, eli onko puhelin vaaka- vai pystysuunnassa. orientation:landscape tarkoittaa että puhelinta pidetään vaakasuunnassa, kun taas orientation:portrait tarkoittaa pystysuuntaa.

Media query CSS3:n tuoma uusi ominaisuus, joka on jo kuitenkin nykyään erittäin laajasti tuettu. Ajantasaisen listan tuetuista selaimista löydät caniuse.com-sivustolta.

Media Queryt Qvikin kotisivuilla

Qvikin kotisivuilla on käytetty media queryjä sovittamaan sivuston ulkoasu näytön koon mukaiseksi. Sivustolla käytetään kolmea eri moodia: täysileveä, portrait ja mobiili. Täysileveässä moodissa käytettävät tyylit määritellään niin, että sen leveys on vähintään 1024 pikseliä:


@media screen and (min-device-width:1024px) {
...
}

Tällä leveydellä sivun kokonaisleveys on 1000 pikseliä. Yksi sivuston käyttöliittymäelementeistä on neljän, kiinteän leveyden laatikon ‘gridi’ joka näyttää kokoleveällä näytöllä tältä:

Jos tämä osuus näytettäisiin tällaisenaan pienemmällä näytöllä, boksit joko leikkaantuisivat, tai niitä joutuisi skrollaamaan ja zoomaamaan. Esimerkiksi tableteille (kuten iPad pystymoodissa, jonka leveys 768px) on tässä tapauksessa käytännöllistä esittää tämä gridi eri tavalla. Tyylitiedostossa nämä tyylit määritellään seuraavasti:


@media screen and (min-device-width:321) and (max-device-width:999) {

}

Tällä tavalla määritellyt tyylitiedostot näkyvät esimerkiksi iPhonen vaakatilassa ja iPadin pystytilassa. Alla kuvankaappaus Qvikin kotisivuilla:

Laatikot on asetettu 2×2 gridiin, jolloin ne ovat luettavia. Koska laatikoiden tarkoitus on tässä tapauksessa pysyä tasalevyisenä, niin ne eivät vie enää koko ruutua tilaa. On kuitenkin täysin mahdollista tehdä niin, että kaksi laatikkoa veisivät aina sen 50% ikkunan leveydestä. Tällöin niiden leveys määritellään CSS:ssä prosenttilukuna sen containerista.

Qvikin sivut on tehty myös näyttämään käytännölliseltä myös mobiililaitteella, eli 320px tai kapeammilla näytöillä:

Lisätietoa media queryjen käytöstä löytyy englanniksi W3C:n sivuilta.

Mitä voimme oppia Facebook-sovelluksen kotinäkymän kehityksestä?

Applen tarjoama standardiratkaisu iPhone-sovelluksen osiosta toiseen liikkumiseen on enimmillään viisipaikkainen tab bar. Jos on tarvetta useammalle osiolle, viimeiseksi valinnaksi voidaan laittaa kolme pistettä, ja sijoittaa tämän taakse listamuodossa lisäkohteita.

Apple tulee omissa sovelluksissaan toimeen tällä ratkaisulla, mutta usein on tarvetta toteuttaa navigaatio jotenkin muuten. Tärkeitä kohteita saattaa olla enemmän kuin viisi tai näytön pinta-ala haluttaisiin tehokkaammin käyttöön, eikä näytön alalaitaa raaskita varata navigointipalkille.

Facebookin iPhone-sovellus käytti alkuaikoinaan myös tätä ratkaisua. Ohessa kuvakaappaus versiosta 2.0 (cnet.com).

Kuvassa pistää erityisesti silmään vaikeakäyttöinen vieritettävä navigointipalkki välilehtirakenteen alapuolella. Muistan, ettei sen kanssa ollut mukava näpertää. Tab bar, välilehdet, vieritettävä palkki – on siinä hierarkiaa kerrakseen! Ei ihme, että Facebook päätti panna navigaation uusiksi sovelluksen 3.0-versiossa.

Versiossa 3.0 esiteltiin sittemmin varsin laajasti käytetty kotinäkymäpatterni, jossa päänavigaatio on koko ruudun kokoinen. Skaalautuminen hoidetaan samaan tapaan kuin iPhonen kotinäytöllä, näyttö kerrallaan vaakasuunnassa vierittämällä. (kuva Ars Technica)

Etuna on, että kohteita mahtuu näkyviin enemmän kuin tab bariin ja näkymästä saadaan visuaalisempi. Valikko ei myöskään haaskaa tilaa muilta näkymiltä. Haittapuolena on, että valikko täytyy avata erillisellä painalluksella.

Päädyimme vastaavaan ratkaisuun viime kesänä myös Taloussanomien iPhone-version kanssa. Ensin toteutetussa iPad-versiossa tärkeimmät osiot olivat mahtuneet näkyviin vaakasuuntaiseen palkkiin, mutta puhelimen ruudulla olisi tullut ahdasta emmekä halunneet tuoda ylimääräistä palkkia pystysuuntaista tilaa viemään, joten toteutimme kotinäkymäratkaisun.

Facebookiin tapaan sovellus aukeaa ensisijaisesti tärkeimpään uutisnäkymään, ja kotinäkymään pääsee erikseen painamalla. Käytimme painikkeen symbolina Facebookista poiketen talon kuvaa, sillä Facebookin käyttämä kuva toi liikaa mieleen list/grid-painikkeen.

Pian saimme huomata, että ratkaisu oli juuri mennyt muodista. Facebookin nykyinen versio toi mukanaan uuden version kotinäkymästä. Se ei enää olekaan erillinen koko ruudun näkymä, vaan näytön reunasta aukeava osittain valitun näkymän alle peittoon jäävä lista. Vierityssuunnaksi on valittu jälleen Applen standardin more-näkymän tapaan pystysuunta.

Jos vanha kotinappi näytti grid-valinnalta, uusi näyttää vastaavasti list-napilta…

Raa’asti painallusten määrällä analysoiden tämä lähestymistapa ei ole sen tehokkaampi kuin aiempi koko ruudun kotinäkymä. Yhtä lailla se vaatii yhden painalluksen aueatakseen ja samaan tapaan se estää nykyisen näkymän käytön valikon ollessa auki. Henkinen etäisyys osittain nykyisen näytön takaa aukeavaan valikkoon sen sijaan tuntuu lyhyemmältä.

Valikon vaikutelma ei ole yhtä visuaalinen kuin koko ruudun kotinäkymän ja mikä yllättävintä, listaan ei mahdu sen enempää kohteitatakaan. Aiempaan kotinäkymään mahtuu hakuluukku ja yhdeksän kohdetta. Uudessa listassa on hakuluukun lisäksi vain kahdeksan kohdetta.

Niin tai näin, uusi patterni on jo osoittautunut suosituksi. Google on ottanut sen käyttöönsa iPhonen Gmail-sovelluksessa oleellisella parannuksella: valikon saa näkyviin ja piiloon valikkonapin lisäksi sormella pyyhkäisemällä.

Viimeiseen asti hiotusta käyttöliittymästään tunnettu oman elämänsä facebook Path on toteuttanut kenties hienoimman version tästä lähestymistavasta. Valikko aukeaa Gmailin tapaan pyyhkäisyeleellä, mutta toimii huomattavasti pehmeämmin joustopomppuineen kaikkineen. Vastaavasti osiosta toiseen siirryttäessä se animoituu hetkeksi pois näkyvistä ja palaa takasin niin, että näkymä on jo valmiiksi piirretty.

Android-listat ja -adapterit

Näyttäessämme tietoa laitteen näytöllä joudumme usein turvautumaan listoihin. Emme usein tiedä kuinka monta riviä listassa on ja ne voivat ohjelman ajoaikana muuttua. Lähes jokaisessa ohjelmassa joutuu käyttämään jotain listaa, gridiä, galleriaa tai jotain muuta Androidin AdapterViewn toteuttavaa luokkaa.

Kaikki luokat jotka jatkavat AdapterViewtä on tarkoitettu näyttämään tietoa selkeissä riveissä (list), sarakkeissa (gallery) tai ruudussa (grid). AdapterView tarvitsee toimiakseen Adapterin, jolle on annettu tarvittavat objektit. Adapterin ei kuitenkaan tule itse hallinnoida objekteja vaan säilyttää vain viittaus niihin. Objektien säilyttäminen on tämän artikkelin asian ulkopuolella. Tässä artikkelissa käydään läpi kuinka Adapterit toimivat ja mikä on paras käytäntö niiden tekemisessä. Kiinnitämme myös huomiota listan ja sen myötä myös adapterin muistin kulutukseen, sillä esimerkiksi kuvien näyttäminen listassa aiheuttaa herkästi muistin ylittämisen ja ohjelman kaatumisen.

Kuinka tehdä Adapteri

Adapteria tehdessä suosittelen käyttämään yliluokkana ArrayAdapter<näytettävä objekti> luokkaa, joka toteuttaa jo lähes kaikki tarvitsemasi metodit. Sinun tarvitsee vain käsitellä, miten jokainen adapterille antamasi objekti näytetään getView()-metodissa.

Toimivan adapterin tekemiseen sinun täytyy jatkaa ArrayAdapteria ja toteuttaa tälle adapterille construktori ja haluamasi lainen getView()-metodi. Tässä esimerkki yksinkertaisesta adapterista, joka näyttää objektin tekstiä jokaisella adapteria käyttävän AdapterView:n alkiossa.

public class TestAdapter extends ArrayAdapter<YourObject> {
public TestAdapter(Context context, int textViewResourceId, List<YourObject> objects) {
    super(context, textViewResourceId, objects);
} 

// Luo AdapterViewn ilmentymän alkio tallennetusta objektista
@Override
public View getView(int position, View convertView, ViewGroup parent) {
    // Adapteri uudelleen käyttää listassa olevia näkymiä uudelleen,
    // mutta listaa luotaessa nämä ovat null:ja ja tarvitsee alustaa.
    // Jos listan näkymä on jo asetettu ei sitä tarvitse enää uudelleen luoda
    // tehokkaassa adapterissa tämä luonti jätetään väliin, kun sitä ei tarvita.
    if (convertView == null) {
        LayoutInflater inflater = LayoutInflater.from(getContext());
        convertView = inflater.inflate(R.layout.your_adapter_view_layout, null);
    }
    // tämän jäälkeen tarvitsee vain ottaa oikeasta adapterin alkiosta tarvittavat tiedot
    //  ja asettaa ne näkymään oikeille paikoille.
    YourObject item = getItem(position);
    // Huomaa että tekstikentän etsiminen vie turhaan tehoja ja aiheuttaa tarpeettomia viittauksia
    TextView text = (TextView)
    convertView.findViewById(R.id.adapter_item_textview);
    text.setText(YourObject.getText()); 

    return convertView; }
}

 

ViewHolder pattern: Lisää adapterin suorituskykyä

ViewHolder-malli tehostaa Adapterien käyttöä poistamalla tarpeen etsiä tarvittavat näkymä widgetit uudelleen getView()-metodissa. Mallin idea on luoda uudelleen käytettävä, Adapterin sisäinen, ViewHolder luokka, joka asetetaan uudelleenkäytettävän convertViewn sisälle tagiin. ViewHolder pitää sisällään referenssit oman convertViewnsä käytettyihin widgetteihin. Tämä poistaa widgettien etsimisen findViewById()-metodilla.

Myös jos tekstikentille tarvitsee lisätä kuuntelijoita riittää että ne tehdään vain kerran ViewHolderia luotaessa. Adapterisi toteuttaa ViewHolder mallin esimerkiksi näin:

// Luo AdapterViewn ilmentymän alkio tallennetusta objektista
@Override
public View getView(int position, View convertView, ViewGroup parent) {
    if (convertView == null) {
        LayoutInflater inflater = LayoutInflater.from(getContext());
        convertView = inflater.inflate(R.layout.your_adapter_view_layout, parent, false);

        // Luo ja lisää ViewHolder omalle näkymälleen tagiksi
        convertView.setTag(new ViewHolder(convertView));
    }
    ViewHolder viewHolder = (ViewHolder) convertView.getTag();
    YourObject item = getItem(position);

    viewHolder.text.setText(item.getText());
    viewHolder.image.setImageDrawable(item.getImageDrawable());

    return convertView;
}

private static class ViewHolder {
    final TextView text;
    final ImageView image;

    public ViewHolder(View convertView) {
        text = (TextView) convertView.findViewById(R.id.adapter_item_textview);
        image = (ImageView) convertView.findViewById(R.id.adapter_item_imageview);
    }
}

Monimutkaisemmissa riveissä ViewHoldering käyttäminen helpottaa ja tehostaa listan käyttämistä, koska vaikka oikean näkymän etsiminen convertView:stä kestää vain muutamia millisekuntteja, jos sen tekee esimerkiksi kymmenen kertaa voi lista nykiä selvästi kovassa käytössä.

Vinkki:

Listat ovat hyviä näyttämään tietoa, mutta vältä tiedon muokkauksen mahdollistamista listojen tai muiden AdapterView:n sisällä, esimerkiksi EditText widgeteillä. Tallentaminen ja oikean tiedon näyttäminen, kun listan näkymiä uudelleenkäytetään tulee todella vaikeaksi.