11/02/15

Come realizzare il menù alla Inbox con GWT e CSS3




css 'minimale'
La notizia che Google ha utilizzato GWT per realizzare Inbox sembra aver ridato linfa vitale al toolkit.
Noi ne troviamo estremamente piacevole anche lo stile, basato su material design, molto pulito e chiaro. Per questo abbiamo provato a riprodurne uno degli elementi caratterizzanti, i.e. il "menù rotolante", provando ad ‘imitare’ alcune delle scelte stilistiche fatte dagli engineers Google.


Per rendere painless la stilizzazione delle componenti (colori, ombre, forme e icone) abbiamo utilizzato la css 'material' che si trova a questo url http://fezvrasta.github.io/bootstrap-material-design inserendola direttamente nel progetto GWT.


Come nostra consuetudine, tutta la UI è descritta in un uiBinder, che riportiamo: 

<!DOCTYPE ui:UiBinder SYSTEM "http://dl.google.com/gwt/DTD/xhtml.ent">
<ui:UiBinder xmlns:ui="urn:ui:com.google.gwt.uibinder"
xmlns:g="urn:import:com.google.gwt.user.client.ui">
 <ui:with field="resources"
type="com.jooink.videochat.client.widgets.InboxMenu.Resources"/>
 <g:SimplePanel>
   <g:HTMLPanel ui:field="panel" addStyleNames='{resources.style.panel}'>


<div class='{resources.style.buttons} {resources.style.button2}'>
 <g:Anchor ui:field="info" styleName='btn'
addStyleNames='btn-primary btn-fab btn-raised mdi-action-info-outline'>
 </g:Anchor>
</div>


<div class='{resources.style.buttons} {resources.style.button1}'>
 <g:Anchor ui:field="share" styleName='btn'
addStyleNames='btn-primary btn-fab btn-raised mdi-social-share'>
 </g:Anchor>
</div>


      <g:Anchor ui:field="head" styleName='btn'
 addStyleNames='btn-warning btn-fab btn-raised {resources.style.head}'>
 <div style="position: absolute;" class="mdi-content-add"></div>
 <div style="position: absolute;" class="mdi-communication-call"></div>
</g:Anchor>
    </g:HTMLPanel>
    </g:SimplePanel>
</ui:UiBinder>


dove si può vedere che:

  • le 3 Anchor sono stilizzate utilizzando i classname di material-bootstrap;
  • gli ui:field share ed info sono solo delle semplici Anchor che abbiamo racchiuso in opportuni div che useremo per la stilizzazione;
  • lo ui:field head è leggermente più complicato in quanto, per come funziona il menu "inbox like", ci vogliono 2 icone, una da usare a menu aperto ed una a menu chiuso.


La classe java corrispondente è altrettanto semplice


package com.jooink.videochat.client.widgets;
/*imports*/
public class InboxMenu extends Composite  {
public interface Style extends CssResource {
String  DEFAULT_CSS = "inboxmenu.gss";
String panel();
String head();
String buttons();
String button1();
String button2();
}
public interface Resources extends ClientBundle {
@Source(Style.DEFAULT_CSS)
Style style();
}


/* getDefaultResources ..., copiato da CellList */


@UiField(provided = true)
Resources resources;
@UiField
HasClickHandlers head;
@UiField
HasClickHandlers share;
@UiField
HasClickHandlers info;
/* getters … */


public InboxMenu() {
this(null);
}
public InboxMenu(Resources resources) {
this.resources =
resources == null ? getDefaultResources() : resources;
this.resources.style().ensureInjected();
initWidget(uiBinder.createAndBindUi(this));
}
}


senza stilizzazione
che ‘espone’ all’esterno tre elementi di tipo HasClickHandlers (ndr, meglio usarli attraverso i getter, se si accede direttamente ai field poi la versione mobile diventa più complicata da fare) pronti per essere wired!


Abbiamo usato ‘gss’ invece che ‘css’ per parametrizzare il comportamento del menu, in quanto i mixin e le variabili negli stili sono comode, anche se non è essenziale.


L'immagine riportata a sinistra a sinistra mostra il menù privo di stilizzazione.

Inserendo le seguenti CSS riusciamo a mimare l'aspetto dell'Inbox menù, anche se ancora non animato:

@def ROTATION_ANGLE            135deg;
@defmixin transform(TRANSFORMATION) {
css 'minimale'
transform: TRANSFORMATION;
-webkit-transform: TRANSFORMATION;
-moz-transform: TRANSFORMATION;
-ms-transform: TRANSFORMATION;
}
.panel, .head, .button1,  .button2, .head {}


.buttons {
  @mixin transform( scale(0.8) );
}
.head > div {
@mixin transform( rotate(ROTATION_ANGLE) );
}
.head > div:first-child {
opacity: 0;
}


La stilizzazione definitiva richiede a questo punto solo di osservare che vogliamo che .buttons siano invisibili quando .panel non è :hover (mentre deve essere visibile .head > div:last-child); viceversa vogliamo che quando il mouse è su .panel i bottoni compaiano assieme a .head > div:first-child; le transition CSS3 fanno il resto :)


/* HEAD */
@def ROTATION_TIME             400ms;
@def FADING_TIME               300ms;
@def ROTATION_ANGLE            135deg;


/* BUTTONS */


@def DISAPPEARING_TIME 300ms;
@def DISAPPEARING_DELAY_DELTA 80ms;
@def DISAPPEARING_DURATION_DELTA 30ms;
@def DISAPPEARING_FUNCTION ease-out;
@def APPEARING_TIME 400ms;
@def APPEARING_DELAY_DELTA 100ms;
@def APPEARING_DURATION_DELTA 50ms;
@def APPEARING_FUNCTION ease-in;
@defmixin transform(TRANSFORMATION) {
transform: TRANSFORMATION;
-webkit-transform: TRANSFORMATION;
-moz-transform: TRANSFORMATION;
-ms-transform: TRANSFORMATION;
}
@defmixin transition(PROP, DURATION, DELAY, FUNCTION) {
transition: PROP DURATION DELAY FUNCTION;
-webkit-transition: PROP DURATION DELAY FUNCTION;
-moz-transition: PROP DURATION DELAY FUNCTION;
}
.panel, .head, .button1, .button2, .button2 {}
.head > div {
 transition: transform ROTATION_TIME, opacity FADING_TIME;
 -webkit-transition: -webkit-transform  ROTATION_TIME, opacity FADING_T;
 -moz-transition: -moz-transform  ROTATION_TIME, opacity FADING_T;
}


.panel:hover .head > div {
@mixin transform( rotate(ROTATION_ANGLE) );
}
.head > div:first-child {
opacity: 1;
}


.head > div:last-child {
opacity: 0;
}


.panel:hover .head > div:first-child {
opacity: 0;
}
.panel:hover .head > div:last-child {
opacity: 1;
}


/* http://css-tricks.com/different-transitions-for-hover-on-hover-off/ */
/* the effect is inverted on mouseenter mouseleave */
.buttons {
 opacity: 0;
 @mixin transform( scale(0.1) );
 transition(all,
   DISAPPEARING_TIME,DISAPPEARING_DURATION_DELTA,
DISAPPEARING_DELAY_DELTA, DISAPPEARING_FUNCTION);
}
.panel:hover .buttons {
opacity:1;
@mixin transform( scale(0.8) );
transition(all, APPEARING_TIME,APPEARING_DURATION_DELTA,
APPEARING_DELAY_DELTA, APPEARING_FUNCTION);
}

Con questa css si ottengono i menu ‘rotolanti’ che vedete nella demo (è possibile farne una variane orizzontale semplicemente specificando “display: inline-blocksulla classe .buttons, come a questo url: menu orizzontale).


Ad onor del vero la demo è leggermente  più complicata del codice riportato in quanto usa timing diversi per i diversi bottoni e filtra (blur) i button mentre compaiono, ma sono dettagli ‘di design’ che abbiamo volutamente omesso in quanto complicavano inutilmente il codice senza aggiungere nulla all’idea generale.


La versione mobile è sfortunatamente meno straightforward, in quanto tutto il funzionamento del menu è basato su :hover che su mobile ha poco senso; il browser mobile ci prova a trasformare gli :hover in una sorta di ‘selected status’ ma il pulsante rosso diventa difficile da usare…
La soluzione è semplicemente quella di gestire a mano una classe ‘selected’ da inserire nel menu quando viene aperto; potete provare il risultato a: versione mobile friendly