Von rechts nach links

Verdrehte Welt: Right-to-Left-Unterstützung in Android

Danny Preußler
android-right-to-left

© Shutterstock / Bloomua

Software funktioniert global. Ein einzelner Entwickler mag eine App in einem kleinen Dorf irgendwo auf unserem Erdball entwickeln. Sichtbar wird sie in den diversen App Stores in der ganzen Welt. Internationalisierung von Apps ist daher ein wichtiges Thema. Der Probleme mit unterschiedlichen Zahlen-, Datums- und Währungsformatierungen sind sich viele Entwickler bewusst. Auch wenn es um Sprachunterstützung geht, weiß der Android-Entwickler, dass man die Übersetzungen in XML-Dateien ablegt und das System die Arbeit übernimmt. In einigen Fällen stimmt dies leider nicht ganz.

Unsere Sprache entstammt der lateinischen und ist eine waagerechte, rechtsläufige Schrift. Das ist jedoch nicht in allen Sprachen der Fall. Die so genannten sunnitischen Sprachen, beispielsweise Hebräisch und Arabisch, sind waagerechte linksläufige Schriften d. h., man liest sie von rechts nach links. Was hat das für Auswirkungen auf unsere App, auf das Design? Was muss man beachten? Dieser Artikel versucht, ein wenig Licht ins Dunkle zu bringen.

Die Unterstützung von Right-to-Left (RTL) in Android-Telefonen ist mittlerweile sehr gut. Beachtet man einige Regeln, wird die App recht passabel in diesen Sprachen. Auch wird dem Entwickler, ob er will oder nicht, RTL mehr und mehr in Form von Warnungen im Android Studio begegnen.

Textrichtung

Der offensichtliche Unterschied der Leserichtung muss sich in allen Texten, die wir in unserer App haben, widerspiegeln. Das Schöne ist, dass Android uns dabei hilft. Ein hebräischer Text wird vom System bereits korrekt dargestellt, die Wörter laufen von rechts nach links. Wo wir dem Telefon jedoch helfen müssen, ist anzugeben, wie wir uns die Ausrichtung des Textes innerhalb des UI vorstellen.

Ein Text, der links ausgerichtet ist, soll vermutlich im Hebräischen rechts ausgerichtet sein. Gibt man einer TextView keine weiteren Anweisungen bez. der Ausrichtung, passiert auch genau das. Wenn wir die Ausrichtung jedoch manuell festsetzen z. B. mittels

android:gravity="left"

dann wird das Telefon auch bei einer RTL-Sprache den Text links ausrichten. Dies ist vermutlich nicht der gewünschte Effekt.

Wie können wir also die Orientierung selbst bestimmten, ohne dadurch die RTL-Unterstützung zu stören? Um dieses Problem zu beheben, wurden mit API Level 17 (Android 4.2) die neuen Schlüsselwörter start bzw end eingeführt, die in der App anstelle von left und right Verwendung finden sollten. Start ist als der Anfang eines Texts gedacht, in der westlichen Hemisphäre also links, in RTL-Sprachen jedoch rechts bedeutend.Wenn man dies beachtet, hat man den ersten Schritt zur Unterstützung einer neuen Zielgruppe geschafft. Seit Kurzem warnt uns auch das Lint-Werkzeug aus dem Android SDK hierüber:

Use "start" instead of "left" to ensure correct behavior in right-to-left-locales

Dies ist ein schönes Beispiel für die Wichtigkeit von solchen Warnungen, auf die man am besten mit einer Zero Tolerance Policy anstrebt, um keine dieser Hinweise zu verpassen. Auf diese Weise kann ein Verstoß, z. B. automatisiert auf dem Jenkins, als instabiler Build gewertet werden.

Ränder (Margins und Paddings)

In den meisten Apps stößt man dann schnell auf ein Problem. Da wir (ob Entwickler oder Designer) das Oberflächendesign aus der uns bekannten Leserichtung erstellen, machen wir uns um den rechten Bildschirmrand oft weniger Sorgen. Einer Textview mit einem kurzen Text geben wir oft die Margin nach links; dass man das Gleiche für den rechten Rand setzen muss, daran denkt man oft nicht. Dass uns dieser Abstand dann fehlt, wenn der Text plötzlich vom rechten Rand beginnt, merkt man oft erst, wenn man es mit eigenen Augen auf dem Gerät sieht. Am besten setzt man direkt entweder Margins oder Paddings für beide Seiten oder baut auch hier auf die neuen Alternativen von start und end wie beispielsweise:

android:layout_marginStart

Erfreulicherweise gibt es auch hier wieder eine entsprechende Lint-Warnung:

Consider adding android:layout_marginStart to better support right-to-left-layouts

