Stand: 16.06.2018

In diesem Modul beschäftigen wir uns mit dem Default-Layout ConstraintLayout und schauen uns außerdem das Einbinden von Bildern mit Hilfe des ImageView genauer an.

8.1 ConstraintLayout

ConstraintLayout steht ab API-Level 9 (Android 2.3) zur Verfügung.

Heutzutage muss man natürlich ein Layout so anlegen, dass es sowohl auf einem kleinen Smartphone als auch auf einem großen Tablet funktioniert. Wenn das der Fall ist, spricht man von Responsive Design.

Das relativ neue ConstraintLayout soll dazu möglichst alle Möglichkeiten bieten. Ähnlich wie das RelativeLayout arbeitet es mit Constraints, um das Layout zu definieren. Man sollte keine verschachtelten Layouts mehr benötigen, da die Verschachtelung Performance-Einbußen zur Folge haben kann.

Sie können die Position einer Komponente in Bezug auf (a) der Elternkomponente oder (b) einer zweiten Komponente definieren.

Warum ordnen wir Elemente links- oder rechtsbündig an? Das hat oft auch mit der Leserichtung der Beschriftung zu tun, in den meisten Sprachen ist das von links nach rechts. Wenn dem aber so ist, dann müsste sich ein Layout zumindest teilweise ändern, wenn wir die App auf eine Sprache umstellen, die von links nach rechts geschrieben wird (z.B. Arabisch, Hebräisch oder Persisch).

Deswegen unterscheidet Android seit API-Level 17 (Android 4.2) zwischen:

Wenn wir uns im weiteren mit Constraints beschäftigen, können Sie entscheiden, ob Sie zwei Komponenten auf der linken Seite aneinander ausrichten oder auf der Start-Seite. Oder ob Sie zwei Komponenten auf der rechten Seite aneinander ausrichten oder auf der Ende-Seite.

Hierbei bedeutet:

Auch das können Sie in Android Studio testen: Verwenden Sie ein "start" oder "end" Constraint und ändern Sie die Sprache auf eine RTL-Sprache wie Arabisch.

8.1.2 Position in Bezug auf Elternkomponente

Eine Komponente kann in Bezug auf ihre Elternkomponente gesetzt werden. Die Elternkomponente ist meistens die komplette Screen, also geht es hier darum eine Komponente in Bezug auf den Bildschirmrand zu positionieren. Dazu stehen Ihnen entsprechende Constraints als (XML-)Eigenschaften Ihrer Komponente (z.B. eines Buttons) zur Verfügung.

Um auszudrücken, dass ein Button am oberen Rand des Elterncontainers positioniert sein, soll verwenden Sie:

<Button
    ...
    app:layout_constraintTop_toTopOf="parent" />

Dieses Constraint hat zwei Teile. Der erste Teil "constraintTop" sagt aus, auf welchen Teil der betroffenen Komponente (hier ein Button) sich das Constraint bezieht (hier der obere Rand). Der zweite Teil "toTopOf" sagt aus, auf welchen Teil der in-Bezug-gesetzten Komponente (hier die Elternkomponente) sich das Constraint bezieht. Ingesamt heißt das: "Setze die obere Kante des Button an die obere Kante der Elternkomponente". Hier eine Übersicht der Constraints:

ConstraintLayout: Bezug zur Elterncomponente

Wollen Sie den Button rechts-oben platzieren, ergänzen Sie einen zweiten Constraint, der sich ebenfalls auf den Elterncontainer bezieht:

<Button
    ...
    app:layout_constraintTop_toTopOf="parent"
    app:layout_constraintRight_toRightOf="parent" />

Das neue Constraint sagt aus "Setze die rechte Kante des Button an die rechte Kante der Elternkomponente".

ConstraintLayout: rechts oben

Hinweis: Wenn sich Ihr Layout bei RTL-Sprachen anpassen soll (s.o.), verwenden Sie "Start" statt "Left" und "End" statt "Right".

