Rich Graphics mit OpenGL ES

Jenseits des Standard-UI
Kommentare

Wenn die Standard-Widgets von Android an ihre Grenzen stoßen, sind alternative Wege gefragt. Dafür bietet Android dem Entwickler zahlreiche APIs auf unterschiedlichen Abstraktionsebenen an. In dieser Situation gilt es, früh eine Entscheidung zu treffen und sich der Konsequenzen bewusst zu sein. Denn die App-Welt ist selten schwarzweiß und verlangt nach einer Mischung aus verschiedenen APIs. Wir zeigen anhand eines konkreten Projekts, was den Entwickler jenseits der Standard-Widgets erwartet und wie er trotzdem an den richtigen Stellen auf etablierte Komponenten zurückgreifen kann.

Welche nehme ich denn jetzt? Diese Frage ist angesichts der mannigfaltigen Möglichkeiten im Bereich der Grafik-APIs auf der Android-Plattform nicht immer leicht zu beantworten. Und da die Auswahl der API meistens früh im Projekt erfolgt, hat diese Entscheidung oft weitreichende Konsequenzen. Um sich derer bewusst zu sein, lohnt sich ein Blick auf die Architektur von Android aus Sicht der Grafikausgabe. Im Diagramm in Abbildung 1 betrachten wir die verschiedenen Grafik-APIs von Android aus Sicht der Anwendung. Jede grafische Ausgabe endet irgendwann in einer so genannten Surface, die den Puffer für die Pixel repräsentiert. Am einfachsten zu verwenden sind die Standard-Widgets von Android, also Buttons, Checkboxen etc. Sie werden über ein High-Level API im Package android.wigdet verwendet. Letztendlich nutzen diese Widgets das Canvas API im Package android.graphics, um Linien, Gradienten und Formen zu zeichnen. Von dort aus verlässt man die Java-Welt, denn das Canvas API kommuniziert mit der SGL (Skia Graphics Library), die entweder direkt auf die Surface schreibt (in der Prä-3.0-Android-Welt) oder das native OpenGL ES API (ab 3.0) verwendet, um die gewünschten Objekte zu zeichnen.

Abb. 1: Die Grafik-APIs von Android

Das Widget API ist die abstrakteste und einfachste Möglichkeit, um grafische Elemente zu erzeugen, und sollte so lange wie möglich verwendet werden. Reicht das nicht aus, kann man seine eigenen Widgets erstellen, indem man direkt mit dem Canvas API interagiert. Dass dieses API nicht nur auf 2-D-Darstellungen beschränkt ist, zeigen wir im Abschnitt „3-D-Effekte ohne OpenGL“. Reichen diese einfachen 3-D-Möglichkeiten nicht aus oder sieht man Probleme mit der Performance auf 2.x-Geräten (hier verwendet die SGL ja noch kein OpenGL ES), muss man sich weitere Alternativen anschauen.
Weit verbreitet ist das OpenGL ES API, das wir später noch detaillierter besprechen werden. Es kann auf Java-Ebene über das Package android.opengl angesprochen werden oder über JNI direkt auf dem nativen Weg. Letzteres ist allerdings nur in Ausnahmefällen zu empfehlen, zum Beispiel wenn bestehender OpenGL-Code portiert werden soll. Hat man den Luxus, nur mit Geräten ab der Version 3.0 zu arbeiten, lohnt sich ein Blick auf das Render Script API. Es bietet die Vorteile des OpenGL ES API mit einer relativ leicht zu erlernenden und plattformunabhängigen C-basierten Syntax. Generell gilt es, immer das einfachste API zu verwenden, solange es den Anforderungen genügt.

Tipp: das Bitmap-Budget

