lunedì 11 aprile 2011

Android: GridView


GridView è un oggetto ViewGroup, come ListView. La particolarità di questo componente è ovviamente il layout applicato agli elementi di una sorgente di dati. GridView, infatti, dispone i dati all'interno di una griglia scorrevole. L'oggetto può essere dichiarato all'interno dei file xml, attraverso l'omonimo tag GridView, che all'interno del codice Java, attraverso l'omonima classe del package android.widget. GridView dispone di diversi attributi per la regolazione delle proprietà di layout:
  • android:columnWidth specifica la larghezza delle colonne, la stessa cosa viene fatta all'interno del codice con il metodo setColumnWidth(int);
  • android:horizontalSpacing specifica lo spazio vuoto fra le righe della griglia, nel codice Java va usato il metodo setHorizontalSpacing(int);
  • android:verticalSpacing specifica lo spazio vuoto fra le colonne della griglia, nel codice Java va usato il metodo setVerticalSpacing(int);
  • android:stretchMode specifica come gli oggetti devono occupare lo spazio messo a disposizione dalla griglia, nel codice Java va usato il metodo setStretchMode(int) a cui va passata una delle possibili costanti definite all'interno della classe (come ad esempio NO_STRETCH e STRETCH_COLUMN_WIDTH);
Trovate altre informazioni nella guida in linea per Android. Nell'esempio che vi propongo l'oggetto GridView viene utilizzato per la raccolta di immagini che compongono un puzzle. Il click su una immagine riassegna all'oggetto nella griglia una nuova immagine, prendendola dal set di pezzi che compone il puzzle.
Il layout principale dell'activity descrive un bottone per il reset (che riassegna nuovi tasselli alla griglia) e la griglia per le immagini. Vi riporto il pezzo di codice che descrive l'oggetto GridView:
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/PuzzleGridView"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:columnWidth="62dp"
android:numColumns="3"
android:verticalSpacing="2dp"
android:horizontalSpacing="2dp"
android:stretchMode="columnWidth"
android:gravity="center"
Trovate il codice sorgente dell'intera applicazione a questo indirizzo. Il codice del metodo onCreate() è il seguente:
public void onCreate(Bundle savedInstanceState) {
 super.onCreate(savedInstanceState);
 setContentView(R.layout.main);
 GridView gridview=(GridView) findViewById(R.id.PuzzleGridView);
 adapterForImage=new ImageAdapter(this);
 gridview.setAdapter(adapterForImage);
 gridview.setOnItemClickListener(new OnItemClickListener() {
  public void onItemClick(AdapterView<?> parent, View v, int position, long id) {
   adapterForImage.moveImage(position);
   adapterForImage.notifyDataSetChanged();
  }
 });
}
Dopo l'assegnazione del layout principale (con setContentView()) il codice procede con la ricerca della view dell'oggetto GridView, a cui viene assegnato l'Adapter per la sorgente di dati. Viene, poi, descritto il gestore per gli eventi (che in questo esempio viene chiamato al tocco della View, metodo onItemClickListener()). Particolarmente importante è il metodo getView() della classe ImageAdapter (che estende BaseAdapter, una classe astratta che è alla base di altri Adapter). Il compito di questo metodo è la restituzione della View da inserire nella griglia.
public View getView(int position, View convertView, ViewGroup parent) {
 ImageView imageViewOfPiece;
 if (convertView==null) { 
  imageViewOfPiece=new ImageView(mContext);
  imageViewOfPiece.setLayoutParams(new GridView.LayoutParams(100,100));
  imageViewOfPiece.setScaleType(ImageView.ScaleType.FIT_XY);
  imageViewOfPiece.setPadding(1,1,1,1);
 } else imageViewOfPiece=(ImageView)convertView;
 imageViewOfPiece.setImageResource(table[position]);
 return imageViewOfPiece;
}
Per far avanzare le immagini toccate a quelle successive (in modo pseudo-casuale) ho scritto il seguente metodo:
public void moveImage(int position) {
 if(i==pieces.length-1) i=0;
 else i++;
 table[position]=pieces[i];
 nClick++;
 if(nClick==3) {
  i=(int)(Math.random()*(pieces.length-1));
  nClick=0;
 }
}
I primi tre click spostano di una posizione l'indice che punta alla successiva immagine dell'array. Dopo il terzo click l'indice assume un valore casuale, incrementato di una posizione per i nuovi prossimi tre click e così via... Viene effettuato anche un controllo ciclico sull'indice che non deve mai superare la capacità dell'array (diminuita di uno). Questo il puzzle completo:


Potete installare l'applicazione vista in questo esempio attraverso il seguente codice QR:

qrcode

domenica 3 aprile 2011

Android: override del metodo getView (GList v.1)


Come già detto qui, ArrayAdapter assegna di default l'item di una sorgente di dati a una TextView. Questo, ovviamente, non sempre è ciò che vogliamo fare nella nostra applicazione. Soprattutto se l'informazione da dare all'utente è strutturata su più campi dati. Il comportamento di default previsto dalla classe ArrayAdapter nel metodo public View getView(int position, View convertView, ViewGroup parent) può essere allora modificato sovrascrivendo proprio quest'ultimo! I parametri usati dal metodo prevedono: la posizione (variabile position) dell'item all'interno dell'oggetto ArrayAdapter; il riferimento a una vecchia View (variabile convertView) non più visibile, ad esempio a causa dello scorrimento della lista, che è preferibile riciclare anziché istanziare una nuova View; il riferimento a l'oggetto ViewGroup (variabile parent) che contiene gli oggetti View.
Il nostro obiettivo, dunque, è descrivere la view dell'item all'interno del metodo getItem(). Dovendo modificare il processo di inflating (che, ricordo, istanzia gli oggetti a partire dalla risorsa xml che li definisce, il layout dell'item in questo caso) occorre in primo luogo ottenere un riferimento all'oggetto che svolge questo lavoro. Subito dopo, quindi, passiamo a tale oggetto un riferimento al layout da usare per le view degli item (che personalizzeremo a nostro piacimento). Le operazioni dette sopra avvengono attraverso le seguenti righe di codice:
LayoutInflater inflater=(LayoutInflater)getSystemService(Context.LAYOUT_INFLATER_SERVICE);
View itemView=inflater.inflate(R.layout.record,null);
Nell'ultimo metodo possiamo infatti notare il riferimento al layout da applicare ai record della lista lista di oggetti! Non ci resta che caratterizzare il contenuto dei widget con le informazioni dell'item, il cui riferimento può essere ottenuto con il metodo getItem(position). Il codice del metodo dovrà eventualmente collegare gli eventi dei bottoni ai rispettivi metodi di callback (nel nostro esempio occorre descrivere l'azione da intraprendere alla pressione del bottone Delete, vedere l'immagine dell'applicazione GList).
Possiamo migliorare ulteriormente il codice dell'applicazione del metodo getView()! Non abbiamo infatti usato il parametro convertView che, come detto prima, è un riferimento a una View da riciclare. E' importante, infatti, evitare l'istanza di nuovi oggetti View per non appesantire l'esecuzione dell'applicazione. Meglio quindi verificare la presenza di tale variabile e regolarsi di conseguenza, come viene mostrato nel codice che segue:
...
if(convertView==null) {
LayoutInflater inflater=(LayoutInflater)getSystemService(Context.LAYOUT_INFLATER_SERVICE);
convertView=inflater.inflate(R.layout.record,null);
}
...
Ho modificato l'applicazione vista qui, personalizzando il layout degli item e aggiungendo nuove funzionalità, come il salvataggio e il ripristino dello stato dell'applicazione! Trovate qui il codice sorgente dell'applicazione. Attraverso il codice QR che segue potete invece installare l'applicazione sul vostro dispositivo Android:

qrcode

Alcune osservazioni sull'applicazione:
Per salvare e leggere lo stato dell'applicazione, ovvero il contenuto della lista, ho usato un file di appoggio. L'oggetto da salvare va dunque serializzato e scritto attraverso uno stream sul file, a tale proposito la classe che descrive l'oggetto deve necessariamente implementare l'interfaccia Serializable. La lettura o la scrittura richiede uno stream verso il file di appoggio detto sopra che Android colloca, per ogni applicazione, nel path /data/data/package_name/file_name.dat. Per l'input da file usate lo stream della classe FileInputStream, per l'output su file usate lo stream della classe FileOutputStream. Dagli stream ottenuti istanziamo, a seconda dei casi, uno stream per la lettura o la scrittura degli oggetti. In tal caso usate lo stream della classe ObjectOutputStream e il metodo writeObject(oggetto_da_salvare) per la scrittura dell'oggetto, oppure lo stream ObjectInputStream e il metodo readObject() per la lettura dell'oggetto (che va trattato con un casting per ripristinare la classe dell'oggetto). Non dimenticate, infine, di chiudere gli stream aperti con il metodo close()! Vi riporto il metodo usato per salvare lo stato della lista:
public boolean save() {
Log.i("INFO","Method save()!");
if(!save) return true;
try {
Toast.makeText(this,"Saving...",Toast.LENGTH_LONG).show();
FileOutputStream file=new FileOutputStream(fileName);
ObjectOutputStream data=new ObjectOutputStream(file);
data.writeObject(arrayOfItems);
data.close();
file.close();
return true;
}
catch(Exception e) {
Log.i("INFO",e.toString());
return false;
}
}
e quello per caricarlo dal file:
public boolean read() {
Log.i("INFO","Method read()!");
try {
FileInputStream file=new FileInputStream(fileName);
Toast.makeText(this,"Loading...",Toast.LENGTH_LONG).show();
ObjectInputStream data=new ObjectInputStream(file);
arrayOfItems=(ArrayList<MyItem>)data.readObject();
arrayOfItemsAdapter=this.configArrayAdapter();
ListView listView=(ListView)findViewById(R.id.viewOfItems);
listView.setAdapter(arrayOfItemsAdapter);
data.close();
file.close();
return true;
}
catch(Exception e) {
Log.i("INFO",e.toString());
return false;
}
}
Gli eventi legati ai clic sui bottoni dell'interfaccia sono legati, all'interno dei file xml che descrivono i vari layout, ai metodi definiti nella classe dell'activity attraverso l'attributo android:onClick="nomeMetodo". Non dimenticate di implementarli all'interno della classe!
Quando salvare la lista di oggetti? La lista va evidentemente salvata solo se l'utente la modifica. A tale proposito ho inserito all'interno della classe una variabile booleana (save) il cui stato influenzerà le operazioni di lettura e scrittura. I metodi in grado di modificare la variabile save sono: addItem(), clearAll() e remove(). Se ad esempio l'utente avvia l'applicazione per una consultazione, senza aggiungere o rimuovere oggetti nella lista, all'uscita dell'applicazione lo stato della lista non va conservato poiché immutato!
In che punto dell'applicazione devo salvare o ripristinare lo stato dell'applicazione? In questa applicazione ho deciso di salvare la lista di oggetti non appena l'utente preme il tasto back o il tasto home. Il metodo save() viene quindi invocato all'interno del metodo onPause(). Il metodo read() va invece invocato all'interno dei metodi onCreate() , onRestart() e reloadItems().
L'applicazione vista in questo esempio si presta alla memorizzazione di cose da fare, da comprare, etc... GList sta per generic list, lista generica. Nell'esempio ho memorizzato nella lista l'occorrente per cucinare i fusilli alla sorrentina!

lunedì 21 marzo 2011

Android: arrayAdapter

L'utilità delle activity, sia per la presentazione dei dati che per l'interazione con l'utente, è stata più volte messa in evidenza nei precedenti articoli. Android disegna i componenti di una schermata attraverso oggetti della classe View, raccolti all'interno di oggetti ViewGroup (che possono raccogliere, a loro volta, altri oggetti View e ViewGroup). Esistono diverse specializzazioni degli oggetti detti sopra. View è la classe base per i widgets, così vengono chiamati gli elementi dell'interfaccia grafica. Alcune specializzazioni della classe View sono: la classe Button (per i bottoni), la classe TextView (per il testo), la classe ImageView (per le immagini). ViewGroup, invece, è la classe base per i layout, gli schemi che descrivono la disposizione degli oggetti sullo schermo (le activity). Alcune specializzazioni della classe ViewGroup sono: la classe LinearLayout (per un layout lineare), la classe ListView (per un layout a elenco), la classe TableLayout (per un layout tabellare). Le specializzazioni dette sopra ereditano dalle rispettive classi padre numerosi metodi e costanti. Pertanto, durante la scrittura del codice, vi invito a consultare la documentazione in linea se siete alla ricerca di metodi che non trovate documentati all'interno della classe!
La presentazione di informazioni all'interno di un'activity, per alcune applicazioni, è spesso legata a una sorgente di dati. In tal caso è particolarmente importante collegare quest'ultima ai meccanismi che hanno il compito di presentare le informazioni all'interno degli oggetti ViewGroup dell'activity.
Adapter è l'interfaccia pubblica usata da Android per accedere a una sorgente di dati e istanziare oggetti View per ogni record. In questa interfaccia, pertanto, sono raccolte le firme dei metodi che permettono di gestire le azioni dette sopra. Per l'accesso a un record, ad esempio, esistono i metodi:
  • public abstrac Obejct getItem(int position): che ritorna i dati di un record nella posizione indicata da position;
  • public abstract long getItemId(int position): che ritorna la chiave del record nella posizione indicata da position;
Per il conteggio dei record, invece, l'interfaccia prevede i metodi:
  • public abstract getCount(): che ritorna il numero di record contenuti nell'oggetto Adapter;
  • public abstract boolean isEmpty(): che ritorna true se l'oggetto Adapter non ha record, altrimenti false;
Il riferimento a un oggetto View, che rappresenta un record dell'oggetto istanziato da Adapter, viene ottenuto con il metodo:
  • public abstract View getView(int position, View convertView, ViewGroup parent): dove position indica la posizione del record all'interno della lista, convertView è il riferimento a una precedente View non più visibile (a causa dello scrolling e quindi riutilizzabile) e parent, infine, è il riferimento all'oggetto ViewGroup che contiene le singole View dei record;
Spesso la sorgente di dati da visualizzare è soggetta a variazioni e l'oggetto ViewGroup che ne permette la visualizzazione sul display va dunque aggiornato. A tale proposito l'interfaccia Adapter prevede i seguenti metodi per la gestione del suddettoevento:

  • public abstract void registereDataSetObserver(DataSetObserver observer): che registra un oggetto DataSetOberver come ascoltatore che verrà chiamato non appena si verifica una modifica ai dati;
  • public abstract void unregisterDataSetObserver(DataSetObserver observer): che cancella l'oggetto DataSetOberver impostato dal metodo visto prima, rendendo l'oggetto ViewGroup insensibile alle modifiche applicate sui dati;
L'interfaccia prevede la firma di altri metodi, vi consiglio pertanto di consultare la documentazione in linea.
AdapterView è la classe astratta che, estendendendo ViewGroup, implementa i meccanismi per la comunicazione fra un oggetto ViewGroup e un Adapter. La disposizione delle singole View viene lasciata alle successive specializzazioni di AdapterView (come ad esempio ListView, Gallery e Spinner). Un AdapterView, riassumendo, si occupa di:
  • riempire il layout principale (dell'oggetto ViewGroup) con i dati, usando una specializzazione di Adapter (per accedere alla sorgente di dati);
  • gestire le interazioni dell'utente con i dati presentati;
Pertanto, a seconda dello schema da adottare per le View è prevedibile aspettarsi, all'interno del package di Android, più specializzazioni e implementazioni dell'interfaccia Adapter. L'obiettivo di ognuna di queste specializzazioni è l'aggiunta di nuove firme di metodi orientati alla gestione del particolare layout. Ad esempio, per organizzare i record di una sorgente di dati secondo una lista di record, nel package di Android troviamo:

  • la classe ListView, che estende ViewGroup (per il layout);
  • l'interfaccia ListAdapter che estende l'interfaccia Adapter aggiungendo le firme dei metodi utili alla gestione dei dati organizzati secondo una lista scorrevole (i metodi aggiunti sono: isEnabled(int row), che ritorna true se l'elemento della lista è abilitato, altrimenti torna false e areAllItemsEnabled(), che ritorna true se tutti gli elementi della lista sono abilitati, altrimenti torna false);
  • la classe ArrayAdapter implementa l'interfaccia ListAdapter (potete infatti verificare la presenza dei metodi isEnabled() e areAllItemsEnabled() all'intero della classe che effettivamente li implementa);
La classe ArrayAdapter, del package android.widget, si interfaccia con un array di oggetti (la sorgente di dati) e istanzia per ogni record una View. Esistono diversi costruttori di ArrayAdapter, come ad esempio public ArrayAdapter (Context context, int resource, int textViewResourceId, T[] object), dove:
  • context è il riferimento a una risorsa o classe dell'applicazione;
  • resource è il riferimento al layout dell'oggetto ViewGroup;
  • textViewResourceId è il riferimento al layout che conterrà le View da istanziare per ogni occorrenza dell'array di oggetti;
  • T[] è l'array di oggetti, quindi la sorgente di dati;
L'assegnazione dell'oggetto ArrayAdapter all'oggetto ListView (il contenitore per le View) avviene con il metodo setAdapter(ListAdapter adapter). Vediamo con un esempio come applicare le cose fin qui dette.
Supponiamo di voler realizzare un'applicazione in grado di gestire una lista di valori, le operazioni utili (per il momento) saranno svolte attraverso la pressione di due bottoni: uno per l'aggiunta di un nuovo elemento alla lista e uno per la cancellazione di tutti gli elementi della lista. Per l'aggiunta di un nuovo elemento, inoltre, sarà presente un'area di testo per l'input. Realizziamo con il seguente codice xml (file main.xml) il layout principale dell'applicazione:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/mainLayout"
android:layout_height="wrap_content"
android:layout_width="fill_parent"
android:orientation="vertical">
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_height="wrap_content"
android:layout_width="fill_parent"
android:orientation="horizontal">
<TextView
android:id="@+id/labelForNewItem"
android:text="New item:"
android:layout_width="wrap_content"
android:layout_height="wrap_content">
</TextView>
<EditText
android:id="@+id/newItem"
android:layout_width="fill_parent"
android:layout_height="wrap_content">
</EditText>
</LinearLayout>
<Button
android:id="@+id/addButton"
android:layout_height="wrap_content"
android:layout_width="fill_parent"
android:text="Add item"
android:onClick="addItem">
</Button>
<Button
android:id="@+id/clearButton"
android:layout_height="wrap_content"
android:layout_width="fill_parent"
android:text="Clear all items"
android:onClick="clearAll">
</Button>
<ListView
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/viewOfItems"
android:layout_width="fill_parent"
android:layout_height="fill_parent">
</ListView>
</LinearLayout>
Dovendo in seguito specificare nel codice le azioni per gli eventi legati alla pressione di uno dei due bottoni ho deciso di utilizzare, per gli elementi Button, l'attributo android:onClick. La stringa riferita da questo attributo all'oggetto istanziato in fase di inflating ha l'importante compito di indicare allo stesso il metodo da invocare alla pressione della view (in questo caso il bottone). Tali metodi, dunque, vanno implementati all'interno della classe che descrive l'activity.
Il layout principale mostrato sopra prevede infine un oggetto ViewGroup, in questo caso un oggetto ListView. Sarà questo componente ad occuparsi delle View relative agli elementi aggiunti. Ogni singola View verrà presentata secondo uno schema descritto in un file xml a parte che per il nostro esempio è il file row.xml:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/record"
android:layout_height="wrap_content"
android:layout_width="fill_parent">
<ImageView
android:id="@+id/itemImage"
android:src="@drawable/item"
android:layout_height="wrap_content"
android:layout_width="wrap_content">
</ImageView>
<TextView
android:id="@+id/itemLabel"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:paddingLeft="10px"
android:text="Information here...">
</TextView>
</LinearLayout>
Ogni nuovo elemento aggiunto sarà quindi descritto da un'immagine e da un testo a lato. Non ci resta che collegare tutto all'interno del codice. In questo esempio la sorgente di dati viene descritta con un oggetto della classe ArrayList (del package java.util), i cui metodi add() e clear() permettono la gestione della lista costruita così come descritto già sopra. Tali metodi saranno allora invocati all'interno dei metodi addItem() e clearAll() dell'activity.
package android.mylistview;

import java.util.ArrayList;
import android.os.Bundle;
import android.app.Activity;
import android.view.View;
import android.widget.ArrayAdapter;
import android.widget.EditText;
import android.widget.ListView;
import android.widget.Toast;

public class ListViewActivity extends Activity {

private ArrayList<String> arrayOfItems;
private ArrayAdapter<String> arrayOfItemsAdapter;

@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
arrayOfItems=new ArrayList<String>();
ListView listView=(ListView)findViewById(R.id.viewOfItems);
arrayOfItemsAdapter=new ArrayAdapter<String>(this,R.layout.record,R.id.itemLabel,arrayOfItems);
listView.setAdapter(arrayOfItemsAdapter);
}

public void addItem(View v) {
EditText input=(EditText)findViewById(R.id.newItem);
String newItem=input.getText().toString();
if(newItem.trim().equals((String)""))
Toast.makeText(this,"Hey... i've a null item!",Toast.LENGTH_LONG).show();
else {
arrayOfItems.add(newItem);
arrayOfItemsAdapter.notifyDataSetChanged();
Toast.makeText(this,"New item in the list!",Toast.LENGTH_LONG).show();
input.setText("");
}
}

public void clearAll(View v) {
if(!arrayOfItems.isEmpty()) {
arrayOfItems.clear();
arrayOfItemsAdapter.notifyDataSetChanged();
Toast.makeText(this,"Bye bye old items!",Toast.LENGTH_LONG).show();
}
}

}
Tralasciando le fasi dedicate al setup del layout principale, all'interno del metodo onCreate() viene: creata la sorgente di dati per la lista di elementi; ottenuto un riferimento all'oggetto ListView; istanziato l'oggetto ArrayAdapter, fornendo i riferimenti ai layout (quello principale e quello di riga) e alla sorgente di dati; collegato l'oggetto ArrayAdapter all'oggetto ListView. E' particolarmente importante, nei metodi add() e clearAll(), invocare sull'oggetto ArrayAdapter il metodo notifyDataSetChanged() per notificare un cambiamento nella sorgente di dati (l'aggiunta di un nuovo elemento oppure la cancellazzione dell'intera lista) e provocare l'aggionamento dell'oggetto ViewGroup (ListViewArrayAdapter nell'associazione fra elementi della sorgente di dati e il layout per la View. La stringa aggiunta viene legata di default a un elemento TextView. Nessun problema, quindi, per l'esempio appena visto. Gli item aggiunti sono stringhe e una TextView è sufficiente a raccoglierli. Se l'informazione da aggiungere è strutturata diversamente occorre necessariamente sovrascrivere il metodo getView() implementato nella classe ArrayAdapter e personalizzare allora la View da restituire. Vedremo a breve come fare anche questo. Per adesso vi lascio lo screenshot dell'applicazione (provata sia sull'emulatore che sul mio dispositivo Android):


Qui, trovate l'archivio compresso con il codice sorgente dell'applicazione. Attraverso il codice QR che segue potete, invece, installare l'applicazione sul vostro dispositivo Android:

qrcode