Sollte die eigene App ältere Geräte unterstützen, so kann man, wie es Lint hier vorschlägt, beide Attribute setzen:

        <TextView android:id="@+id/login_forgot"
             android:layout_marginLeft="1dp"
             android:layout_marginStart="1dp"
             android:layout_width="wrap_content"
             android:layout_height="wrap_content"
             android:text="@string/login_forgot_password"/>

Der aufmerksame Leser wird sich fragen, warum hier nicht einfach nur

android:layout_marginStart

genügt. Anders als bei gravity, wo ein textueller Wert gesetzt wird, haben wir es hier mit einem Attribut zu tun, das auf alten Versionen leider nicht existiert. Auch hier wird man netterweise gewarnt, falls man die alte Schreibweise vergisst:

To support older versions than API 17 you should also add android:layout_marginLeft

Nun werden die XML-Dateien durch obige Duplikation leider nicht unbedingt lesbarer. Daher sollte man sich überlegen, diese Attribute in style-Dateien auszulagern. Die „alten“ links-rechts-Werte können in die Default-styles.xml-Datei im Standard-res/values-Ordner gelegt werden, eine überschreibende findet im res/values-v17-Ordner ihren Platz (Listing 1).

Listing 1

Layout: 
    <TextView android:id="@+id/login_forgot"
        style="@style/login_forget"
        android:text="@string/login_login_forgot_password"/>

styles.xml: 
    <style name="login_forget">
        <item name="android:layout_width">wrap_content</item>
        <item name="android:layout_height">wrap_content</item>
        <item name="android:layout_marginLeft">1dp</item>
    </style>

v17/styles.xml: 
    <style name="login_forget">
        <item name="android:layout_width">wrap_content</item>
        <item name="android:layout_height">wrap_content</item>
        <item name="android:layout_marginStart">1dp</item>
    </style>

Tipp: Da wir dem DRY (Don’t Repeat Yourself) treu sind, optimieren wir das noch leicht durch Auslagern der lästigen Höhen- und Breitenattribute (Listing 2).

styles.xml:
    <style name="wrap_wrap">
        <item name="android:layout_width">wrap_content</item>
        <item name="android:layout_height">wrap_content</item>
    </style>

    <style name="login_forget" parent="wrap_wrap">
        <item name="android:layout_marginLeft">1dp</item>
    </style>

v17/styles.xml: 
    <style name="login_forget" parent="wrap_wrap">
        <item name="android:layout_marginStart">1dp</item>
    </style>

Ausrichtung von Elementen

Mit dem Anpassen auf Margin und Textausrichtung hat man den wichtigsten Schritt geschafft, die App entspricht dem, was auch viele Webseiten als RTL-Unterstützung bieten. Eine echte Unterstützung hat man damit aber leider noch nicht. Schauen wir uns ein einfaches Beispiel an. Zwei Texte, die in etwa so aussehen könnten:

verdrehte-welt-tab1

Als Eintrag in einer Liste könnte man sich solche Attribute gut vorstellen. Das Produkt ist links ausgerichtet, der Preis rechts und beide teilen sich den verfügbaren Platz z.B: in einem horizontalen LinearLayout. In Voraussicht für RTL haben wir in Bezug auf die Textausrichtung auch auf start und end bei beiden Texten gesetzt.

Wie würde das beim Wechsel ins Hebräische aussehen? Vermutlich entsteht so etwas:

verdrehte-welt-tab2

Das ist vermutlich nicht das Ergebnis, das wir erwarten. Die beiden Texte machen jedoch genau das, was wir ihnen gesagt haben. Der Titel beginnt am Beginn und der Preis am Ende der Leserichtung, jedoch nur innerhalb ihres verfügbaren Platzes. Vermutlich wollten wir hier jedoch, dass der Nutzer zuerst den Artikel sieht und nicht den Preis, und dies wird mit unserer Leserichtung auch erreicht.

Das Problem: Nicht nur der Text hat eine Ausrichtung, sondern unsere gesamte Oberfläche. Wir haben uns aber bisher keine Gedanken darüber gemacht, wie sich Elemente untereinander verhalten sollen. Wir müssten den beiden Elementen also eigentlich sagen, dass sie den Platz tauschen sollen.

Hätte man hier wie oben angedeutet auf ein Linearlayout gesetzt, wäre es schwierig. Einfacher ist es mit dem Relative Layout. Hier wird jedem Element gesagt, an welchem es sich orientieren soll. So würde man dem Preis sagen, dass er rechts vom Titel steht:

android:layout_toRightOf="@id/title"

Und hier gibt es auch die neuen Attribute, die wir einfach einsetzen:

android:layout_toEndOf="@id/title"

Oder man nutzt:

android:layout_alignParentEnd="true"

