sabato 12 marzo 2011

Android: view e layout

L'interfaccia grafica di un'applicazione Android è descritta attraverso oggetti della classe View e ViewGroup (chiamati anche widget). La descrizione può avvenire sia attraverso file xml che attraverso codice java, in quest'ultimo caso l'interfaccia grafica può essere creata o manipolata a run-time (approccio che va sotto il nome di programmazione imperativa). Il primo modo di procedere è anche detto programmazione dichiarativa, l'interfaccia grafica viene infatti dichiarata in un file xml il cui nome è l'id che ne permette il riferimento. Il metodo findViewById(int ResourceId) della classe Activity ritorna un riferimento a un oggetto View rintracciato, all'interno delle risorse di progetto, attraverso il suo id. In questo articolo mi occuperò per buona parte della programmazione dichiarativa delle interfacce grafiche per applicazioni Android.
Prima di procedere è opportuno sottolineare un importante questione fra i due modi di programmazione offerti da Android. La programmazione dichiarativa offre notevoli vantaggi per la descrizione di interfacce grafiche e la definizione di risorse poiché usa file esterni (xml, nel caso di Android) al codice java. Ogni modifica, infatti, non richiederà la compilazione dell'intero progetto. Cosa che invece accade necessariamente nella programmazione imperativa a fronte di una modifica nel codice. L'uso di una delle due metodologie va comunque valutato in fase di progettazione dell'applicazione.
Android raccoglie gli elementi di una interfaccia grafica attraverso oggetti della classe ViewGroup, una specializzazione della classe View. Un oggetto ViewGroup può contenere a sua volta altri oggetti View e/o ViewGroup. Un modo particolarmente utile di sintetizzare l'interfaccia grafica, indipendentemente dall'approccio usato, consiste nella sua rappresentazione gerarchica ad albero.
Android disegna l'interfaccia grafica a partire dall'elemento radice di tale albero, analizzando prima il sottoalbero sinistro, quindi il nodo centrale ed infine il sottoalbero destro. La visita del suddetto albero, così come l'ho appena descritta, è detta in-order. Questo modo di procedere, disegna gli oggetti dell'albero a partire dal basso verso l'alto. In caso di sovrapposizione fra oggetti può accadare che l'ultimo oggetto disegnato copra il precedente!
Il processo compiuto da Android che a partire dalla visita dell'albero genera gli oggetti dell'interfaccia, istanziandoli man mano, è detto inflating e segue un preciso algoritmo che può essere anche personalizzato.
Anche se una view descrive l'oggetto dotandolo di dimensioni e misure per lo scostamento, l'oggetto ViewGroup che fa da contenitore ne fissa per diritto le proprietà (occupando nell'albero una posizione a un livello superiore). Ne consegue che non sempre le proprietà volute da una view vengano poi garantite dall'oggetto ViewGroup contenitore se quest'ultimo impone le proprie caratteristiche. Alla luce di quanto appena detto, allora, è preferibile assegnare le proprietà generali, quelle in comune a tutti gli elementi View e/o ViewGroup, ai nodi ViewGroup di livello superiore.
Un oggetto della classe View ritorna con i metodi getLeft() e getTop() le coordinate del vertice in alto a sinistra del componente da disegnare. I metodi getMeasuredHeight() e getMeasuredWidth() della classe View ritornano, invece, le dimensioni desiderate dal componente, mentre i metodi getWidth() e getHeight() quelle effettive. Più avanati vedremo come impostare queste proprietà all'interno di una view assegnata come layout per l'applicazione. Vi ricordo che tale assegnazione avviene grazie al metodo setContentView() (della classe Activity), a cui va passato l'elemento radice di un oggetto ViewGroup. Il layout dell'applicazione viene disegnato, durante la visita all'albero delle View, in due passi: measure pass e layout pass. Vengono cioè invocati i metodi per l'accesso alle coordinate e alle dimensioni visti sopra. Per layout si intende, quindi, il modo di organizzare e raccogliere gli oggetti View e ViewGroup, Android ne mette a disposizioni alcuni di default: LinearLayout, RelativeLayout, TableLayout e FrameLayout.
Ognuno di questi, come già accennato sopra, può impostare delle proprietà generali valide, poi, per tutte le view contenute. Alcune di queste sono passate agli oggetti attraverso gli attributi android:layout_height e android:layout_width per fissare, rispettivamente, l'altezza e la larghezza del layout. Possiamo caratterizzare queste proprietà con le costanti fill_parent e wrap_parent, oppure con misure fisse. La costante fill_parent indica la volontà del componente di impegnare tutto lo spazio disponibile mentre con wrap_parent il componente occuperà lo spazio minimo. Altre proprietà, infine, sono tipiche del layout e saranno prese in considerazione solo se l'oggetto creato in fase di inflating le prevede. Di seguito vi parlo dei principali layout di Android.

LinearLayout
Si tratta di un layout che dispone le view lungo la direzione stabilita dalla proprietà android:layout_orientation, che può assumere i valori horizontal (le view sono disposte su colonne) e vertical (le view sono disposte su righe). Qualora il numero di view sia tale da andare oltre le dimensioni dello schermo vi suggerisco di ricorrere all'elemento ScrollView (un oggetto ViewGroup), che aggiunge una barra laterale per lo scorrimento. Il tag da usare all'interno del file xml è LinearLayout. Ecco un esempio di LinearLayout il cui flusso è orientato orizzontalmente:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="horizontal"
android:layout_width="fill_parent"
android:layout_height="fill_parent">
<Button
android:text="Invia"
android:id="@+id/Button1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
<Button
android:text="Cancella"
android:id="@+id/Button2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
</LinearLayout>


Qui, invece, lo stesso layout con orientazione verticale:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="fill_parent"
android:layout_height="fill_parent">
<Button
android:text="Invia"
android:id="@+id/Button1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
<Button
android:text="Cancella"
android:id="@+id/Button2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
</LinearLayout>


In questi due esempi possiamo subito apprezzare la validità della costante wrap_content. Il componente, in questi esempi un bottone, occupa lo spazio minimo che coincide con quello necessario a contenere la label del bottone. Particolarmente utile è il seguente esempio:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="horizontal"
android:layout_width="fill_parent"
android:layout_height="fill_parent">
<Button
android:text="Invia"
android:id="@+id/Button1"
android:layout_width="fill_parent"
android:layout_height="wrap_content"/>
<Button
android:text="Cancella"
android:id="@+id/Button2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
</LinearLayout>


Che fine ha fatto il secondo bottone? Tutto deriva dall'algoritmo usato da Android per disegnare il layout. Infatti, visitando l'albero delle view con la modalità in-order il sistema operativo in fase di inflating istanzia l'oggetto per disegnare il primo bottone assegnando per quest'ultimo tutta la larghezza messa a disposizione dal contenitore (attributo android:layout_width=fill_parent del primo bottone). Pertanto, quando si troverà a visitare l'altra view per istanziare il secondo bottone, lo spazio a disposizione è già stato consumato interamente dal componente che lo ha preceduto. Il risultato è quello in figura, il secondo bottone non è visibile nel layout!
Un LinearLayou rispetta i margini delle view contenute, questi possono essere assegnati alle rispettive view attraverso gli attributi android:layout_marginBottom, android:layout_marginTop, androi:layout_marginLeft e android:layout_marginRight per i rispettivi bordi. Mentre con l'attributo android:layout_margin possiamo assegnare un unico valore valido per tutti i bordi del componente.

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="horizontal"
android:layout_width="fill_parent"
android:layout_height="fill_parent">
<Button
android:text="Invia"
android:id="@+id/Button1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginLeft="25px"
android:layout_marginRight="25px"/>
<Button
android:text="Cancella"
android:id="@+id/Button2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
</LinearLayout>


Un LinearLayout permette l'assegnazione di un opportuno peso da assegnare a una view attraverso l'attributo android:layout_weight. L'intero, oppure il decimale, dato al componente come peso lo autorizza a espandersi fino a impegnare lo spazio offerto dal contenitore, in relazione agli altri pesi dati agli altri componenti. In altre parole, a seconda dell'orientazione del layout, lo spazio lungo le righe o le colonne viene ripartito fra le view del layout. La view con il peso più grande occuperà meno spazio (oppure, quella con il decimale più grande occuperà più spazio, usate decimali compresi fra 0 ed 1).

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="horizontal"
android:layout_width="fill_parent"
android:layout_height="fill_parent">
<Button
android:text="Invia"
android:id="@+id/Button1"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:layout_weight="1"/>
<Button
android:text="Cancella"
android:id="@+id/Button2"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:layout_weight="2"/>
</LinearLayout>


Un LinearLayout, infine, permette di stabilire attraverso l'attributo android:layout_gravity dove e come inserire una view nel layout. I possibili valori sono: top, bottom, left, right, center_vertical, fill_vertical, center_horizontal, fill_horizontal, center, fill, clip_vertical e clip_horizontal. In altre parole, lo spazio che il contenitore mette a disposizione della view verrà impegnato seguendo il significato di uno degli attributi detti sopra.

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="fill_parent"
android:layout_height="fill_parent">
<LinearLayout
android:orientation="horizontal"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:layout_weight="1">
<Button
android:text="Invia"
android:id="@+id/Button1"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:layout_weight="1"/>
<Button
android:text="Cancella"
android:id="@+id/Button2"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:layout_weight="2"/>
</LinearLayout>
<LinearLayout
android:orientation="vertical"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:layout_weight="1">
<EditText
android:text="Messaggio..."
android:id="@+id/text1"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"/>
</LinearLayout>
</LinearLayout>


Nell'ultimo visto possiamo apprezzare la possibilità di nidificare più layout all'interno di un layout!

TableLayout
Con TableLayout le view figlie vengono posizionate all'interno di righe e colonne di una tabella. Ogni riga può avere un certo numero di colonne, anche nullo se necessario. Una cella può inoltre descrivere altri oggetti ViewGroup. Il tag da usare all'interno del file xml è TableLayout. Per l'aggiunta di una riga, invece, va usato TableRow. Gli attributi android:layout_column e android:layout_span permettono, rispettivamente, di assegnare una view ad una precisa colonna della tabella oppure di estendere una view anche ad altre celle successive.

<?xml version="1.0" encoding="utf-8"?>
<TableLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:stretchColumns="0,1,2,3">
<TableRow
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:id="@+id/row1">
<TextView
android:text="Risultato..."
android:id="@+id/output"
android:layout_gravity="center_horizontal"
android:layout_column="0"
android:layout_span="4"
android:textStyle="bold"/>
</TableRow>
<TableRow
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:id="@+id/row2">
<Button
android:text="9"
android:id="@+id/Button9"
android:layout_weight="0.25"/>
<Button
android:text="8"
android:id="@+id/Button8"
android:layout_weight="0.25"/>
<Button
android:text="7"
android:id="@+id/Button7"
android:layout_weight="0.25"/>
<Button
android:text="+"
android:id="@+id/ButtonPlus"
android:layout_width="fill_parent"
android:layout_weight="0.25"/>
</TableRow>
<TableRow
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:id="@+id/row3">
<Button
android:text="6"
android:id="@+id/Button6"
android:layout_weight="0.25"/>
<Button
android:text="5"
android:id="@+id/Button5"
android:layout_weight="0.25"/>
<Button
android:text="4"
android:id="@+id/Button4"
android:layout_weight="0.25"/>
<Button
android:text="-"
android:id="@+id/ButtonMinus"
android:layout_weight="0.25"/>
</TableRow>
<TableRow
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:id="@+id/row4">
<Button
android:text="3"
android:id="@+id/Button3"
android:layout_weight="0.25"/>
<Button
android:text="2"
android:id="@+id/Button2"
android:layout_weight="0.25"/>
<Button
android:text="1"
android:id="@+id/Button1"
android:layout_weight="0.25"/>
<Button
android:text="*"
android:id="@+id/ButtonMul"
android:layout_weight="0.25"/>
</TableRow>
<TableRow
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:id="@+id/row5">
<Button
android:text="("
android:id="@+id/ButtonPsx"
android:layout_weight="0.25"/>
<Button
android:text="0"
android:id="@+id/Button0"
android:layout_weight="0.25"/>
<Button
android:text=")"
android:id="@+id/ButtonPdx"
android:layout_weight="0.25"/>
<Button
android:text=":"
android:id="@+id/ButtonDiv"
android:layout_weight="0.25"/>
</TableRow>
<TableRow
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:id="@+id/row6">
<Button
android:text="."
android:id="@+id/ButtonForDecimal"/>
<Button
android:text="C"
android:id="@+id/ButtonForClear"/>
<Button
android:text="="
android:id="@+id/ButtonForTotal"
android:layout_column="2"
android:layout_span="2"/>
</TableRow>
</TableLayout>


RelativeLayout
Con l'oggetto RelativeLayout (usate l'omonimo tag nel file xml) possiamo descrivere un layout le cui view vengono disposte sullo schermo in relazione ad altre view appartenenti allo stesso layout. I possibili valori sono molti e tutti indicano l'id della view di riferimento, oppure abilitano un particolare allineamento. Una view può essere allineata rispetto al contenitore attraverso gli attributi android:layout_alignParentRight, android:layout_alignParentLeft, android:layout_alignParentTop e android:layout_alignParentBottom, a cui va passato il valore booleano true. Altre possibili soluzioni sono: android:layout_alignTop, android:layout_alignBotton, android:layout_alignLeft e android:layout_alignRight a cui va passato l'id della view di riferimento. Per una lista completa dei possibili attributi vi invito a consultare la guida in linea.

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/Layout1"
android:layout_width="fill_parent"
android:layout_height="fill_parent">
<TextView
android:layout_height="wrap_content"
android:layout_width="wrap_content"
android:id="@+id/label"
android:text="Select a value:"
android:layout_alignParentLeft="true"
/>
<Button
android:layout_width="wrap_content"
android:id="@+id/ButtonNext"
android:text="Next"
android:layout_below="@+id/label"
android:layout_height="wrap_content"
android:layout_alignLeft="@+id/label"
android:layout_alignRight="@+id/label"/>
<SeekBar
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:id="@+id/bar"
android:layout_toRightOf="@+id/label"
android:layout_alignTop="@+id/label"
android:layout_alignBottom="@+id/label"/>
</RelativeLayout>


FrameLayout
E' l'oggetto (classe FrameLayout, usate l'omonimo tag per segnalarlo nel file xml) per layout più semplice, raccoglie le view disegnandole a partire dal vertice in alto a sinistra dello schermo. Non esistono metodi e attributi capaci di intervenire sulla collocazione delle view che verranno inevitabilmente sovrapposte fra loro. L'attributo android:visibility permette di nascondere una view, attivabile in un secondo momento attraverso il metodo setVisibility() a cui va passata la costante FrameLayout.VISIBLE. Con la costante FrameLayout.GONE, invece, disabilitiamo la visione di una view.

<?xml version="1.0" encoding="utf-8"?>
<FrameLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="fill_parent"
android:layout_height="fill_parent">
<TextView
android:id="@+id/frame1"
android:text="Android"
android:id="@+id/text1"
android:layout_width="fill_parent"
android:layout_height="wrap_content"/>
<TextView
android:id="@+id/frame2"
android:text=" Android"
android:id="@+id/text2"
android:layout_gravity="center_horizontal"
android:layout_width="fill_parent"
android:layout_height="wrap_content"/>
<TextView
android:id="@+id/frame3"
android:text=" Android"
android:id="@+id/text3"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:visibility="gone"/>
</FrameLayout>


Nell'esempio visto sopra possiamo notare l'inutulità dell'attributo android:layout_gravity. Il FrameLayout, come detto sopra, non permette di agire sul posizionamento delle View. La sovrapposizione è adesso evidente!

3 commenti:

  1. Eccellente lavoro complimenti. Mi chiedevo come è possibile implementare il menu di una applicazione.

    RispondiElimina
  2. Dai uno sguardo qui http://developer.android.com/guide/topics/ui/menus.html. Anziché scrivere il codice per la gestione del tuo menu puoi descriverlo attraverso un file xml (ogni elemento del menu ha il suo tag) e istanziare l'oggetto nel metodo onCreateOptionsMenu(). Per la gestione degli eventi devi invece sovrascrivere il metodo onOptionsItemSelected(). Trovi tutto al link scritto sopra ;)

    RispondiElimina