Jede Android-Applikation hat ein eigenes Bitmap-Budget, das nicht immer dem Heap der Dalvik VM entspricht. Die Berechnung dieses Budgets unterscheidet zwischen verschiedenen Android-Versionen und ist auch abhängig von der Hardware und der Konfiguration des Herstellers. Um Speicherprobleme mit Bildern zu vermeiden, gibt es ein paar Regeln:
1. Werden Bilder nicht mehr benötigt, muss unbedingt die recycle()-Methode aufgerufen werden. Aber selbst danach gibt Android den Speicher nicht deterministisch frei.
2. Bilder sollten nicht von der ImageView skaliert werden. Das verbraucht unnötig Speicher.
3. Am besten lädt man die Bilder direkt in der richtigen Größe in den Speicher. Hierbei helfen die Optionen inSampleSize und justDecodeBounds der BitmapFactory.

Tipp: Hardwarebeschleunigung für 2-D-Grafik

Wie in Abbildung 1 zu sehen ist, kann die SGL ab der Android-Version 3.0 das Open GL ES API des Geräts verwenden, um zweidimensionale Grafiken zu zeichnen. Das führt in der Regel zu einer wesentlich besseren Performance. Um dieses Feature zu aktivieren, setzt man in der AndroidManifest.xml-Datei das Attribut android:hardwareAccelerated=“true“. Das kann auf Activity– oder Application-Ebene erfolgen. Speziell bei selbstgeschriebenen Widgets oder Views empfiehlt es sich, detailliert zu testen, da sich das 2D Canvas API zum Teil anders verhält als vorher!

Aufmacherbild: Abstract 3d background illustration. von Shutterstock / Urheberrecht: : Ute Schwendt

[ header = 3-D-Effekte ohne OpenGL]

3-D-Effekte ohne OpenGL

Möchte man seine App mit 3-D-Effekten aufwerten, muss man nicht sofort Render Script oder OpenGL verwenden. Denn neben der android.hardware.Camera existiert noch eine zweite Klasse, die denselben Namen trägt. Das sorgt nicht nur leicht für Verwechslung, sondern führt auch dazu, dass die Klasse android.graphics.Camera  oft übersehen wird. Doch durch sie ist es möglich, einfache 3-D-Effekte mit dem bekannten 2-D-Canvas-API zu erzeugen. Da die Dokumentation dieser Klasse sehr spärlich ist, werden wir hier Schritt für Schritt ein Beispiel betrachten, in dem eine ListView mit 3-D-Effekt erzeugt wird. Schauen Sie sich Abbildung 2 an, um sich ein Bild vom Endergebnis zu machen. Oder Sie laden sich gleich den Sourcecode [2] für unser Beispiel und probieren es auf ihrem Gerät aus.

Abb. 2: 3-D-Effekte mit dem Canvas API

Schritt 1: eine eigene ListView

Zuerst wird die ListView-Klasse erweitert (Listing 1). Das Camera und das Matrix-Objekt werden später genutzt, um die einzelnen Elemente der Liste im Raum zu bewegen. Das Paint-Objekt wird zum Zeichnen der Listenelemente verwendet. Mithilfe des FILTER_BITMAP_FLAG vermeiden wir dabei unschöne Treppchenbildung an den Kanten.

public class ListView3d extends ListView {
  private final Camera mCamera = new Camera();
  private final Matrix mMatrix = new Matrix();
  private final Paint mPaint = new Paint(Paint.FILTER_BITMAP_FLAG);

  public ListView3d(Context context, AttributeSet attrs) {
    super(context, attrs);
    mPaint.setAntiAlias(true);
  }
}

Schritt 2: Verwendung von DrawingCache

Als Nächstes übernehmen wir durch Überschreiben von drawChild() die Kontrolle über die Darstellung der Elemente der Liste (Listing 2). In der Methode getChildDrawingCache() erzeugen wir von der Child View zuerst ein Bitmap. So wird um den Preis eines erhöhten Speicherbedarfs die Performance gesteigert, da das Zeichnen eines Bitmaps nicht so aufwendig ist wie das Neu-Rendern der gesamten Child View in jedem Frame. Mithilfe der Matrix und canvas.drawBitmap() wird das Bitmap zuletzt an die richtige Position auf dem Bildschirm gebracht.

  @Override
  protected boolean drawChild(Canvas canvas, View child, long drawingTime) {
    Bitmap bitmap = getDrawingCache(child);
    // (top,left) is the position of the child inside the list 
    final int top = child.getTop();
    final int left = child.getLeft();

    mMatrix.reset();
    mMatrix.postTranslate(left, top);
    canvas.drawBitmap(bitmap, mMatrix, mPaint);
    return false;
  }
}
  private Bitmap getChildDrawingCache(final View child){
    Bitmap bitmap = child.getDrawingCache();
    if (bitmap == null) {
      child.setDrawingCacheEnabled(true);
      child.buildDrawingCache();
      bitmap = child.getDrawingCache();
    }
    return bitmap;
  }