anstelle von:

android:layout_alignParentRight="true"

Heißt dies, dass wir mit dem Linearlayout verloren sind? Nein, Android hat uns einen einfachen Mechanismus an die Hand gegeben:Wenn man sich relativ sicher ist, dass man start und end konsequent eingesetzt hat, kann man versuchen, in seinem Manifest

android:supportsRtl="true"

als Attribut der „application“ (ebenfalls verfügbar ab API Level 17) zu definieren.

Nun beginnt die eigentliche Magie. Das System versucht nun, die Elemente der UI zu tauschen. Aus links wird rechts und umgekehrt. So wandert der in der Actionbar zu findende altbewährte „Up“-Pfeil auf die andere Seite und dreht sich selbstverständlich auch um. Womit wir leider auch schon beim nächsten Problem angekommen sind.

Abb. 1: Spracheinstellungen in Hebräisch

Abb. 1: Spracheinstellungen in Hebräisch

Bilder

Ohne es zu wissen, entwerfen Designer Bilder oft in unserer Leserichtung. Nehmen Sie spaßeshalber mal ein paar Grafiken zur Hand und spiegelt Sie sie mit einem Grafikprogramm. Oft fühlt sich die Grafik jetzt falsch an. Genau so geht es jedoch jemandem, der unsere „LTR“-Grafiken zu sehen bekommt. Wenn unsere Oberfläche nun also gespiegelt wird, sollten das auch die Grafiken.

Hier ist für das System jedoch die Grenze erreicht. Es kann nicht selbst entscheiden, was gespiegelt werden kann oder sogar darf. Man denke nur an Grafiken, die Text enthalten. Diese zu spiegeln, ist nicht die beste Idee (und an dieser Stelle ein guter Hinweis an den Designer, Texte nicht in Bilder zu setzen, sondern dies programmatisch von der App erledigen zu lassen).

Woher weiß das Gerät nun aber, dass es den Navigationspfeil umkehren durfte? Erfreulicherweise ist dies sehr einfach. Man erzeugt ein XML-Drawable, das man anstelle der direkten PNG-Grafik verwendet:

drawable/my_rtl_icon.xml
<?xml version="1.0" encoding="utf-8"?>
<bitmap  
   xmlns:android="http://schemas.android.com/apk/res/android"
   android:src="@drawable/my_icon"
   android:autoMirrored="true"
</bitmap>

Mit dem Attribut autoMirrored teilen wir dem System mit, dass es dieses Bild in RTL-Umgebungen spiegeln kann und soll.

Abb. 2a: Das Bild funktioniert am rechten Rand

Abb. 2a: Das Bild funktioniert am rechten Rand

Abb. 2b: Das Bild wandert in RTLnach links

Abb. 2b: Das Bild wandert in RTLnach links

Abb. 2c: Das Bild mit „automirror“-Attribut

Abb. 2c: Das Bild mit „automirror“-Attribut

Selbst wenn man der Meinung ist, die eigenen Grafiken seien unabhängig von der Leserichtung, sollte man sich die App einmal komplett in RTL ansehen. Eventuell wurden Grafiken am Bildschirmrand ausgerichtet und entsprechend abgeschnitten. Wenn das UI dank des Manifestattributs gespiegelt wird, kann das sehr schnell zu unerwarteten Effekten kommen.

Ein Problem beim automirrored-Attribut: Google ist das Problem mit den Bildern selbst zu spät aufgefallen, sodass diese Möglichkeit erst ab API Level 19 zur Verfügung steht.

Wegen des späteren API-Levels müssen wir obigem XML nun noch hinzufügen, dass wir uns diesem Problem bewusst sind, zumindest wenn wir die Zero Lint Warning Policy anstreben:

   xmlns:tools="http://schemas.android.com/tools"
   tools:ignore="UnusedAttribute">

Eine Alternative zum Spiegeln, die bereits seit API Level 17 funktioniert, ist ein eigener Order für RTL-Grafiken ldrtl. Ähnlich wie seine Kollegen für andere Konfigurationen (Sprachen, Größen, Auflösungen, Orientierungen) kann dies für alle Arten von Ressourcen verwendet werden.

Custom Views

Wie verhält es sich, wenn man nicht auf Standardkomponenten setzt? Hier muss man leider etwas genauer hinschauen und zur Laufzeit je nach Sprachrichtung Unterscheidungen machen. Die kleine Methode in Listing 3 ermittelt uns dies. Für ältere Versionen ohne RTL-Unterstützung geben wir einfach false zurück.

Listing 3

@TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR1)
public boolean isRTL() {
   if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN_MR1) {
       return false;
    }
    return getResources.getConfiguration().getLayoutDirection() == View.LAYOUT_DIRECTION_RTL;
    }