Zentrieren und Bias

Wollen Sie eine Komponente zentrieren, so setzen Sie zwei im Grund "unmögliche" Constraints (z.B. links und rechts) gleichzeitig an. Die Komponente müsste sich eigentlich strecken, damit Sie die Constraints erfüllt. Stattdessen kann man sich zwei Sprungfedern an jeder Seite vorstellen, die die Komponente gleichermaßen in ihre Richtung ziehen.

<Button
    ...
    app:layout_constraintStart_toStartOf="parent"
    app:layout_constraintEnd_toEndOf="parent" />

Hinweis: Es spielt keine Rolle, ob Sie left/right oder start/end nehmen, Sie dürfen die beiden bloß nicht mischen.

Wenn Sie das Element nicht genau in der Mitte haben wollen, sondern z.B. auf 25% von links aus gesehen, dann geben Sie einen horizontal bias an. HJier kann man sich vorstellen, dass die eine Feder stärker ist.

<Button
    ...
    app:layout_constraintStart_toStartOf="parent"
    app:layout_constraintEnd_toEndOf="parent"
    app:layout_constraintHorizontal_bias="0.25" />

In der Vertikalen sieht das so aus (ebenfalls mit einem Bias, 20% vom unteren Rand entfernt).

<Button
    ...
    app:layout_constraintTop_toTopOf="parent"
    app:layout_constraintBottom_toBottomOf="parent"
    app:layout_constraintVertical_bias="0.8" />

ConstraintLayout: Zentrieren und Bias

Hier eine Übersicht:

ConstraintLayout: Zentrieren und Bias in der Übersicht

8.1.3 Position in Bezug auf andere Komponenten

Nehmen wir an, der Button aus dem vorigen Abschnitt hat den ID "buttonA":

<Button
    android:id="@+id/buttonA"
    ...
    app:layout_constraintTop_toTopOf="parent"
    app:layout_constraintRight_toRightOf="parent" />

Dann können wir die Position eines zweiten Button B in Relation zu Button A definieren.

<Button
    android:id="@+id/buttonB"
    ...
    app:layout_constraintTop_toBottomOf="@id/buttonA"/>

Das heißt jetzt "Setze die obere Kante von Button B and die untere Kante von Button A". Wenn Sie einen Abstand von 20dp setzen wollen, machen Sie das über den Margin:

<Button
    android:id="@+id/buttonB"
    ...
    app:layout_constraintTop_toBottomOf="@id/buttonA"
    android:layout_marginTop="20dp" />

Schematisch:

ConstraintLayout: Beziehung zwischen Komponenten

Diese Constraints machen keine Aussage über die Position von Button B in der Horizontalen, also stehen Button A ganz rechts und Button B ganz links.

Sie können jetzt zusätzlich definieren, dass die linke Kante (oder "Startkante") von B auf gleicher Position wie die linke Kante von A sein soll:

<Button
    android:id="@+id/buttonB"
    ...
    app:layout_constraintTop_toBottomOf="@id/buttonA"
    app:layout_constraintStart_toStartOf="@id/buttonA"
    android:layout_marginTop="20dp" />

ConstraintLayout: zweiter Button

Hinweis: Wenn sich Ihr Layout bei RTL-Sprachen anpassen soll (s.o.), verwenden Sie "Start", ansonsten geht natürlich auch "Left".

Jetzt sind die zwei Buttons linksbündig ausgerichtet, auch wenn Sie z.B. in den Landscape-Modus gehen.

Genauso wie Sie mit "marginTop" den Abstand zur oberen Komponente kontrollieren können, können Sie mit marginStart einen Abstand zu eigentlich alignierten linken Kante (Start) definineren.

Hier ein Beispiel mit drei Buttons:

<Button
    android:id="@+id/buttonA"
    ...
    app:layout_constraintTop_toTopOf="parent" />