Schritt 3: Der 3-D-Effekt

Bisher unterscheidet sich unsere Liste nicht von einer gewöhnlichen. Doch wir haben durch das Matrix-Objekt den Schlüssel zur Erzeugung unseres 3-D-Effekts in der Hand. Laut Dokumentation der Matrix-Klasse existieren jedoch nur Methoden zum Verschieben, Drehen und Skalieren in 2-D. Abhilfe schafft hier die bereits erwähnte android.graphics.Camera. Sie stellt das notwendige API bereit, um eine 3-D-Transformation auf Matrix-Objekte anzuwenden.
Zuerst wird es nur darum gehen, die Elemente in Abhängigkeit von ihrer Position auf der Z-Ebene nach hinten zu verlagern. Je weiter ein Element von der Mitte des Bildschirms entfernt ist, umso kleiner soll es erscheinen. Damit der Effekt interessanter wirkt, kommt eine Kreisformel zum Einsatz. Die Verschiebung in den Bildschirm hinein wird also überproportional stärker, je weiter ein Element von der Mitte der Liste entfernt ist (Listing 3). Nachdem in drawChild() der Abstand von der Mitte des Child-Elements zur Mitte der Liste berechnet wurde, wird in prepareMatrix() durch mCamera.translate() die Verschiebung in Z-Richtung durchgeführt. Danach übertragen wir diese Transformation durch mCamera.getMatrix() auf unsere Zeichenmatrix. In onDrawChild() legen wir mit mMatrix.preTranslate() den Fixpunkt der Z-Verschiebung auf die Mitte des Elements.

  @Override
  protected boolean drawChild(Canvas canvas, View child, long drawingTime) {
    Bitmap bitmap = getChildDrawingCache(child);
    // (top,left) is the pixel position of the child inside the list 
    final int top = child.getTop();
    final int left = child.getLeft();
    // center point of child
    final int childCenterY = child.getHeight() / 2;
    final int childCenterX = child.getWidth() / 2;
    //center of list
    final int parentCenterY = getHeight() / 2;
    //center point of child relative to list
    final int absChildCenterY = child.getTop() + childCenterY;
    //distance of child center to the list center
    final int distanceY = parentCenterY - absChildCenterY;
    //radius of imaginary cirlce
    final int r = getHeight() / 2;
  
    prepareMatrix(mMatrix, distanceY, r);

    mMatrix.preTranslate(-childCenterX, -childCenterY);
    mMatrix.postTranslate(childCenterX, childCenterY);

    mMatrix.postTranslate(left, top);
    canvas.drawBitmap(bitmap, mMatrix, mPaint);
    return false;
  }
  
  private void prepareMatrix(final Matrix outMatrix, int distanceY, int r){
    //clip the distance
    final int d = Math.min(r, Math.abs(distanceY));
    //use circle formula
    final float translateZ = (float) Math.sqrt((r * r) - (d * d));

    mCamera.save();
    mCamera.translate(0, 0, r - translateZ);
    mCamera.getMatrix(outMatrix);
    mCamera.restore();
  }

Schritt 4: 3-D-Rotation