In seiner CustomView würde man diese wie folgt verwenden:

@SuppressWarnings("RtlHardcoded")
public void setTextGravity(TextView view) {
     if (isRTL()) {
          view.setGravity(Gravity.RIGHT);
      } else {
          view.setGravity(Gravity.LEFT);
      }
 }

Wenn man nur auf Standardelemente setzt, muss man sich jedoch eventuell trotzdem hiermit auseinander setzen. So hat der beliebte ViewPager aus der Supportbibliothek leider keine Unterstützung für RTL. Jedoch sollte eine Gallery oder ein Wizard, die hierauf oft basieren, das umgekehrte Wischen unterstützen.

Im Prinzip sind die Schritte sehr einfach: Man invertiere die Elemente bzw. den Zugriff darauf und setze das „erste“ Element nicht auf 0, sondern auf das Ende. Dies lässt sich durch einfaches Überladen der entsprechenden Funktionen bewerkstelligen:

    @Override
    public void setCurrentItem(int item) {
        super.setCurrentItem(verifyPosition(item));
    }

    @Override
    public void setCurrentItem(int item, boolean smoothScroll) {
        super.setCurrentItem(verifyPosition(item), smoothScroll);
    }

    private int verifyPosition(int position) {
        if (getAdapter() == null) {
            return position;
        }
        return isRTL() ? getAdapter().getCount() - 1 - position : position;
    }

Das wäre es schon fast, wenn ViewPager nicht intern für den OnPageChangeListener seine eigene Variante setCurrentItemInternal verwendet, an die man leider nicht so einfach herankommt. Aber auch das lässt sich entsprechend überschreiben (Listing 4). Komplizierter sollte es auch bei anderen Komponenten kaum werden.

Listing 4

   @Override
    public void setOnPageChangeListener(final OnPageChangeListener listener) {
        if (listener == null) {
            super.setOnPageChangeListener(null);
            return;
        }
        // we can not rely on setCurrentItem as viewpager has setCurrentItemInternal for pager listener
        super.setOnPageChangeListener(new MyOnPageChangeListener(listener));
    }


private class MyOnPageChangeListener implements OnPageChangeListener {
        private final OnPageChangeListener listener;

        public MyOnPageChangeListener(OnPageChangeListener listener) {
            this.listener = listener;
        }
....

        @Override
        public void onPageSelected(int position) {
            listener.onPageSelected(verifyPosition(position));
        }
...}

Aufpassen sollte man wie beim ViewPager bei allen UI-Komponenten, die sich horizontal bewegen. Man denke zum Beispiel an eine horizontale Scroll-View. Hier ist die Wahrscheinlichkeit groß, dass man für RTL die Reihenfolge umkehren muss – ähnlich wie es oben erfolgte. Alternativ kann man dies auch über den Adapter lösen; man vergesse jedoch nicht, die initiale Position ans Ende der Liste zu setzen.

Fazit

Mitnehmen aus diesem Artikel sollte man für sich Folgendes: Egal, ob man linksläufige Sprachen von Beginn an unterstützen möchte oder nicht, solange das API Level mindestens 17 beträgt, sollte man von nun an einfach immer auf start und end statt left und right in den Layoutdateien setzen. Damit wäre eine gute Grundlage geschaffen.

Durch die unterschiedliche Unterstützung in den Android-Versionen wird die RTL-Unterstützung in der App leider manchmal etwas zu Stückwert. Je nach API-Level ist die Unterstützung besser oder schlechter, und damit verändert sich auch das Erlebnis für den Anwender. In der schnelllebigen Mobile-Welt wird sich dieses Problem aber bald selbst beheben, sobald die alten Geräte aus dem Markt verschwinden.

Wie so vieles funktioniert eine gute RTL-Unterstützung am besten, wenn jeder im Team darauf achtet und sich der Fallstricke bewusst ist: Im nächsten Gespräch mit dem Designer kann man die Problematiken darlegen, damit auch er sich früh Gedanken machen kann. Texte in Bildern sollte man in jedem Fall vermeiden. Sich die eigene App einmal in Hebräisch anzuschauen, und sei es nur im Layouteditor, kann in jedem Falle nicht schaden und macht vielleicht Lust auf mehr.

Aufmacherbild: Close-up shot of brand new Google Nexus 5 via Shutterstock / Urheberrecht: Bloomua

Geschrieben von
Danny Preußler

Der Autor entwickelt seit vielen Jahren Software für Smartphones; der Weg zu Android begann schon bei Java-ME- und BlackBerry-Projekten. Er arbeitet derzeit für Groupon in Berlin.

Kommentare

Hinterlasse einen Kommentar

avatar
4000
  Subscribe  
Benachrichtige mich zu: