Freitag, 4. Juni 2010

Coole iPhone-kompatible "Loading Indicator" im Apple-Stil mit SVG


Seit hunderten von Jahren wurden "loading indicator", "activity indicator", "progress indicator", "spinning wait indicator" oder auch "Throbber" mittels animierter GIFs realisiert, die man sich auf Seiten wie ajaxload.info in allen Farben und Formen generieren lassen kann. Der Vorteil ist, dass die Animation von so ziemlich allen Browsern korrekt dargestellt wird. Die Nachteile bemerkt man erst, sobald man als Webentwickler die Grafik skalieren möchte oder noch viel schlimmer die Grafik auf verschiedenen Hintergründen anzeigen will. Um das Problem zu demonstrieren, habe ich diese Testseite erstellt: http://dev.ubbel.de/html/loading/gif.html
Fangen wir mit der ersten Zeile an: hier sieht alles in Ordnung aus. In der zweiten Zeile wird die gleiche Grafik auf rotem Hintergrund verwendet und man erkennt einen unschönen Rand. Das liegt daran, dass GIF zwar Transparenz unterstützt, aber keine Alpha-Transparenz (Teiltransparenz). In der dritten Zeile wurde eine andere Grafik mit angepasstem Hintergrund verwendet und es ist wieder alles in Ordnung. Wie man sieht, kann man die unschönen Ränder vermeiden, indem man für jeden Hintergrund eine Grafik erstellt - nicht gerade schön, aber es funktioniert.
Was aber, wenn einem der Hintergrund unbekannt ist? Dazu folgende Demonstration: http://dev.ubbel.de/html/loading/gif_alpha.html
Hier wurde mit der CSS3-Funktion "rgba()" ein Teiltransparenter Overlay erzeugt, der Hintergrund von loading_alpha.gif ist auf die Farbe eingestellt, die entsteht, wenn schwarz mit 70 prozentiger Deckkraft auf weißem Hintergrund entsteht. Durch die ausgesprochen komplizierte und höchtwissenschaftliche Berechnung dieser Farbe (#4C4C4C) ist es mir gelungen, dass es zumindest auf dem weißen Hintergrund gut aussieht. Ganz anders sieht es in der unteren Hälfte, dort wo der rote Hintergrund durchscheint, aus - hier sind wieder diese gemeinen Ränder entstanden.
Zusammengefasst: GIFs sind doof und unflexibel!

Also suchte ich nach einer anderen Möglichkeit und bin auf APNG - animierte PNGs - gestoßen, die aber derzeit nur von Firefox und Opera unterstützt werden. Zonk, APNGs sind raus, da mir die Unterstützung von Safari und speziell MobileSafari auf dem iPhone sehr wichtig ist. So suchte ich also weiter...

Meine erste Idee war, eine nicht animierte Grafik, die Alphatransparenz unterstützt, zu nehmen und diese mit der CSS3-Eigenschaft "transform" zu drehen. Da Safari (auch auf dem iPhone) SVGs darstellen können, bot sich dieses Format an (PNGs wären genauso möglich gewesen). Das Ergebnis könnt ihr hier bestaunen: http://dev.ubbel.de/html/loading/css_transform.html
Die Animation ist mit purem CSS3 realisiert und läuft auf dem iPhone dank Hardwarebeschleunigung deutlich flüssiger als auf dem PC.
Der HTML-Teil ist ziemlich einfach und benötigt keine weitere Erklärung:
<img id="loading" src="loading.svg" alt="Laden..."/>
Spannender wird's im CSS-Teil: hier wird zunächst die Animation definiert...
@-webkit-keyframes rotate {
  from {
    -webkit-transform:rotate(0deg);
  }
  to {
    -webkit-transform:rotate(360deg);
  }
}

...und anschließend dem img-Element "#loading" mit einer Umlaufdauer von 5 Sekunden zugewiesen:
#loading {
  height: 200px;
  width: 200px;
  -webkit-animation-name: rotate;
  -webkit-animation-duration: 5s;
  -webkit-animation-iteration-count: infinite;
  -webkit-animation-timing-function: linear;
}

Da ich mich für ein SVG entschieden habe, kann man die Grafik beliebig hoch- oder runterskalieren.
Eine coole Erweiterung dieser Methode ist die Grafik nicht direkt mit einem img-Element anzuzeigen sondern als Maske für einen Container zu verwenden. Dadurch kann man die Farbe des loading indicators mittels der Hintergrundfarbe des Containers steuern.
Das ganze sieht in etwa so aus: http://dev.ubbel.de/html/loading/css_transform_mask.html
Der unheimlich komplexe HTML-Teil vereinfacht sich zu:
<div id="loading"></div>
Der CSS-Teil wird um die CSS3-Eigenschaft "-webkit-mask-image" und die gewünschte Hintergrundfarbe erweitert:
#loading {
  height: 200px;
  width: 200px;
  -webkit-animation-name: rotate;
  -webkit-animation-duration: 5s;
  -webkit-animation-iteration-count: infinite;
  -webkit-animation-timing-function: linear;
  -webkit-mask-box-image: url(loading.svg);
  background-color: red;
}
So schön, unser loading indicator dreht sich. Aber halt, er soll sich ja garnicht drehen! Stattdessen sollte er eigentlich in 30°-Schritten "springen". Blöderweise gibt es noch keine Möglichkeit mit reinem CSS diese Sprünge zu animieren. Denkbar wären in Zukunft "discrete" oder "paced" als "timing-function". Für andere Typen von "Throbber"n, kann diese Methode aber durchaus eingesetzt werden: http://dev.ubbel.de/html/loading/star_css_transform_mask.html.
Ein Nachteil bleibt aber: Die Grafik braucht immer einen Container bzw. ein img-Element und kann nicht wie die doofen GIFs als "background-image" zugewiesen werden. Das erschwert die praktischen Einsatzmöglichkeiten, da man an jeder Stelle, an der ein indicator angezeigt werden soll, ein Container platziert werden muss.

Suchen wir also wieder weiter... und da wir schon bei SVG sind bleiben wir doch gleich hier. Im Gegensatz zu CSS bietet SVG nämlich die Möglichkeit einer "diskreten", d.h. "springenden" Animation.
Das Ergebnis ist hier zu bestaunen: http://dev.ubbel.de/html/loading/svg.html
Perfekt, die Animation stimmt jetzt und läuft sehr flüssig.
Der HTML-Teil ist wieder äußerst komplex:
<img id="loading" src="loading_animated.svg" alt="Laden..."/>
Auch der CSS-Teil ist deutlich komplizierter geworden:
#loading {
  height: 200px;
  width: 200px;
}

Eine kleine Erklärung vielleicht: "height" gibt die Höhe der Grafik an und "width" die Breite :-P

Na ja, viel interessanter ist der SVG-Teil:
<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="320" height="320">
  <g>
    <line id="line" x1="15" y1="160" x2="65" y2="160" stroke="#111" stroke-width="30" style="stroke-linecap:round"/>
    <use xlink:href="#line" transform="rotate(30,160,160)" style="opacity:.0833"/>
    <use xlink:href="#line" transform="rotate(60,160,160)" style="opacity:.166"/>
    <use xlink:href="#line" transform="rotate(90,160,160)" style="opacity:.25"/>
    <use xlink:href="#line" transform="rotate(120,160,160)" style="opacity:.3333"/>
    <use xlink:href="#line" transform="rotate(150,160,160)" style="opacity:.4166"/>
    <use xlink:href="#line" transform="rotate(180,160,160)" style="opacity:.5"/>
    <use xlink:href="#line" transform="rotate(210,160,160)" style="opacity:.5833"/>
    <use xlink:href="#line" transform="rotate(240,160,160)" style="opacity:.6666"/>
    <use xlink:href="#line" transform="rotate(270,160,160)" style="opacity:.75"/>
    <use xlink:href="#line" transform="rotate(300,160,160)" style="opacity:.8333"/>
    <use xlink:href="#line" transform="rotate(330,160,160)" style="opacity:.9166"/>
    
    <animateTransform attributeName="transform" attributeType="XML" type="rotate" begin="0s" dur="1s" repeatCount="indefinite" calcMode="discrete"
    keyTimes="0;.0833;.166;.25;.3333;.4166;.5;.5833;.6666;.75;.8333;.9166;1"
      values="0,160,160;30,160,160;60,160,160;90,160,160;120,160,160;150,160,160;180,160,160;210,160,160;240,160,160;270,160,160;300,160,160;330,160,160;360,160,160"/>
  </g>
</svg>
Zunächst wird eine kleine Linie "#line" gezeichnet, diese dann 11 mal geklont und jeweils um 30° gedreht (die erste Zahl von "rotate()" steht für den Drehwinkel in Grad, die beiden anderen legen den Drehmittelpunkt fest, hier in der Mitte der Grafik) und die Deckkraft abgestuft.
"animateTransform" ist für die Animation verantwortlich: in "keyTimes" sind in 1/12 Schritten die Zeiten für die in "values" angegebenen Werte für "rotate()" definiert. Durch den angegebenen "calcMode" ("discrete") wird zwischen den Werten nicht interpoliert, sondern "gesprungen".
OK, die Animation läuft wie sie soll im Safari, blöderweise aber nicht auf dem iPhone. Versuche die Grafik mit dem object- oder embed-Element anstatt des img-Elements einzubinden scheiterten ebenfall, da zwar die Animation lief, leider aber diese zwei Elemente einen unveränderlichen weißen Hintergrund haben. Dann hätte man gleich so doofe GIFs nehmen können.
ABER, die Rettung: Bindet man die Grafik als "background-image" ein, läuft die Animation perfektísimo: http://dev.ubbel.de/html/loading/svg_background.html
HTML-Teil ohne Kommentar:
<div id="loading"></div>
CSS-Teil:
#loading {
  height: 200px;
  width: 200px;
  background-image: url(loading_animated.svg);
  -webkit-background-size: 200px;
}

Pretty simple, right?
Geil, nutzen wir die Euphorie und bauen noch einen dezenten Schatten ein, der auf nicht weißem Hintergrund ganz nett aussieht: http://dev.ubbel.de/html/loading/svg_background_shadow.html
"#line" wird dazu einfach durch diese Gruppe ersetzt:
<g id="line" stroke-width="30" style="stroke-linecap:round">
  <line x1="20" y1="165" x2="70" y2="165" stroke="white" style="opacity:.7"/>
  <line x1="15" y1="160" x2="65" y2="160" stroke="#111"/>
</g>
Es wird also unter die eigentliche Linie noch eine schwächere, leicht versetze Linie gezeichnet.


Abschließend noch ein Anwendungsbeispiel unseres wunderschönen loading indicators in einer iPhone-Webapp:



Der HTML-Teil sieht einfach wie immer aus:
<div id="loading">Laden...</div>
Der CSS-Teil ist etwas größer geworden, aber trotzdem noch selbsterklärend:
#loading {
  min-height: 80px;
  -webkit-border-radius: 8px;
  -webkit-box-shadow: rgba(0, 0, 0, .3) 1px 1px 3px;
  width: 90%;
  margin: 0 5%;
  position: absolute;
  top: 55px;
  background: url(img/loading_white.svg) 50% 16px no-repeat, rgba(0, 0, 0, .7);
  -webkit-background-size: 25px;
  color: #fff;
  text-shadow: rgba(0, 0, 0, 1) 0 -1px 1px;
  line-height: 40px;
  text-align: center;
  font-size: 18px;
  font-weight: bold;
  z-index: 10;
  padding: 40px 10px 20px;
  -webkit-box-sizing: border-box;
}


Also nochmal zusammengefasst: Wir haben herausgefunden, dass GIFs doof sind, APNGs (noch) keine Alternativen sind, CSS3 auch noch nicht ganz ausgereift ist und SVGs ziemlich cool sind. Außerdem haben wir einen super sexy loading indicator mit modernen "Open Web"-Standards erstellt und gesehen wie man ihn wunderschön einsetzen kann.

Noch eine kleine Anmerkung was die Dateigröße betrifft: die GIFs haben um die 4 KB, die finale SVG nur 2 KB. Dabei darf man nicht vergessen, dass die GIFs 32 x 32 Pixel groß sind und die SVGs auf jede beliebige Größe skaliert werden können.

Für weitere Fragen, Anmerkungen oder Ehrungen bitte die Kommentare benutzen :-)