mercoledì 2 aprile 2014

Un menù dinamico per app mobile (con Delphi XE5 per iOS e Android)

Introduzione

Uno dei problemi più comuni quando si sviluppa un'applicazione mobile è che, per implementare tutte le funzionalità richieste, abbiamo bisogno di più "schermate" (per esempio perchè dobbiamo visualizzare molti dati o controlli utente o perchè le funzionalità richiedono di essere suddivise in unità visuali isolate).

Al di là della scelta da compiere (valutando i pro e contro del caso) fra l'uso di TTabControl con più pagine rispetto all'opzione di realizzare una applicazione con più form, per tutti esiste il problema di guidare il proprio utente attraverso le varie "schermate".

Una soluzione molto diffusa è quella di implementare un drawer (un compartimento laterale a scomparsa che mostra un elenco di voci cliccabili). José Leon ha una serie di articoli a riguardo che sono molto interessanti.
Alternativamente, è possibile avere un pulsante che mostri un pannello a scomparsa più piccolo (simile ad un popup menu) e trovate un esempio di questo sempre sul blog di José Leon

Voglio proporvi anche un'altra alternativa, con relativa implementazione di base, che consiste in un menù (sempre a scomparsa: lo spazio è prezioso nelle app mobile) ma che abbia un po' di flessibilità sul numero di elementi cliccabili e che sia facile da manutenere a design-time.

Realizzazione

In breve, ho cercato di combinare la flessibilità (e le performance) di una TListView con il meccanismo di transizione a scomparsa utilizzato anche negli altri approcci che vi ho indicato poco sopra. Inoltre, per cercare di rendere più facile l'utilizzo e la manutenzione del menù a design-time, ho pensato di usufruire di una TActionList (comoda da maneggiare nell'IDE): lo sviluppatore in questo modo non deve fare altro che popolare l'ActionList con le voci che vorrà ritrovare nel menù a scomparsa.