<Button
    android:id="@+id/buttonB"
    ...
    app:layout_constraintTop_toBottomOf="@id/buttonA"
    app:layout_constraintStart_toStartOf="@id/buttonA"
    android:layout_marginTop="20dp"
    android:layout_marginStart="30dp" />

<Button
    android:id="@+id/buttonC"
    ...
    app:layout_constraintTop_toBottomOf="@id/buttonB"
    app:layout_constraintStart_toStartOf="@id/buttonB"
    android:layout_marginTop="20dp"
    android:layout_marginStart="30dp" />

ConstraintLayout: drei Buttons

8.1.4 Bezug auf die Baseline

Sie können auch die Baseline des Textes von zwei Komponenten alignieren. Wenn wir das Drei-Button-Beispiel von oben fortführen und einen Text hinzufügen:

<TextView
    ...
    android:text="Hallo, Welt!"
    android:textSize="12sp"
    app:layout_constraintBaseline_toBaselineOf="@id/buttonC"
    app:layout_constraintStart_toEndOf="@id/buttonC"
    android:layout_marginStart="20dp"/>

Wir sehen, dass der Text von Button C und das "Hallo, Welt" an der Baseline aligniert sind.

ConstraintLayout: Baseline-Alignierung

8.1.5 Orientierungslinien

Es gibt zwei Möglichkeiten, "unsichtbare" Orientierungslinien herzustellen und daran Komponenten auszurichten.

Guidelines

Es können virtuelle Guidelines definiert werden, anhand derer Komponenten ausgerichtet werden. Sie erstellen eine neue Guideline über die Schaltfläche über der Vorschau (in der Design-Ansicht oder dem Preview):

ConstraintLayout: Guidelines

Sie können dann die gestrichelte Linie (nur sichtbar beim Hovern) hin- und herziehen. In der Text-Ansicht sehen Sie:

<android.support.constraint.Guideline
    android:id="@+id/guideline"
    ...
    android:orientation="vertical"
    app:layout_constraintGuide_begin="50dp" />

In diesem Fall haben Sie mit constraintGuide_begin eine absolute Positionierung (50dp) vom linken Rand der Screen vorliegen. Es gibt insgesamt drei Möglichkeiten der Positionierung:

Außer diesen dreien sollten Sie keine anderen Positions-Constraints mit Guidelines verwenden!

Sie können dann z.B. den ersten der drei Buttons an diese Linie heften:

<Button
    android:id="@+id/buttonA"
    ...
    app:layout_constraintStart_toEndOf="@id/guideline" />

Jetzt sehen Sie beim Ziehen an der Guideline, dass sich alle Buttons und Textfeld mitbewegen.

Natürlich ist auch hier die Empfehlung, keine absoluten Angaben zu verwenden, sondern eine prozentuale Angabe. Dazu setzen Sie den Abstand zu begin und end auf Null und verwenden dann die Prozentangabe, z.B. hier bei 80%:

<android.support.constraint.Guideline
    android:id="@+id/guideline"
    ...
    android:orientation="vertical"
    app:layout_constraintGuide_begin="0dp"
    app:layout_constraintGuide_end="0dp"
    app:layout_constraintGuide_percent="0.8" />

Barriere

Eine Barriere ist eine äußere Grenze einer Sammlung von Komponenten.

Nehmen wir unsere drei Buttons. Sie können von Android berechnen lassen, welches die rechteste Kante dieser drei Komponenten ist. Das geschieht natürlich automatisch.

Sie erstellen eine neue Barriere über die Schaltfläche über der Vorschau (in der Design-Ansicht oder dem Preview):

ConstraintLayout: Guidelines

Im Komponenten-Baum können Sie Komponenten in die Barriere hineinziehen. Sie können das aber auch in der Text-Ansicht tun:

<android.support.constraint.Barrier
    android:id="@+id/barrier"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    app:barrierDirection="right"
    app:constraint_referenced_ids="buttonC,buttonB,buttonA" />

