Benutzerdefinierte Ansichtskomponenten erstellen

Funktion „Schreiben“ ausprobieren
Jetpack Compose ist das empfohlene UI-Toolkit für Android. Hier erfahren Sie, wie Sie in der Funktion „Compose“ mit Layouts arbeiten.

Android bietet ein komplexes und leistungsstarkes komponentenbasiertes Modell zum Erstellen Ihrer UI, das auf den grundlegenden Layoutklassen View und ViewGroup basiert. Die Plattform enthält eine Vielzahl vordefinierter View- und ViewGroup-Unterklassen (Widgets bzw. Layouts genannt), mit denen Sie Ihre UI erstellen können.

Eine unvollständige Liste der verfügbaren Widgets umfasst Button, TextView, EditText, ListView, CheckBox, RadioButton, Gallery, Spinner und die speziellen Widgets AutoCompleteTextView, ImageSwitcher und TextSwitcher.

Zu den verfügbaren Layouts gehören LinearLayout, FrameLayout, RelativeLayout und weitere. Weitere Beispiele finden Sie unter Gängige Layouts.

Wenn keines der vordefinierten Widgets oder Layouts Ihren Anforderungen entspricht, können Sie Ihre eigene abgeleitete Klasse View erstellen. Wenn Sie nur kleine Anpassungen an einem vorhandenen Widget oder Layout vornehmen müssen, können Sie eine Unterklasse für das Widget oder Layout erstellen und die zugehörigen Methoden überschreiben.

Wenn Sie eigene abgeleitete View-Klassen erstellen, können Sie die Darstellung und Funktion eines Bildschirmelements präzise steuern. Im Folgenden finden Sie einige Beispiele für die Möglichkeiten, die Sie mit benutzerdefinierten Ansichten haben:

  • Sie können einen vollständig benutzerdefiniert gerenderten View-Typ erstellen, zum Beispiel einen in 2D-Grafik gerenderten Lautstärkeregler, der einem analogen elektronischen Steuerelement ähnelt.
  • Sie können eine Gruppe von View-Komponenten zu einer neuen einzelnen Komponente kombinieren, z. B. zu einem Kombinationsfeld (eine Kombination aus Pop-up-Liste und Textfeld mit kostenlosem Eingabefeld), einem Steuerelement für die Auswahl aus zwei Bereichen (linker und rechter Bereich mit jeweils einer Liste, in der Sie neu zuweisen können, welches Element sich in welcher Liste befindet) usw.
  • Sie können die Art und Weise überschreiben, wie eine EditText-Komponente auf dem Bildschirm gerendert wird. Die Beispiel-App NotePad nutzt dies, um eine gezeichnete Notizblock-Seite zu erstellen.
  • Sie können andere Ereignisse wie das Drücken von Tasten erfassen und auf eine benutzerdefinierte Weise verarbeiten, z. B. für ein Spiel.

In den folgenden Abschnitten wird erläutert, wie Sie benutzerdefinierte Ansichten erstellen und in Ihrer Anwendung verwenden. Ausführliche Referenzinformationen finden Sie in der Klasse View.

Der grundlegende Ansatz

Im Folgenden finden Sie eine allgemeine Übersicht darüber, was Sie wissen müssen, um Ihre eigenen View-Komponenten zu erstellen:

  1. Erweitern Sie eine vorhandene View-Klasse oder abgeleitete Klasse mit Ihrer eigenen Klasse.
  2. Überschreiben Sie einige Methoden aus der Basisklasse. Die zu überschreibenden Basisklassenmethoden beginnen mit on, z. B. onDraw(), onMeasure() und onKeyDown(). Dies ähnelt den on-Ereignissen in Activity oder ListActivity, die Sie für Lebenszyklus- und andere Funktions-Hooks überschreiben.
  3. Verwenden Sie die neue Erweiterungsklasse. Anschließend können Sie die neue Erweiterungsklasse anstelle der Ansicht verwenden, auf der sie basiert.

Vollständig angepasste Komponenten

Sie können vollständig benutzerdefinierte grafische Komponenten erstellen, die beliebig dargestellt werden. Vielleicht brauchen Sie ein grafisches VU-Messtool, das wie eine alte analoge Anzeige aussieht, oder eine Textansicht zum Mitsingen, in der sich eine hüpfende Kugel über die Wörter bewegt, während Sie mit einer Karaokemaschine singen. Möglicherweise benötigen Sie etwas, das die integrierten Komponenten nicht können, egal wie Sie sie kombinieren.

Glücklicherweise können Sie Komponenten erstellen, die sich ganz nach Ihren Vorstellungen und Funktionen anpassen – allerdings nur durch Vorstellungskraft, Bildschirmgröße und verfügbare Rechenleistung. Beachten Sie dabei, dass Ihre Anwendung unter Umständen mit deutlich weniger Energie als Ihre Desktop-Workstation ausgeführt werden muss.

Berücksichtigen Sie beim Erstellen einer vollständig angepassten Komponente Folgendes:

  • Die allgemeinste Ansicht, die erweitert werden kann, ist View. Daher erweitern Sie normalerweise diese Ansicht, um Ihre neue Superkomponente zu erstellen.
  • Sie können einen Konstruktor angeben, der Attribute und Parameter aus der XML-Datei übernehmen kann. Außerdem können Sie eigene Attribute und Parameter wie die Farbe und den Bereich des VU-Messtools oder die Breite und Dämpfung der Nadel verwenden.
  • Sie möchten wahrscheinlich Ihre eigenen Ereignis-Listener, Property-Zugriffsfunktionen und -Modifikatoren erstellen sowie ausgefeiltere Verhaltensweisen in Ihrer Komponentenklasse.
  • Es ist sehr wahrscheinlich, dass Sie onMeasure() überschreiben und wahrscheinlich auch onDraw() überschreiben müssen, wenn die Komponente etwas anzeigen soll. Beide haben das Standardverhalten, aber der Standardwert von onDraw() hat keine Auswirkungen und der Standardwert von onMeasure() legt immer eine Größe von 100 × 100 fest, was Sie wahrscheinlich nicht wünschen.
  • Sie können bei Bedarf auch andere on-Methoden überschreiben.

onDraw() und onMeasure() erweitern

Die Methode onDraw() liefert eine Canvas, auf die Sie alles nach Belieben implementieren können: 2D-Grafiken, andere Standard- oder benutzerdefinierte Komponenten, formatierten Text oder alles andere, was Ihnen einfällt.

onMeasure() ist ein wenig stärker eingebunden. onMeasure() ist ein wichtiger Bestandteil des Renderingvertrags zwischen Ihrer Komponente und ihrem Container. onMeasure() muss überschrieben werden, um die Messungen der enthaltenen Teile effizient und genau zu erfassen. Dies wird durch die Grenzwertanforderungen des übergeordneten Elements, die an die Methode onMeasure() übergeben werden, und durch die Anforderung, nach der Berechnung die Methode setMeasuredDimension() mit der gemessenen Breite und Höhe aufzurufen, etwas komplexer. Wenn Sie diese Methode nicht über eine überschriebene onMeasure()-Methode aufrufen, wird zum Messzeitpunkt eine Ausnahme ausgelöst.

Auf übergeordneter Ebene sieht die Implementierung von onMeasure() in etwa so aus:

  • Die überschriebene onMeasure()-Methode wird mit Spezifikationen für Breite und Höhe aufgerufen, die als Anforderungen für die Einschränkungen in Bezug auf die von Ihnen erstellten Breiten- und Höhenmessungen gelten. Die Parameter widthMeasureSpec und heightMeasureSpec sind Ganzzahlcodes, die Dimensionen darstellen. Eine vollständige Referenz zu den Arten von Einschränkungen, für die diese Spezifikationen erforderlich sind, finden Sie in der Referenzdokumentation unter View.onMeasure(int, int). Außerdem wird dort der gesamte Messvorgang erläutert.
  • Mit der Methode onMeasure() der Komponente werden die Breite und Höhe der Messung berechnet, die zum Rendern der Komponente erforderlich sind. Es muss versucht werden, die übergebenen Spezifikationen einzuhalten, auch wenn sie diese überschreiten können. In diesem Fall kann das übergeordnete Element auswählen, was zu tun ist, z. B. Zuschneiden, Scrollen, eine Ausnahme auslösen oder onMeasure() bitten, es noch einmal zu versuchen – eventuell mit anderen Messspezifikationen.
  • Wenn Breite und Höhe berechnet wurden, rufen Sie die Methode setMeasuredDimension(int width, int height) mit den berechneten Maßen auf. Andernfalls wird eine Ausnahme ausgelöst.

Nachfolgend finden Sie eine Zusammenfassung weiterer Standardmethoden, die das Framework für Ansichten auffordert:

Kategorie Methoden Beschreibung
Erstellung Konstruktoren Es gibt ein Formular des Konstruktors, der aufgerufen wird, wenn die Ansicht aus Code erstellt wird, und ein Formular, das aufgerufen wird, wenn die Ansicht aus einer Layoutdatei aufgebläht wird. Im zweiten Formular werden die in der Layoutdatei definierten Attribute geparst und angewendet.
onFinishInflate() Wird aufgerufen, nachdem eine Ansicht und alle untergeordneten Elemente aus XML aufgeblasen wurden.
Layout onMeasure(int, int) Wird aufgerufen, um die Größenanforderungen für diese Ansicht und alle untergeordneten Elemente zu ermitteln.
onLayout(boolean, int, int, int, int) Wird aufgerufen, wenn in dieser Ansicht allen untergeordneten Elementen eine Größe und Position zugewiesen werden muss.
onSizeChanged(int, int, int, int) Wird aufgerufen, wenn die Größe dieser Ansicht geändert wird.
Zeichnung onDraw(Canvas) Wird aufgerufen, wenn die Ansicht ihren Inhalt rendern muss.
Ereignisverarbeitung onKeyDown(int, KeyEvent) Wird aufgerufen, wenn ein Key-down-Ereignis eintritt.
onKeyUp(int, KeyEvent) Wird aufgerufen, wenn ein Key-up-Ereignis eintritt.
onTrackballEvent(MotionEvent) Wird aufgerufen, wenn eine Trackball-Bewegung auftritt.
onTouchEvent(MotionEvent) Wird beim Auftreten einer Touchscreen-Bewegung aufgerufen.
Fokus onFocusChanged(boolean, int, Rect) Wird aufgerufen, wenn die Ansicht gewinnt oder den Fokus verliert
onWindowFocusChanged(boolean) Wird aufgerufen, wenn das Fenster mit der Ansicht gewinnt oder den Fokus verliert.
Wird angehängt onAttachedToWindow() Wird aufgerufen, wenn die Ansicht an ein Fenster angehängt ist.
onDetachedFromWindow() Wird aufgerufen, wenn die Ansicht vom Fenster getrennt ist
onWindowVisibilityChanged(int) Wird aufgerufen, wenn sich die Sichtbarkeit des Fensters mit der Ansicht ändert.

Komplexe Steuerelemente

Wenn Sie keine vollständig benutzerdefinierte Komponente erstellen möchten, sondern eine wiederverwendbare Komponente aus einer Gruppe vorhandener Steuerelemente zusammenstellen möchten, ist es wahrscheinlich am besten, eine zusammengesetzte Komponente (oder eine kombinierte Steuerung) zu erstellen. Zusammenfassend lässt sich sagen, dass dadurch eine Reihe von mehr atomaren Steuerelementen oder Ansichten zu einer logischen Gruppe von Elementen zusammengefasst wird, die als ein Element behandelt werden können. Ein Kombinationsfeld kann beispielsweise eine Kombination aus einem einzeiligen EditText-Feld und einer angrenzenden Schaltfläche mit einer angehängten Pop-up-Liste sein. Wenn der Nutzer auf die Schaltfläche tippt und etwas aus der Liste auswählt, wird das Feld EditText ausgefüllt. Er kann aber auch etwas direkt in EditText eingeben.

In Android stehen dazu zwei weitere Ansichten zur Verfügung: Spinner und AutoCompleteTextView. Trotzdem ist dieses Konzept für ein Kombinationsfeld ein gutes Beispiel.

So erstellen Sie eine Verbundkomponente:

  • Verwenden Sie wie bei einem Activity entweder den deklarativen (XML-basierten) Ansatz, um die enthaltenen Komponenten zu erstellen, oder verschachteln Sie sie programmatisch aus Ihrem Code. Der übliche Ausgangspunkt ist eine Layout. Erstellen Sie daher eine Klasse, die eine Layout erweitert. Bei einem Kombinationsfeld können Sie LinearLayout mit horizontaler Ausrichtung verwenden. Sie können andere Layouts verschachteln, sodass die zusammengesetzte Komponente beliebig komplex und strukturiert sein kann.
  • Nehmen Sie im Konstruktor der neuen Klasse alle Parameter, die die übergeordnete Klasse erwartet, und übergeben Sie sie zuerst an den Konstruktor der übergeordneten Klasse. Anschließend können Sie die anderen Ansichten einrichten, die Sie in Ihrer neuen Komponente verwenden möchten. Hier erstellen Sie das Feld EditText und die Pop-up-Liste. Sie können eigene Attribute und Parameter in den XML-Code einführen, den Ihr Konstruktor abrufen und verwenden kann.
  • Optional können Sie Listener für Ereignisse erstellen, die Ihre enthaltenen Ansichten möglicherweise generieren. Ein Beispiel hierfür ist eine Listener-Methode für den Listener für Listenelemente, mit dem der Inhalt von EditText aktualisiert wird, wenn eine Listenauswahl getroffen wird.
  • Optional können Sie eigene Attribute mit Zugriffsmethoden und Modifikatoren erstellen. Beispielsweise kann der Wert EditText anfangs in der Komponente festgelegt werden und der Inhalt wird bei Bedarf abgefragt.
  • Überschreiben Sie optional onDraw() und onMeasure(). Dies ist normalerweise nicht erforderlich, wenn ein Layout erweitert wird, da das Layout ein Standardverhalten hat, das wahrscheinlich gut funktioniert.
  • Sie können andere on-Methoden wie onKeyDown() überschreiben, um z. B. bestimmte Standardwerte aus der Pop-up-Liste eines Kombinationsfelds auszuwählen, wenn auf eine bestimmte Taste getippt wird.

Die Verwendung eines Layout als Grundlage für ein benutzerdefiniertes Steuerelement hat folgende Vorteile:

  • Du kannst das Layout wie bei einem Aktivitätsbildschirm mithilfe der deklarativen XML-Dateien angeben oder Ansichten programmatisch erstellen und sie aus deinem Code in das Layout verschachteln.
  • Die Methoden onDraw() und onMeasure() sowie die meisten anderen Methoden on haben ein geeignetes Verhalten, sodass Sie diese nicht überschreiben müssen.
  • Beliebig komplexe zusammengesetzte Ansichten lassen sich schnell erstellen und wie eine einzelne Komponente wiederverwenden.

Vorhandenen Ansichtstyp ändern

Wenn es eine Komponente gibt, die Ihrem Wunsch ähnelt, können Sie diese Komponente erweitern und das Verhalten überschreiben, das Sie ändern möchten. Sie können alles tun, was Sie mit einer vollständig angepassten Komponente tun, aber wenn Sie mit einer stärker spezialisierten Klasse in der View-Hierarchie beginnen, können Sie das Verhalten, das Ihren Vorstellungen entspricht, kostenlos erhalten.

Die Beispiel-App NotePad demonstriert beispielsweise viele Aspekte der Android-Plattform. Dazu gehört unter anderem die Erweiterung einer EditText-Ansicht, um einen markierten Notizblock zu erstellen. Dies ist kein perfektes Beispiel und die dafür erforderlichen APIs können sich ändern, aber es verdeutlicht die Prinzipien.

Falls noch nicht geschehen, importiere das NotePad-Beispiel in Android Studio oder rufe die Quelle über den angegebenen Link auf. Lesen Sie insbesondere die Definition von LinedEditText in der Datei NoteEditor.java.

Beachten Sie Folgendes in dieser Datei:

  1. Die Definition

    Die Klasse wird mit der folgenden Zeile definiert:
    public static class LinedEditText extends EditText

    LinedEditText ist als innere Klasse innerhalb der NoteEditor-Aktivität definiert. Es ist jedoch öffentlich, sodass von außerhalb der NoteEditor-Klasse als NoteEditor.LinedEditText darauf zugegriffen werden kann.

    Außerdem ist LinedEditText static. Das bedeutet, dass nicht die sogenannten "synthetischen Methoden" generiert werden, die den Zugriff auf Daten aus der übergeordneten Klasse ermöglichen. Das bedeutet, dass sie sich als separate Klasse verhält und nicht als eng mit NoteEditor verbunden ist. So lassen sich innere Klassen einfacher erstellen, wenn sie keinen Zugriff auf den Zustand von der äußeren Klasse benötigen. Dadurch bleibt die generierte Klasse klein und kann problemlos von anderen Klassen verwendet werden.

    LinedEditText erweitert EditText. In diesem Fall muss die Ansicht angepasst werden. Wenn Sie fertig sind, kann die neue Klasse die normale EditText-Ansicht ersetzen.

  2. Klasseninitialisierung

    Wie immer wird der Supers zuerst genannt. Dies ist kein Standardkonstruktor, sondern ein parametrierter Konstruktor. Der EditText wird mit diesen Parametern erstellt, wenn er aus einer XML-Layoutdatei aufgeblasen wird. Daher muss der Konstruktor sie ebenfalls an den Superklassen-Konstruktor übergeben.

  3. Überschriebene Methoden

    In diesem Beispiel wird nur die Methode onDraw() überschrieben. Möglicherweise müssen Sie jedoch andere überschreiben, wenn Sie Ihre eigenen benutzerdefinierten Komponenten erstellen.

    In diesem Beispiel können Sie durch Überschreiben der Methode onDraw() die blauen Linien auf dem Canvas der EditText-Ansicht zeichnen. Der Canvas wird an die überschriebene onDraw()-Methode übergeben. Die Methode super.onDraw() wird aufgerufen, bevor die Methode endet. Die übergeordnete Klassenmethode muss aufgerufen werden. Rufen Sie es in diesem Fall am Ende auf, nachdem Sie die Linien gezeichnet haben, die enthalten sein sollen.

  4. Benutzerdefinierte Komponente

    Sie haben jetzt Ihre benutzerdefinierte Komponente, aber wie können Sie sie verwenden? Im NotePad-Beispiel wird die benutzerdefinierte Komponente direkt aus dem deklarativen Layout verwendet. Sehen Sie sich daher note_editor.xml im Ordner res/layout an:

    <view xmlns:android="http://schemas.android.com/apk/res/android"
        class="com.example.android.notepad.NoteEditor$LinedEditText"
        android:id="@+id/note"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:background="@android:color/transparent"
        android:padding="5dp"
        android:scrollbars="vertical"
        android:fadingEdge="vertical"
        android:gravity="top"
        android:textSize="22sp"
        android:capitalize="sentences"
    />
    

    Die benutzerdefinierte Komponente wird als generische Ansicht in der XML-Datei erstellt und die Klasse wird mithilfe des vollständigen Pakets angegeben. Auf die innere Klasse wird mit der NoteEditor$LinedEditText-Notation verwiesen. Diese ist eine Standardmethode, um in der Programmiersprache Java auf innere Klassen zu verweisen.

    Wenn die Komponente der benutzerdefinierten Ansicht nicht als innere Klasse definiert ist, können Sie die Ansichtskomponente mit dem Namen des XML-Elements deklarieren und das Attribut class ausschließen. Beispiel:

    <com.example.android.notepad.LinedEditText
      id="@+id/note"
      ... />
    

    Die Klasse LinedEditText ist jetzt eine separate Klassendatei. Wenn die Klasse in der Klasse NoteEditor verschachtelt ist, funktioniert diese Technik nicht.

    Die anderen Attribute und Parameter in der Definition sind diejenigen, die an den Konstruktor der benutzerdefinierten Komponente übergeben und dann an den EditText-Konstruktor übergeben werden. Es handelt sich also um dieselben Parameter, die Sie für eine EditText-Ansicht verwenden. Sie können auch eigene Parameter hinzufügen.

Das Erstellen benutzerdefinierter Komponenten ist nur so kompliziert wie nötig.

Eine komplexere Komponente kann noch mehr on-Methoden überschreiben und eigene Hilfsmethoden einführen, um ihre Eigenschaften und ihr Verhalten erheblich anzupassen. Die einzige Grenze ist Ihre Vorstellungskraft und was die Komponente tun soll.