Der erreichte Effekt ist noch wenig spektakulär. Aber das wird durch eine Rotation um die X- und Y-Achse jetzt geändert. Dazu wird die prepareMatrix()-Methode erweitert (Listing 4). Ähnlich wie bei der Verschiebung in Z-Richtung ist die Rotation abhängig von der Entfernung zur Mitte der Liste. Befindet sich ein Element exakt in der Mitte, so beträgt der Rotationswinkel 0 Grad, während an den Rändern um 90 Grad gedreht wird.

  private void prepareMatrix(final Matrix outMatrix, int distanceY, int r){
    //clip the distance
    final int d = Math.min(r, Math.abs(distanceY));
    //use circle formula
    final float translateZ = (float) Math.sqrt((r * r) - (d * d));
    //solve for t: d = r*cos(t)
    double radians = Math.acos((float) d / r);
    double degree = 90 - (180 / Math.PI) * radians;

    mCamera.save();
    mCamera.translate(0, 0, r - translateZ);
    mCamera.rotateX((float) degree); 
    if (distanceY < 0) {
      degree = 360 - degree;
    }
    mCamera.rotateY((float) degree);
    mCamera.getMatrix(mMatrix);
    mCamera.restore();
  }

3-D mit dem Canvas API: Fazit

Mithilfe von etwas Schulmathematik und weniger als 80 Zeilen Programmcode wurde eine ListView mit einem echten 3-D-Effekt aufgewertet. Der Schlüssel zum Erfolg lag in der Verwendung der android.graphics.Camera-Klasse, mit der wir Android Views in dreidimensionalem Raum bewegen können. Durch die Verwendung des DrawingCache-Mechanismus haben wir zudem sichergestellt, dass auch bei komplexen Child Views die Performance nicht einbricht. Noch ein kleiner Tipp am Rande: Variieren Sie den 3-D-Effekt, zum Beispiel durch Versetzung des Fixpunktes:

  mMatrix.preTranslate(-childCenterX, -childCenterY + distanceY);
  mMatrix.postTranslate(childCenterX, childCenterY - distanceY);

Oder ändern Sie im XML-Layout der Chil Views die Position des Icons:

   android:drawableLeft="@drawable/inovex_logo"

[ header = OpenGL ES]

OpenGL ES

Wollen Sie komplexe Daten dreidimensional visualisieren, Graphen mit vielen Tausend Datenpunkten animieren oder einfach nur eine wirklich außergewöhnliche Benutzeroberfläche realisieren? Immer dann, wenn Sie wirklich viele Pixel über den Bildschirm schieben müssen und die Möglichkeiten des Canvas API nicht ausreichen, kann OpenGL ES die Lösung sein. Seien Sie sich aber auch bewusst, dass der Einsatz von OpenGL in Ihrem Projekt zu erheblichen Mehraufwänden führen kann. Daher wird jetzt gezeigt, wo sich die größten Fallstricke verbergen und wie man sie umgehen kann.

OpenGL ES: Einsatzmöglichkeiten

Im ersten Teil dieses Artikels haben Sie erfahren, wie das Canvas API genutzt werden kann, um einfache 3-D-Effekte darzustellen. Allerdings stößt die vorgestellte Methode an ihre Grenzen, sobald es um die Darstellung von komplexeren Szenen oder Daten geht. Wenn also echtes 3-D gefragt ist, ist OpenGL das API der Wahl. Das liegt vor allem daran, dass die einzige Alternative, das Render Script API, an mangelnder Unterstützung durch Endgeräte krankt, da Render Script erst ab Honeycomb verfügbar ist. OpenGL ES wird dagegen von allen Androiden unterstützt – ab Android 2.2 sogar in der OpenGL ES Version 2.0 mit Shader Support. Die hohe Grafikleistung von OpenGL kann man wiederum nutzen, um Apps mit wirklich außergewöhnlichem GUI zu entwickeln. Schauen sie sich die offizielle Android Gallery App von Cooliris an, um zu sehen, wie ein gut gemachtes 3-D-GUI eine App aufwerten kann.

Worauf muss ich achten?

Doch diese Vorteile und Möglichkeiten gibt es nicht umsonst. OpenGL ist ein durchaus komplexes Low-Level API mit entsprechender Lernkurve. Auch müssen wir viele der Annehmlichkeiten des Android-View-Frameworks hinter uns lassen. Denn OpenGL ist kein GUI-Framework, das uns beim Layout der GUI-Komponenten und mit vorgefertigten Widgets wie ListViews hilft. Grundlegende Dinge wie Animationen und Verarbeitung von Nutzereingaben müssen per Hand erledigt werden. Doch Sie müssen diesen Herausforderungen nicht allein begegnen, denn Frameworks wie libgdx können Ihnen viel Arbeit abnehmen.
Zusätzlich haben Sie die Möglichkeit, OpenGL mit normalen Android Views zu kombinieren, indem Sie diese als Overlay über die OpenGL-Ansicht legen oder in modale Dialoge einbetten. Diese Technik erlaubt es Ihnen, (die eher langweiligen) Teile ihrer App, zum Beispiel einen Settings-Dialog, mit den bewährten Bordmitteln umzusetzen. Doch welchen Weg man auch einschlägt, als Laie sollte man die Einarbeitungszeit in OpenGL eher in Wochen statt Tagen messen. Eine zusätzliche Schwierigkeit stellt die unglaubliche Vielfalt an Android-Geräten dar. Unterschiedliche Grafikchips, Treiber und Speichergrößen treiben den Testaufwand in die Höhe und erzwingen oft die Implementierung von Workarounds für gerätespezifische Besonderheiten.
In einem unserer letzten Projekte nutzten wir aus diesem Grund den Sourcecode der Cooliris Gallery als Basis für eine Business Intelligence App. Das Betrachten von Fotos und das Betrachten von Business-Reports sind im Grunde sehr ähnliche Tätigkeiten, und erfreulicherweise ließ sich der Programmcode der Gallery relativ leicht für unsere Zwecke erweitern. Außerdem hatten wir so eine Codebasis, die erwiesenermaßen auf fast allen Androiden stabil läuft. Doch wie so oft bei Business-Apps mussten wir zusätzlich komplexe Ansichten umsetzen, die verschachtelte Listen, Radiobuttons und Checkboxes enthalten. Solche eher aufwändigen UI-Elemente wollten wir aufgrund des damit verbunden Entwicklungsaufwands nicht komplett in OpenGL umsetzen. Doch als wir testweise einen Dialog mit einer sehr einfachen Liste einblendeten, mussten wir feststellen, dass die Framerate auf Tablets unter 15 fps fiel. Aber das ist nicht das einzige Problem, das auftreten kann, wenn man Android Widgets mit OpenGL kombiniert.

Die Mischung macht’s

Um zu verstehen, wie man die Probleme beim Kombinieren von Android Widgets und OpenGL am besten lösen kann, muss man wissen, wie eine Android OpenGL App grundsätzlich aufgebaut ist. Die Grundlage fast jeder Android OpenGL App ist die GLSurfaceView. Diese spezielle View bietet eine Fläche, auf der die 3-D-Inhalte gezeichnet werden und stellt zugleich die Verbindung zur View Hierarchy der App dar. Außerdem verwaltet er einen eigenen Render Thread. Doch die GLSurfaceView ist keine normale View. Verwendet man zusätzlich noch andere Android Widgets, sei es direkt daneben, als Overlay oder in einem separaten Dialog, so muss man folgende Punkte beachten:

  1. Die Framerate beim Scrollen von Listen kann sehr schlecht sein.
  2. Die GLSurfacveView wird sich nicht animieren (verschieben, skalieren, rotieren) lassen.
  3. Die Framerate beim Animieren von anderen Views kann sehr schlecht sein.

Der Einbruch der Framerate tritt vor allem bei Tablets auf, da hier, anders als bei Telefonen, die Leistung der Hardware oft in einem weniger guten Verhältnis zur Pixelanzahl des Displays steht. Ein weiterer Grund für diesen Leistungsabfall liegt in einer besonderen Eigenschaft der GLSurfaceView: Anders als normale Views werden die Inhalte der GLSurfaceView in einen separaten Puffer (Surface) gerendert. Bevor die Inhalte unserer App aufs Display kommen, müssen also verschiedene Surfaces vom System kombiniert werden. Die Leistungseinbußen können Sie aber stark abmildern, indem Sie bewegte Elemente wie ListViews, GridViews und Scrollviews nur anzeigen, wenn Sie zugleich das Rendern der GLSurfaceView pausieren. Dazu gibt es verschiedene Möglichkeiten:

  1. Man verwendet den Render Mode RENDER_WHEN_DIRTY
  2. Man ruft die onPause()-Methode der GLSurfaceView auf, muss aber in Kauf nehmen, dass der Render-Kontext verloren geht und Texturen später neu geladen werden müssen.
  3. Man überprüft ein selbst gesetztes Flag und beendet die onDrawFrame()-Methode vorzeitig, wenn es gesetzt wurde.

Leider gibt es für die Einschränkung, dass die GLSurfaceView nicht animiert werden kann, keine zufriedenstellende Lösung. Abhilfe verspricht jedoch die TextureView [10], die aber erst ab Ice Cream Sandwich zur Verfügung steht.

Fazit

Bei der Planung, welche Grafik-APIs in einem Projekt eingesetzt werden sollen, gilt es folgende Regeln zu beachten:

  1. Es empfiehlt sich, so lange wie möglich die Standard-Widgets zu verwenden und zu stylen. Eigene Widgets sind ebenfalls recht einfach zu implementieren und stellen kein größeres Risiko dar.
  2. Bevor man den Schritt in die Welt von Open GL ES beziehungsweise Render Script macht, gilt es zu prüfen, ob nicht einfache 3-D-Effekte über das Canvas API ausreichend sind.
  3. Entschließt man sich für Open GL ES, empfehlen wir, den Einsatz von Frameworks zu prüfen oder auf bestehende Applikationen zurückzugreifen.

Wo immer möglich sollte eine Mischung mit bestehenden Standard-Widgets eingeplant und berücksichtigt werden. Dann steht der Umsetzung von ausgefallenen und performanten Benutzerschnittstellen nichts im Weg.

Tipp: Was ist OpenGL ES?

Die Open Graphics Library erblickte vor fast 20 Jahren das Licht der Welt und wurde seitdem kontinuierlich weiterentwickelt. Die etwa 250 Befehle umfassende OGL API ermöglicht es, zwei- und dreidimensionale Objekte mit vielfältigen Spezialeffekten auf den Bildschirm zu bringen. In leicht abgewandelter Form, als OpenGL ES (Embedded Systems) unterstützen alle Android-Geräte diesen Standard. Da mit OpenGL ES 1.1 und 2.0 zwei inkompatible Versionen existieren, müssen Sie entscheiden, welche Version Sie einsetzen wollen. Wenn Sie unserer Empfehlung folgen, steigen Sie direkt mit OpenGL ES 2.0 ein. Sie erhalten dadurch nicht nur eine höhere Performance [11], sondern durch Shader auch mehr Kontrolle über das Rendering. Wenn Ihre App allerdings auf Androiden der Version 2.1 oder tiefer laufen soll, führt kein Weg an OpenGL ES 1.1 vorbei, da OpenGL ES 2.0 erst ab Android 2.2 zur Verfügung steht. Wenn die Thematik neu für Sie ist, dürften Sie sich jetzt fragen: Was ist denn bitte ein Shader? Im Grunde ist es relativ simpel: Ein Shader ist ein kleines Programm, das direkt vom Grafikprozessor ausgeführt wird. Durch diese Programme steuern Sie das Aussehen der angezeigten Objekte oder erzeugen Spezialeffekte. Da Shader aber in einer speziellen Programmiersprache, der GLSL (OpenGL Shading Language) geschrieben werden müssen, ergibt sich auch ein erhöhter Lernaufwand im Gegensatz zu früheren OpenGL-ES-Versionen.


Unsere Redaktion empfiehlt:

Relevante Beiträge

Meinungen zu diesem Beitrag

X
- Gib Deinen Standort ein -
- or -