Wir müssen der Barriere mitteilen, welche Komponenten einbezogen werden sollen (referenced_ids). Außerdem müssen wir sagen, ob welche Kante (barrierDirection) wir definieren wollen.

Wir können zu Demozwecken einen Text mit dieser Grenze positionieren. Genauer gesagt, alignieren wir den Textanfang mit der Barriere. In y-Richtung verwenden wir Button B zur Ausrichtung (Baseline).

<TextView
    android:id="@+id/textView2"
    ...
    android:text="Grenze"
    android:textSize="30sp"
    app:layout_constraintBaseline_toBaselineOf="@id/buttonB"
    app:layout_constraintStart_toEndOf="@id/barrier" />

ConstraintLayout: Barriere

Wenn Sie Button C nach links verschieben, sehen Sie sehr schön, dass sich die Grenze neu ausrichtet, denn dann ist Button B die Komponente, die am weitesten rechts steht.

ConstraintLayout: Barriere 2

8.1.6 Verteilen von Komponenten mit Chain

Sie wissen, dass Sie eine Komponente zentrieren können, indem Sie zwei im Grunde "unmögliche" Constraints angeben, z.B. für das horizontale Zentrieren:

app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"

Wie oben erwähnt, kann man sich zwei Federn links und rechts vorstellen, die gleich stark an der Komponente ziehen.

Wenn man mehrere Komponenten entlang der Horizontalen zentrieren möchte, ist unklar, wie man das definieren soll und auf welche Weise die Komponenten genau verteilt werden sollen.

Chain definieren

Deshalb gibt es das Konzept einer Chain (engl. für Kette). Man kann Komponenten zu einer horizontalen oder vertikalen Chain zusammenführen und diese Komponenten werden dann gleichmäßig verteilt.

Dazu muss man einer Reihe von Komponenten gegenseitige Constraints geben, so dass sie z.B. in einer Reihe angeordnet sind. Im Grunde ist das die logische Erweiterung des Zentrierens: Man gibt auch hier den Komponenten "unmögliche" Constraints zu beiden Seiten. Auch hier kann man sich Federn an den jeweiligen Enden der Chain und zwischen den Komponenten vorstellen.

<Button
    android:id="@+id/buttonEins"
    ...
    app:layout_constraintEnd_toStartOf="@+id/buttonZwei"
    app:layout_constraintStart_toStartOf="parent"
    app:layout_constraintTop_toTopOf="parent" />

<Button
    android:id="@+id/buttonZwei"
    ...
    app:layout_constraintStart_toEndOf="@+id/buttonEins"
    app:layout_constraintEnd_toStartOf="@+id/buttonDrei" />

<Button
    android:id="@+id/buttonDrei"
    ...
    app:layout_constraintStart_toEndOf="@+id/buttonZwei"
    app:layout_constraintEnd_toEndOf="parent" />

ConstraintLayout: Chain

In der Design-Ansicht kann man das auch bequem machen, indem man die Komponenten alle markiert und dann mit Rechtsklick Chains > Create Horizontal Chain auswählt.

Bei Chains muss man beachten, dass die zweite Achse (in den Beispielen also die y-Achse) damit nicht festgelegt ist. Dies muss mit weiteren Constraints in jeder Komponente einzeln festgelegt werden, z.B. durch Alignierung am oberen Rand, durch Zentrieren oder durch Alignierung entlang der Baseline der ersten Komponente.

Verteilung festlegen

Jetzt gibt es verschiedene Möglichkeiten, wie die Komponenten verteilt werden können. Sollen die Randkomponenten ganz an den Rand anstoßen oder nicht? Oder sollen die Komponenten doch gestreckt werden?

Chain styles

Der Verteilungsstil lässt sich in der Komponente ganz links (bzw. ganz oben), dem sogenannten Chain Head, festlegen. Dort lässt sich spezifizieren, welcher Stil verfolgt wird:

  1. spread: Komponenten gleichmäßig verteilt
  2. spread inside: Endkomponenten sind am Rand, sonst gleichmäßig verteilt
  3. weighted: jede Komponente hat ein Gewicht
  4. packed: alle Komponenten liegen aneinander

In unserem Beispiel wäre das "buttonEins". Wir schalten um auf "spread inside":

<Button
    android:id="@+id/buttonEins"
    ...
    app:layout_constraintHorizontal_chainStyle="spread_inside"
    ... />

Die Endkomponenten rutschen an den Rand:

ConstraintLayout: Chain, spread inside

8.2 Bilder mit ImageView

8.2.1 Einbinden als Ressource

Sie müssen zunächst Ihre Bilddateien (z.B. JPG oder PNG) in das richtige Ressourcenverzeichnis unter "drawable" kopieren. Der echte Pfad ist etwas versteckt: app/src/main/res/drawable (innerhalb Ihres Projektverzeichnis').

Wie wir schon beim Modul über Ressourcen gesehen haben, wählt Android automatisch passende Ressourcen je nach Kontext aus (z.B. je nach Orientierung: Portrait vs. Landscape). Bei Bildern ist jetzt auch die Pixeldichte interessant. Bei Bildschirmen mit hoher Dichte benötige ich Bilder mit höherer Auflösung, wohingegen ich bei solchen mit geringer Dichte "kleinere" Bilder nehmen kann (die entsprechend performanter sind).

Wie schon bei Portrait/Landscape kann man Android die Pixeldichte berücksichtigen lassen, indem man entsprechende Ressourcen-Parallelverzeichnisse (von drawable) anlegt und die Varianten dort hineinkopiert. Für die Pixeldichte gibt es die Varianten:

Wenn Sie z.B. zusätzlich zum Default-Bild welcome.png noch eine hochauflösende Variante hinzufügen wollen, richten Sie Ihre res-Verzeichnis so ein:

/res/drawable/welcome.png
/res/drawable-hdpi/welcome.png

Android wählt dann automatisch die passende Datei.

8.2.2 Skalierung

Egal, wie Sie die Seiten Ihres ImageView definieren, Sie müssen Android sagen, wie das Bild eingepasst werden soll, da Höhe und Breite des sichtbaren Bereichs i.d.R. nicht der Pixelauflösung des Bilds entsprechen.

Diese Einstellung erfolgt über die Eigenschaft scaleType.

Die folgenden drei Optionen zentrieren das Bild, das aber teils geschnitten (engl. to crop) wird.

ImageView: Scale-Optionen 1

Die folgenden vier Optionen zeigen immer das vollständige Bild, das aber unterschiedlich "aufgehängt" wird (angedeutet mit dem roten Punkt). Vorsicht ist bei der Option "fitXY" geboten, weil dies die einzige Option ist, die das Bild i.d.R. verzerrt, da das Verhältnis Breite:Höhe (engl. aspect ratio) des Bildes ignoriert werden.

ImageView: Scale-Optionen 2

Bei den Skalierungsoptionen fitStart, fitEnd, fitCenter und centerInside bleibt der ImageView i.d.R. größer als das eigentliche Bild. Man kann dann die Größe des ImageView optimieren mit

android:adjustViewBounds="true"

8.2.3 Begrenzen

Wie kann man Bilder gut eingrenzen, wenn doch eine absolute Angabe wie "300dp" problematisch ist? Sinnvoller ist es z.B. eine prozentuale Angabe zu machen: "mein Bild soll 70% der Screenhöhe ausnutzen". In diesem Fall möchten Sie in der Breite die volle Spanne der Screen ausnutzen und schalten daher auf match_parent. In der anderen Richtung möchten Sie das Bild auf 70% der Höhe begrenzen:

ImageView mit Guideline

Hier bietet sich an, eine Guideline oder Barrier zu verwenden.

Begrenzen mit Guideline

Wir erzeugen eine horizontale Guideline mit 70% Abstand nach oben:

<android.support.constraint.Guideline
    android:id="@+id/guideline"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:orientation="horizontal"
    app:layout_constraintGuide_percent="0.7" />

Was muss ich jetzt als Constraint und als layout_height angeben? Als Constraints verankert man das Bild oben mit der Kante der Screen und unten mit der Guideline. Bei layout_height gibt man "0dp" an, was von Android als "richte dich nach den Constraints!" interpretiert wird.

<ImageView
    android:id="@+id/imageView"
    android:layout_width="match_parent"
    android:layout_height="0dp"
    android:contentDescription="Tippen am Notebook"
    android:scaleType="centerCrop"
    app:layout_constraintBottom_toTopOf="@id/guideline"
    app:layout_constraintTop_toTopOf="parent"
    app:srcCompat="@drawable/work_731198" />

Begrenzen mit Barrier

Schauen wir uns das gleiche Beispiel an, aber mit einer zusätzlichen Barrier. Eine Barrier ist sinnvoll, wenn wir eine Reihe von anderen Komponenten "abgrenzen" wollen. In unserem Beispiel haben wir zwei Buttons, die rechts neben dem Bild stehen sollen. Android soll automatisch die Grenze berechnen und das Bild daran ausrichten:

ImageView mit Guideline und Barrier

Zunächste die Buttons:

<Button
    android:id="@+id/button"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_marginRight="20dp"
    android:layout_marginTop="20dp"
    android:text="Button"
    app:layout_constraintBottom_toTopOf="@+id/button2"
    app:layout_constraintEnd_toEndOf="parent"
    app:layout_constraintTop_toTopOf="parent" />

<Button
    android:id="@+id/button2"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_marginRight="20dp"
    android:layout_marginTop="20dp"
    android:text="Button"
    app:layout_constraintEnd_toEndOf="parent"
    app:layout_constraintTop_toBottomOf="@+id/button" />

Dann die Barrier:

<android.support.constraint.Barrier
    android:id="@+id/barrier"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    app:barrierDirection="left"
    app:constraint_referenced_ids="button,button2" />

Im ImageView müssen wir noch Constraints hinzufügen und - wichtig - auch bei der Breite "0dp" angeben.

<ImageView
    ...
    android:layout_width="0dp"
    ...
    app:layout_constraintRight_toLeftOf="@+id/barrier"
    app:layout_constraintLeft_toLeftOf="parent"
    android:layout_marginRight="20dp" />

Mit der Margin sorgen wir für 20dp Abstand zu den Buttons.

8.3 Layout als Ressource

Die können z.B. ein separates Layout für Landscape erstellen. Grafisch geht das so (in der Design-Ansicht des Layout-Files):

Separates Layout für Landscape

Android Studio erstellt dann ein zweites Verzeichnis und dort eine zweite Layout-Datei:

/res/layout/activity_main.xml
/res/layout-land/activity_main.xml

Beachten Sie, dass dies im grafischen Editor um der Lesbarkeit willen etwas anders dargestellt wird.

Ressourcendarstellung in der GUI

Sie können jetzt in der zweiten Datei in Ruhe Ihr Layout für das Landscape-Format optimieren. Bei laufender Applikation entscheidet Android dann, welches Layout es nimmt.

8.4 Übungen

(A) Label und Button

Erstellen Sie folgendes Layout unter Verwendung von ConstraintLayout:

Übung zu ConstraintLayout

(B) Bild und Bewertung

Erstellen Sie folgendes Layout unter Verwendung von ConstraintLayout:

Übung zu ConstraintLayout

(C) Responsive Layout

Erstellen Sie folgendes Layout, indem Sie für den Fall "Landscape" eine zweite Layout-Ressource anlegen:

Übung zu responsive Layout