Come potete vedere dal seguente screenshot, i componenti del MenuLayout (che rappresenta il menù nella sua interezza), sono sostanzialmente una TListView, un paio di animazioni, un MenuButton (in realtà un'istanza di TRectangle, colorato con un gradiente) che permette all'utente di avere un punto di "presa" del menù anche quando è chiuso e un effetto di ombra usato fini estetici.


A design-time, lo sviluppatore vede il menù "chiuso", nella parte superiore della form, riducendo così anche fastidiosi "ingombri" durante la fase di sviluppo del resto dell'applicazione:


Nota: il triangolo che si vede è realizzato usando un pezzetto di SVG e un componente TPath (questa funzionalità di FireMonkey mi piace molto e probabilmente sarà oggetto di un futuro blog post).

Grazie ad un utilizzo mirato delle proprietà Align, la MenuListView ha modo di crescere (in verticale) insieme al MenuLayout, mentre il MenuButton è allineato alBottom (in modo da essere sempre sul fondo inferiore del MenuLayout.

L'esecuzione (una tantum) di un pezzo di codice che aggiunge dinamicamente (a runtime) gli item alla TListView partendo dal contenuto della MenuActionList, ci porta in una situazione in cui per ogni action esiste un elemento della TListView. 
Dato che vogliamo preservare la piena funzionalità sia che si tratti di pochi elementi o di molti, sfruttiamo le funzionalità di scroll insite nella TListView per ottenere (senza sforzo) che, qualora gli elementi dovessero essere più di quelli che si possono mostrare fisicamente nello spazio disponibile, l'utente abbia facoltà di vederne solo alcuni ma poter fare scroll della ListView (come di consueto).

Due istanze di TFloatAnimation (MenuDownAnimation e MenuUpAnimation) sono state impostate per espandere il MenuLayout (agendo sulla sua proprietà Height) tanto quanto necessario per mostrare gli elementi del menù (con un effetto grafico piacevole e che potete ulteriormente customizzare modificando le opzioni delle animation stesse) e, viceversa, per collassarlo nuovamente. 

Questo è tutto ciò che ci serve dal punto di vista dei componenti visuali; per quanto riguarda il codice, vi riporto di seguito la porzione che si occupa di travasare le action in elementi della TListView:

procedure TMainForm.BuildMenuItems;
var
  LAction: TAction;
  LIndex: Integer;
  LItem: TListViewItem;
begin
  MenuListView.ItemAppearance.ItemHeight := 75;
  for LIndex := 0 to MenuActionList.ActionCount -1 do
  begin
    LAction := MenuActionList.Actions[LIndex] as TAction;

    LItem := MenuListView.Items.Add;
    try
      LItem.Text := LAction.Text;
      ItemActionDict.Add(LItem, LAction);
    except
      LItem.Free;
      raise;
    end;
  end;
end;

Come potete vedere è tutto abbastanza semplice e si nota l'utilizzo di ItemActionDict: una istanza di TDictionary<TListViewItem, TAction> che mi permette di realizzare una lista associativa fra gli elementi della TListView e le Action della TActionList.
Questa associazione servirà quando, nella gestione dell'evento di click dell'item della TListView, occorre sapere quale action eseguire:

procedure TMainForm.MenuListViewItemClick(const Sender: TObject;
  const AItem: TListViewItem);
var
  LItem: TListViewItem;
  LAction: TAction;
begin
  if ItemActionDict.TryGetValue(AItem, LAction) then
  begin
    if LAction.Execute then
      MenuUp;
  end;
end;

Il resto del codice è molto semplice e lo trovate nei sorgenti completi dell'applicazione di esempio che ho realizzato e di cui vedete alcuni screenshot di seguito (presi con un Nexus 7 e con un iPad):

 




 



Per ottenere un effetto di differenziazione rispetto al normale contenuto della form, ho personalizzato la MenuListView implementando un gestore per l'evento OnUpdatingObjects, che mi fornisce l'occasione di cambiare il colore del testo di ogni item della TListView;

procedure TMainForm.MenuListViewUpdatingObjects(const Sender: TObject;
  const AItem: TListViewItem; var AHandled: Boolean);
begin
  if Assigned(AItem) and Assigned(AItem.Objects) and Assigned(AItem.Objects.TextObject)
     and (AItem.Index < High(FItemColors))
  then
    AItem.Objects.TextObject.TextColor := FItemColors[AItem.Index];
end;

Inoltre, ho voluto personalizzare il colore di sfondo degli item della TListView, per averlo più scuro (differenziando quindi questa TListView dalle altre presenti nell'applicazione), implementando il metodo OnApplyStyleLookup e sfruttando un piccolo hack: modifico dinamicamente un valore dettato dallo style della TListView, in particolare il TColorObject di nome 'itembackground':

procedure TMainForm.MenuListViewApplyStyleLookup(Sender: TObject);
var
  LStyleObject: TFmxObject;
begin
  // hack the ListView style to have a dark background
  LStyleObject := MenuListView.FindStyleResource('itembackground');
  if LStyleObject is TColorObject then
    TColorObject(LStyleObject).Color := TAlphaColorRec.Slategray;
end;

Vi invito a provare ad aggiungere altre Action alla MenuActionList, tante da ottenere l'effetto di scroll nella TListView: il risultato è molto carino!

Conclusioni e materiale

Abbiamo visto come FireMonkey ci offra la possibilità di ottenere con facilità degli effetti grafici (animati) gradevoli e al contempo implementare una funzionalità fondamentale come può essere quella di un menù (con un numero arbitrario di voci) a scomparsa per una applicazione mobile.

Vi lascio anche qualche spunto per sviluppi ulteriori (più o meno facili, in ordine sparso):
  • espandere un po' gli effetti e le animazioni del menù, per migliorarne l'estetica e la user experience;
  • gestire lo stato di abilitazione/visibilità delle action corrispondenti gli item del menù;
  • aggiungere un supporto a icone da abbinare alle voci di menù;
  • migliorare la personalizzazione dei colori delle voci di menù;
  • rimuovere il MenuButton e al suo posto sfruttare delle gestures (o su Android, il pulsante Settings).
Vi lascio come di consueto i link al codice sorgente completo dell'applicazione demo realizzata e ad un'applicazione demo compilata per Android (APK).

Link: Codice sorgente Delphi XE5
Link: Demo APK (da installare sul vostro dispositivo Android)

Buon lavoro e a presto!

Andrea