Benutzerdefinierte Ansichten erstellen

Dieses Codelab ist Teil des Kurses „Advanced Android in Kotlin“. Sie können den größten Nutzen aus diesem Kurs ziehen, wenn Sie die Codelabs der Reihe nach durcharbeiten. Das ist jedoch nicht zwingend erforderlich. Alle Codelabs des Kurses sind auf der Landingpage für Codelabs zu „Android für Fortgeschrittene mit Kotlin“ aufgeführt.

Einführung

Android bietet eine Vielzahl von View-Unterklassen, z. B. Button, TextView, EditText, ImageView, CheckBox oder RadioButton. Mit diesen Unterklassen können Sie eine Benutzeroberfläche erstellen, die Nutzerinteraktionen ermöglicht und Informationen in Ihrer App anzeigt. Wenn keine der View-Unterklassen Ihren Anforderungen entspricht, können Sie eine View-Unterklasse erstellen, die als benutzerdefinierte Ansicht bezeichnet wird.

Wenn Sie eine benutzerdefinierte Ansicht erstellen möchten, können Sie entweder eine vorhandene View-Unterklasse (z. B. Button oder EditText) erweitern oder eine eigene Unterklasse von View erstellen. Wenn Sie View direkt erweitern, können Sie ein interaktives UI-Element beliebiger Größe und Form erstellen, indem Sie die Methode onDraw() für View überschreiben, um es zu zeichnen.

Nachdem Sie eine benutzerdefinierte Ansicht erstellt haben, können Sie sie Ihren Aktivitätslayouts auf dieselbe Weise hinzufügen wie ein TextView- oder Button-Element.

In dieser Lektion erfahren Sie, wie Sie eine benutzerdefinierte Ansicht von Grund auf neu erstellen, indem Sie View erweitern.

Was Sie bereits wissen sollten

  • So erstellen Sie eine App mit einer Aktivität und führen sie mit Android Studio aus.

Lerninhalte

  • View erweitern, um eine benutzerdefinierte Ansicht zu erstellen
  • So zeichnen Sie eine benutzerdefinierte Ansicht, die kreisförmig ist.
  • So verwenden Sie Listener, um Nutzerinteraktionen mit der benutzerdefinierten Ansicht zu verarbeiten.
  • Benutzerdefinierte Ansicht in einem Layout verwenden

Aufgaben

  • Erweitern Sie View, um eine benutzerdefinierte Ansicht zu erstellen.
  • Initialisieren Sie die benutzerdefinierte Ansicht mit Zeichen- und Malwerten.
  • Überschreiben Sie onDraw(), um die Ansicht zu zeichnen.
  • Verwenden Sie Listener, um das Verhalten der benutzerdefinierten Ansicht zu definieren.
  • Fügen Sie die benutzerdefinierte Ansicht einem Layout hinzu.

Die App CustomFanController zeigt, wie eine benutzerdefinierte Ansicht-Unterklasse durch Erweitern der Klasse View erstellt wird. Die neue Unterklasse heißt DialView.

In der App wird ein kreisförmiges UI-Element angezeigt, das einer physischen Lüftersteuerung ähnelt, mit Einstellungen für „Aus“ (0), „Niedrig“ (1), „Mittel“ (2) und „Hoch“ (3). Wenn der Nutzer auf die Ansicht tippt, bewegt sich die Auswahlmarkierung zur nächsten Position: 0–1–2–3 und zurück zu 0. Wenn die Auswahl 1 oder höher ist, ändert sich die Hintergrundfarbe des kreisförmigen Teils der Ansicht von Grau zu Grün (was darauf hinweist, dass die Lüfterleistung aktiviert ist).

Ansichten sind die grundlegenden Bausteine der Benutzeroberfläche einer App. Die Klasse View bietet viele Unterklassen, die als UI-Widgets bezeichnet werden und viele Anforderungen der Benutzeroberfläche einer typischen Android-App abdecken.

UI-Bausteine wie Button und TextView sind abgeleitete Klassen, die die Klasse View erweitern. Um Zeit und Entwicklungsaufwand zu sparen, können Sie eine dieser View-Unterklassen erweitern. Die benutzerdefinierte Ansicht erbt das Aussehen und Verhalten des übergeordneten Elements. Sie können das Verhalten oder das Aussehen überschreiben, das Sie ändern möchten. Wenn Sie beispielsweise EditText erweitern, um eine benutzerdefinierte Ansicht zu erstellen, verhält sich die Ansicht wie eine EditText-Ansicht, kann aber auch so angepasst werden, dass beispielsweise eine X-Schaltfläche angezeigt wird, mit der Text aus dem Texteingabefeld gelöscht wird.

Sie können jede View-Unterklasse wie EditText erweitern, um eine benutzerdefinierte Ansicht zu erhalten. Wählen Sie diejenige aus, die Ihren Anforderungen am nächsten kommt. Sie können die benutzerdefinierte Ansicht dann wie jede andere View-Unterklasse in einem oder mehreren Layouts als XML-Element mit Attributen verwenden.

Wenn Sie eine eigene benutzerdefinierte Ansicht von Grund auf erstellen möchten, erweitern Sie die Klasse View. In Ihrem Code werden View-Methoden überschrieben, um das Erscheinungsbild und die Funktionen der Ansicht zu definieren. Der Schlüssel zum Erstellen einer eigenen benutzerdefinierten Ansicht besteht darin, dass Sie dafür verantwortlich sind, das gesamte UI-Element in beliebiger Größe und Form auf dem Bildschirm zu zeichnen. Wenn Sie eine vorhandene Ansicht wie Button unterteilen, übernimmt diese Klasse das Zeichnen für Sie. (Mehr zum Zeichnen erfahren Sie später in diesem Codelab.)

So erstellen Sie eine benutzerdefinierte Ansicht:

  • Erstellen Sie eine benutzerdefinierte Ansichtsklasse, die View oder eine View-Unterklasse (z. B. Button oder EditText) erweitert.
  • Wenn Sie eine vorhandene View-Unterklasse erweitern, überschreiben Sie nur das Verhalten oder die Aspekte der Darstellung, die Sie ändern möchten.
  • Wenn Sie die Klasse View erweitern, zeichnen Sie die Form der benutzerdefinierten Ansicht und steuern Sie ihre Darstellung, indem Sie View-Methoden wie onDraw() und onMeasure() in der neuen Klasse überschreiben.
  • Fügen Sie Code hinzu, um auf Nutzerinteraktionen zu reagieren und die benutzerdefinierte Ansicht bei Bedarf neu zu zeichnen.
  • Verwenden Sie die benutzerdefinierte Ansichtsklasse als UI-Widget im XML-Layout Ihrer Aktivität. Sie können auch benutzerdefinierte Attribute für die Ansicht definieren, um sie in verschiedenen Layouts anzupassen.

In dieser Aufgabe werden Sie:

  • Erstellen Sie eine App mit einem ImageView als temporären Platzhalter für die benutzerdefinierte Ansicht.
  • Erweitern Sie View, um die benutzerdefinierte Ansicht zu erstellen.
  • Initialisieren Sie die benutzerdefinierte Ansicht mit Zeichen- und Malwerten.

Schritt 1: App mit einem ImageView-Platzhalter erstellen

  1. Erstellen Sie eine Kotlin-App mit dem Titel CustomFanController mithilfe der Vorlage „Empty Activity“. Achten Sie darauf, dass der Paketname com.example.android.customfancontroller ist.
  2. Öffnen Sie activity_main.xml auf dem Tab Text, um den XML-Code zu bearbeiten.
  3. Ersetzen Sie den vorhandenen TextView durch diesen Code. Dieser Text dient als Label in der Aktivität für die benutzerdefinierte Ansicht.
<TextView
       android:id="@+id/customViewLabel"
       android:textAppearance="@style/Base.TextAppearance.AppCompat.Display3"
       android:layout_width="wrap_content"
       android:layout_height="wrap_content"
       android:padding="16dp"
       android:textColor="@android:color/black"
       android:layout_marginStart="8dp"
       android:layout_marginEnd="8dp"
       android:layout_marginTop="24dp"
       android:text="Fan Control"
       app:layout_constraintLeft_toLeftOf="parent"
       app:layout_constraintRight_toRightOf="parent"
       app:layout_constraintTop_toTopOf="parent"/>
  1. Fügen Sie dem Layout dieses ImageView-Element hinzu. Dies ist ein Platzhalter für die benutzerdefinierte Ansicht, die Sie in diesem Codelab erstellen.
<ImageView
       android:id="@+id/dialView"
       android:layout_width="200dp"
       android:layout_height="200dp"
       android:background="@android:color/darker_gray"
       app:layout_constraintTop_toBottomOf="@+id/customViewLabel"
       app:layout_constraintLeft_toLeftOf="parent"
       app:layout_constraintRight_toRightOf="parent"
       android:layout_marginLeft="8dp"
       android:layout_marginRight="8dp"
       android:layout_marginTop="8dp"/>
  1. Extrahieren Sie String- und Dimensionsressourcen in beiden UI-Elementen.
  2. Klicken Sie auf den Tab Design. Das Layout sollte so aussehen:

Schritt 2: Benutzerdefinierte Ansichtsklasse erstellen

  1. Erstellen Sie eine neue Kotlin-Klasse mit dem Namen DialView.
  2. Ändern Sie die Klassendefinition, um View zu erweitern. Importieren Sie android.view.View, wenn Sie dazu aufgefordert werden.
  3. Klicken Sie auf View und dann auf die rote Glühbirne. Wählen Sie Add Android View constructors using '@JvmOverloads' (Android-View-Konstruktoren mit „@JvmOverloads“ hinzufügen) aus. In Android Studio wird der Konstruktor aus der Klasse View hinzugefügt. Die Annotation @JvmOverloads weist den Kotlin-Compiler an, Überladungen für diese Funktion zu generieren, die Standardparameterwerte ersetzen.
class DialView @JvmOverloads constructor(
   context: Context,
   attrs: AttributeSet? = null,
   defStyleAttr: Int = 0
) : View(context, attrs, defStyleAttr) {
  1. Fügen Sie über der DialView-Klassendefinition, direkt unter den Importen, eine enum-Klasse auf oberster Ebene hinzu, um die verfügbaren Lüftergeschwindigkeiten darzustellen. Beachten Sie, dass enum vom Typ Int ist, da die Werte Stringressourcen und keine tatsächlichen Strings sind. Android Studio zeigt für die fehlenden String-Ressourcen in jedem dieser Werte Fehler an. Diese beheben Sie in einem späteren Schritt.
private enum class FanSpeed(val label: Int) {
   OFF(R.string.fan_off),
   LOW(R.string.fan_low),
   MEDIUM(R.string.fan_medium),
   HIGH(R.string.fan_high);
}
  1. Fügen Sie unter enum die folgenden Konstanten hinzu. Sie werden für das Zeichnen der Messinstrumente und Labels verwendet.
private const val RADIUS_OFFSET_LABEL = 30      
private const val RADIUS_OFFSET_INDICATOR = -35
  1. Definieren Sie in der Klasse DialView mehrere Variablen, die Sie zum Zeichnen der benutzerdefinierten Ansicht benötigen. Importieren Sie android.graphics.PointF, falls Sie dazu aufgefordert werden.
private var radius = 0.0f                   // Radius of the circle.
private var fanSpeed = FanSpeed.OFF         // The active selection.
// position variable which will be used to draw label and indicator circle position
private val pointPosition: PointF = PointF(0.0f, 0.0f)
  • Die radius ist der aktuelle Radius des Kreises. Dieser Wert wird festgelegt, wenn die Ansicht auf dem Bildschirm gerendert wird.
  • Die fanSpeed ist die aktuelle Lüftergeschwindigkeit, die einer der Werte in der Aufzählung FanSpeed ist. Der Standardwert ist OFF.
  • Schließlich postPosition ist ein X,Y-Punkt, der zum Zeichnen mehrerer Elemente der Ansicht auf dem Bildschirm verwendet wird.

Diese Werte werden hier erstellt und initialisiert, anstatt wenn die Ansicht tatsächlich gezeichnet wird, damit der eigentliche Zeichenvorgang so schnell wie möglich abläuft.

  1. Initialisieren Sie außerdem in der DialView-Klassendefinition ein Paint-Objekt mit einigen grundlegenden Stilen. Importieren Sie android.graphics.Paint und android.graphics.Typeface, wenn Sie dazu aufgefordert werden. Wie bei den Variablen werden diese Stile hier initialisiert, um den Zeichenvorgang zu beschleunigen.
private val paint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
   style = Paint.Style.FILL
   textAlign = Paint.Align.CENTER
   textSize = 55.0f
   typeface = Typeface.create( "", Typeface.BOLD)
}
  1. Öffnen Sie res/values/strings.xml und fügen Sie die String-Ressourcen für die Lüftergeschwindigkeiten hinzu:
<string name="fan_off">off</string>
<string name="fan_low">1</string>
<string name="fan_medium">2</string>
<string name="fan_high">3</string>

Nachdem Sie eine benutzerdefinierte Ansicht erstellt haben, müssen Sie sie zeichnen können. Wenn Sie eine View-Unterklasse wie EditText erweitern, definiert diese Unterklasse das Erscheinungsbild und die Attribute der Ansicht und zeichnet sich selbst auf dem Bildschirm. Daher müssen Sie keinen Code schreiben, um die Ansicht zu zeichnen. Stattdessen können Sie Methoden des übergeordneten Elements überschreiben, um die Ansicht anzupassen.

Wenn Sie eine eigene Ansicht von Grund auf erstellen (durch Erweitern von View), sind Sie dafür verantwortlich, die gesamte Ansicht bei jeder Aktualisierung des Displays zu zeichnen und die View-Methoden zu überschreiben, die das Zeichnen verarbeiten. Damit eine benutzerdefinierte Ansicht, die View erweitert, richtig gezeichnet wird, müssen Sie Folgendes tun:

  • Berechnen Sie die Größe der Ansicht, wenn sie zum ersten Mal angezeigt wird, und jedes Mal, wenn sich die Größe der Ansicht ändert, indem Sie die Methode onSizeChanged() überschreiben.
  • Überschreiben Sie die Methode onDraw(), um die benutzerdefinierte Ansicht mit einem Canvas-Objekt zu zeichnen, das mit einem Paint-Objekt formatiert wird.
  • Rufen Sie die Methode invalidate() auf, wenn Sie auf einen Nutzerklick reagieren, der die Darstellung der Ansicht ändert, um die gesamte Ansicht ungültig zu machen und so einen Aufruf von onDraw() zu erzwingen, um die Ansicht neu zu zeichnen.

Die onDraw()-Methode wird jedes Mal aufgerufen, wenn der Bildschirm aktualisiert wird. Das kann mehrmals pro Sekunde geschehen. Aus Leistungsgründen und zur Vermeidung visueller Fehler sollten Sie in onDraw() so wenig wie möglich tun. Platzieren Sie Zuweisungen insbesondere nicht in onDraw(), da sie zu einer Garbage Collection führen können, die ein visuelles Ruckeln verursacht.

Die Klassen Canvas und Paint bieten eine Reihe nützlicher Zeichenkürzel:

Weitere Informationen zu Canvas und Paint finden Sie in einem späteren Codelab. Weitere Informationen dazu, wie Android Ansichten rendert

In dieser Aufgabe zeichnen Sie die benutzerdefinierte Ansicht des Lüftercontrollers auf den Bildschirm – das Zifferblatt selbst, die Anzeige der aktuellen Position und die Anzeigelabels – mit den Methoden onSizeChanged() und onDraw(). Außerdem erstellen Sie eine Hilfsmethode, computeXYForSpeed(),,um die aktuelle X- und Y-Position des Indikatorlabels auf dem Zifferblatt zu berechnen.

Schritt 1: Positionen berechnen und Ansicht zeichnen

  1. Überschreiben Sie in der Klasse DialView unter den Initialisierungen die Methode onSizeChanged() aus der Klasse View, um die Größe für das Zifferblatt der benutzerdefinierten Ansicht zu berechnen. Importieren Sie kotlin.math.min, wenn Sie dazu aufgefordert werden.

    Die Methode onSizeChanged() wird immer dann aufgerufen, wenn sich die Größe der Ansicht ändert, einschließlich des ersten Zeichnens, wenn das Layout aufgebläht wird. Überschreiben Sie onSizeChanged(), um Positionen, Dimensionen und alle anderen Werte zu berechnen, die sich auf die Größe Ihrer benutzerdefinierten Ansicht beziehen, anstatt sie bei jedem Zeichnen neu zu berechnen. In diesem Fall verwenden Sie onSizeChanged(), um den aktuellen Radius des Kreiselements des Zifferblatts zu berechnen.
override fun onSizeChanged(width: Int, height: Int, oldWidth: Int, oldHeight: Int) {
   radius = (min(width, height) / 2.0 * 0.8).toFloat()
}
  1. Fügen Sie unter onSizeChanged() diesen Code hinzu, um eine computeXYForSpeed()-Erweiterungsfunktion für die Klasse PointF zu definieren. Importieren Sie kotlin.math.cos und kotlin.math.sin, wenn Sie dazu aufgefordert werden. Diese Erweiterungsfunktion für die Klasse PointF berechnet die X- und Y-Koordinaten auf dem Bildschirm für das Textlabel und den aktuellen Indikator (0, 1, 2 oder 3) anhand der aktuellen FanSpeed-Position und des Radius des Zifferblatts. Sie verwenden diese Funktion in onDraw()..
private fun PointF.computeXYForSpeed(pos: FanSpeed, radius: Float) {
   // Angles are in radians.
   val startAngle = Math.PI * (9 / 8.0)   
   val angle = startAngle + pos.ordinal * (Math.PI / 4)
   x = (radius * cos(angle)).toFloat() + width / 2
   y = (radius * sin(angle)).toFloat() + height / 2
}
  1. Überschreiben Sie die Methode onDraw(), um die Ansicht mit den Klassen Canvas und Paint auf dem Bildschirm zu rendern. Importieren Sie android.graphics.Canvas, wenn Sie dazu aufgefordert werden. Das ist die Skelettüberschreibung:
override fun onDraw(canvas: Canvas) {
   super.onDraw(canvas)
   
}
  1. Fügen Sie in onDraw() diese Zeile hinzu, um die Farbe auf Grau (Color.GRAY) oder Grün (Color.GREEN) festzulegen, je nachdem, ob die Lüftergeschwindigkeit OFF oder ein anderer Wert ist. Importieren Sie android.graphics.Color, wenn Sie dazu aufgefordert werden.
// Set dial background color to green if selection not off.
paint.color = if (fanSpeed == FanSpeed.OFF) Color.GRAY else Color.GREEN
  1. Fügen Sie diesen Code hinzu, um mit der Methode drawCircle() einen Kreis für das Zifferblatt zu zeichnen. Bei dieser Methode werden die aktuelle Breite und Höhe der Ansicht verwendet, um den Mittelpunkt und den Radius des Kreises sowie die aktuelle Malfarbe zu ermitteln. Die Attribute width und height gehören zur Superklasse View und geben die aktuellen Dimensionen der Ansicht an.
// Draw the dial.
canvas.drawCircle((width / 2).toFloat(), (height / 2).toFloat(), radius, paint)
  1. Fügen Sie den folgenden Code hinzu, um mit der Methode drawCircle() einen kleineren Kreis für die Markierung der Lüftergeschwindigkeit zu zeichnen. In diesem Teil wird PointF verwendet.Die Erweiterungsmethode computeXYforSpeed() berechnet die X- und Y-Koordinaten für den Mittelpunkt des Indikators basierend auf der aktuellen Lüftergeschwindigkeit.
// Draw the indicator circle.
val markerRadius = radius + RADIUS_OFFSET_INDICATOR
pointPosition.computeXYForSpeed(fanSpeed, markerRadius)
paint.color = Color.BLACK
canvas.drawCircle(pointPosition.x, pointPosition.y, radius/12, paint)
  1. Zeichne schließlich die Labels für die Lüftergeschwindigkeit (0, 1, 2, 3) an den entsprechenden Positionen um das Zifferblatt herum. In diesem Teil der Methode wird PointF.computeXYForSpeed() noch einmal aufgerufen, um die Position für jedes Label abzurufen. Das pointPosition-Objekt wird jedes Mal wiederverwendet, um Zuweisungen zu vermeiden. Verwenden Sie drawText(), um die Labels zu zeichnen.
// Draw the text labels.
val labelRadius = radius + RADIUS_OFFSET_LABEL
for (i in FanSpeed.values()) {
   pointPosition.computeXYForSpeed(i, labelRadius)
   val label = resources.getString(i.label)
   canvas.drawText(label, pointPosition.x, pointPosition.y, paint)
}

Die fertige onDraw()-Methode sieht so aus:

override fun onDraw(canvas: Canvas) {
   super.onDraw(canvas)
   // Set dial background color to green if selection not off.
   paint.color = if (fanSpeed == FanSpeed.OFF) Color.GRAY else Color.GREEN
   // Draw the dial.
   canvas.drawCircle((width / 2).toFloat(), (height / 2).toFloat(), radius, paint)
   // Draw the indicator circle.
   val markerRadius = radius + RADIUS_OFFSET_INDICATOR
   pointPosition.computeXYForSpeed(fanSpeed, markerRadius)
   paint.color = Color.BLACK
   canvas.drawCircle(pointPosition.x, pointPosition.y, radius/12, paint)
   // Draw the text labels.
   val labelRadius = radius + RADIUS_OFFSET_LABEL
   for (i in FanSpeed.values()) {
       pointPosition.computeXYForSpeed(i, labelRadius)
       val label = resources.getString(i.label)
       canvas.drawText(label, pointPosition.x, pointPosition.y, paint)
   }
}

Schritt 2: Ansicht zum Layout hinzufügen

Wenn Sie der Benutzeroberfläche einer App eine benutzerdefinierte Ansicht hinzufügen möchten, geben Sie sie als Element im XML-Layout der Aktivität an. Sie können das Aussehen und Verhalten mit XML-Elementattributen steuern, wie bei jedem anderen UI-Element.

  1. Ändern Sie in activity_main.xml das ImageView-Tag für dialView in com.example.android.customfancontroller.DialView und löschen Sie das Attribut android:background. Sowohl DialView als auch das ursprüngliche ImageView übernehmen die Standardattribute aus der Klasse View. Daher müssen Sie keine der anderen Attribute ändern. Das neue DialView-Element sieht so aus:
<com.example.android.customfancontroller.DialView
       android:id="@+id/dialView"
       android:layout_width="@dimen/fan_dimen"
       android:layout_height="@dimen/fan_dimen"
       app:layout_constraintTop_toBottomOf="@+id/customViewLabel"
       app:layout_constraintLeft_toLeftOf="parent"
       app:layout_constraintRight_toRightOf="parent"
       android:layout_marginLeft="@dimen/default_margin"
       android:layout_marginRight="@dimen/default_margin"
       android:layout_marginTop="@dimen/default_margin" />
  1. Führe die App aus. Die Ansicht zur Lüftersteuerung wird in der Aktivität angezeigt.

Die letzte Aufgabe besteht darin, Ihrer benutzerdefinierten Ansicht zu ermöglichen, eine Aktion auszuführen, wenn der Nutzer auf die Ansicht tippt. Bei jedem Tippen sollte der Auswahlindikator zur nächsten Position wechseln: Aus – 1 – 2 – 3 und zurück zu Aus. Wenn die Auswahl auf 1 oder höher steht, sollte sich der Hintergrund von Grau zu Grün ändern, um anzuzeigen, dass die Lüfterleistung aktiviert ist.

Damit Ihre benutzerdefinierte Ansicht anklickbar ist, müssen Sie:

  • Legen Sie das Attribut isClickable der Ansicht auf true fest. So kann Ihre benutzerdefinierte Ansicht auf Klicks reagieren.
  • Implementieren Sie die performClick()-Methode der View-Klasse, um Vorgänge auszuführen, wenn auf die Ansicht geklickt wird.
  • Rufen Sie die Methode invalidate() auf. Dadurch wird das Android-System angewiesen, die Methode onDraw() aufzurufen, um die Ansicht neu zu zeichnen.

Normalerweise implementieren Sie bei einer Standard-Android-Ansicht OnClickListener(), um eine Aktion auszuführen, wenn der Nutzer auf diese Ansicht klickt. Bei einer benutzerdefinierten Ansicht implementieren Sie stattdessen die Methode performClick() der Klasse View und rufen super auf.performClick(). Die Standardmethode performClick() ruft auch onClickListener() auf. Sie können Ihre Aktionen also performClick() hinzufügen und onClickListener() für weitere Anpassungen durch Sie oder andere Entwickler, die Ihre benutzerdefinierte Ansicht verwenden, verfügbar lassen.

  1. Fügen Sie in DialView.kt in der Aufzählung FanSpeed eine Erweiterungsfunktion next() hinzu, die die aktuelle Lüftergeschwindigkeit in die nächste Geschwindigkeit in der Liste ändert (von OFF zu LOW, MEDIUM und HIGH und dann zurück zu OFF). Die vollständige Aufzählung sieht jetzt so aus:
private enum class FanSpeed(val label: Int) {
   OFF(R.string.fan_off),
   LOW(R.string.fan_low),
   MEDIUM(R.string.fan_medium),
   HIGH(R.string.fan_high);

   fun next() = when (this) {
       OFF -> LOW
       LOW -> MEDIUM
       MEDIUM -> HIGH
       HIGH -> OFF
   }
}
  1. Fügen Sie in der Klasse DialView direkt vor der Methode onSizeChanged() einen init()-Block ein. Wenn die Eigenschaft isClickable der Ansicht auf „true“ gesetzt wird, kann die Ansicht Nutzereingaben akzeptieren.
init {
   isClickable = true
}
  1. Überschreiben Sie unter init(), die Methode performClick() mit dem folgenden Code.
override fun performClick(): Boolean {
   if (super.performClick()) return true

   fanSpeed = fanSpeed.next()
   contentDescription = resources.getString(fanSpeed.label)
  
   invalidate()
   return true
}

Der Anruf bei super.performClick() muss zuerst erfolgen, wodurch Bedienungshilfe-Ereignisse sowie Aufrufe von onClickListener() aktiviert werden.

In den nächsten beiden Zeilen wird die Geschwindigkeit des Ventilators mit der Methode next() erhöht und die Inhaltsbeschreibung der Ansicht auf die String-Ressource für die aktuelle Geschwindigkeit (Aus, 1, 2 oder 3) festgelegt.

Schließlich wird mit der Methode invalidate() die gesamte Ansicht ungültig gemacht, sodass onDraw() aufgerufen werden muss, um die Ansicht neu zu zeichnen. Wenn sich aus irgendeinem Grund etwas in Ihrer benutzerdefinierten Ansicht ändert, z. B. durch Nutzerinteraktion, und die Änderung angezeigt werden muss, rufen Sie invalidate(). auf.

  1. Führen Sie die App aus. Tippen Sie auf das Element DialView, um den Indikator von „Aus“ auf „1“ zu stellen. Das Zifferblatt sollte grün werden. Bei jedem Tippen sollte sich die Markierung auf die nächste Position verschieben. Wenn die Anzeige wieder aus ist, sollte das Zifferblatt wieder grau werden.

In diesem Beispiel wird die grundlegende Funktionsweise der Verwendung benutzerdefinierter Attribute mit Ihrer benutzerdefinierten Ansicht gezeigt. Sie definieren benutzerdefinierte Attribute für die Klasse DialView mit einer anderen Farbe für jede Position des Lüfterrads.

  1. Erstellen und öffnen Sie res/values/attrs.xml.
  2. Fügen Sie in <resources> ein <declare-styleable>-Ressourcenelement hinzu.
  3. Fügen Sie dem <declare-styleable>-Ressourcenelement drei attr-Elemente hinzu, eines für jedes Attribut, mit einem name und einem format. format ist wie ein Typ und in diesem Fall color.
<?xml version="1.0" encoding="utf-8"?>
<resources>
       <declare-styleable name="DialView">
           <attr name="fanColor1" format="color" />
           <attr name="fanColor2" format="color" />
           <attr name="fanColor3" format="color" />
       </declare-styleable>
</resources>
  1. Öffnen Sie die Layoutdatei activity_main.xml.
  2. Fügen Sie im DialView Attribute für fanColor1, fanColor2 und fanColor3 hinzu und legen Sie ihre Werte auf die unten gezeigten Farben fest. Verwenden Sie app: als Präfix für das benutzerdefinierte Attribut (wie in app:fanColor1) anstelle von android:, da Ihre benutzerdefinierten Attribute zum Namespace schemas.android.com/apk/res/your_app_package_name und nicht zum Namespace android gehören.
app:fanColor1="#FFEB3B"
app:fanColor2="#CDDC39"
app:fanColor3="#009688"

Wenn Sie die Attribute in Ihrer DialView-Klasse verwenden möchten, müssen Sie sie abrufen. Sie werden in einem AttributeSet gespeichert, das Ihrer Klasse bei der Erstellung übergeben wird, sofern es vorhanden ist. Sie rufen die Attribute in init ab und weisen die Attributwerte lokalen Variablen zum Zwischenspeichern zu.

  1. Öffnen Sie die Klassendatei DialView.kt.
  2. Deklarieren Sie im DialView Variablen, um die Attributwerte im Cache zu speichern.
private var fanSpeedLowColor = 0
private var fanSpeedMediumColor = 0
private var fanSeedMaxColor = 0
  1. Fügen Sie im Block init den folgenden Code mit der Erweiterungsfunktion withStyledAttributes hinzu. Sie geben die Attribute und die Ansicht an und legen die lokalen Variablen fest. Beim Importieren von withStyledAttributes wird auch die richtige getColor()-Funktion importiert.
context.withStyledAttributes(attrs, R.styleable.DialView) {
   fanSpeedLowColor = getColor(R.styleable.DialView_fanColor1, 0)
   fanSpeedMediumColor = getColor(R.styleable.DialView_fanColor2, 0)
   fanSeedMaxColor = getColor(R.styleable.DialView_fanColor3, 0)
}
  1. Verwende die lokalen Variablen in onDraw(), um die Zifferblattfarbe basierend auf der aktuellen Lüftergeschwindigkeit festzulegen. Ersetzen Sie die Zeile, in der die Farbe festgelegt wird (paint.color = if (fanSpeed == FanSpeed.OFF) Color.GRAY else Color.GREEN), durch den folgenden Code.
paint.color = when (fanSpeed) {
   FanSpeed.OFF -> Color.GRAY
   FanSpeed.LOW -> fanSpeedLowColor
   FanSpeed.MEDIUM -> fanSpeedMediumColor
   FanSpeed.HIGH -> fanSeedMaxColor
} as Int
  1. Führen Sie die App aus, klicken Sie auf das Zifferblatt und die Farbeinstellung sollte für jede Position unterschiedlich sein, wie unten dargestellt.

Weitere Informationen zu benutzerdefinierten Attributen für Ansichten finden Sie unter View-Klasse erstellen.

Barrierefreiheit umfasst eine Reihe von Design-, Implementierungs- und Testtechniken, mit denen Ihre App für alle Nutzer, einschließlich Menschen mit Behinderungen, nutzbar ist.

Häufige Behinderungen, die die Nutzung eines Android-Geräts beeinträchtigen, sind Blindheit, eingeschränktes Sehvermögen, Farbenblindheit, Taubheit oder Hörverlust und eingeschränkte motorische Fähigkeiten. Wenn Sie Ihre Apps mit Blick auf die Barrierefreiheit entwickeln, verbessern Sie die Nutzerfreundlichkeit nicht nur für Nutzer mit diesen Behinderungen, sondern auch für alle anderen Nutzer.

Android bietet standardmäßig mehrere Bedienungshilfen in den Standard-UI-Ansichten wie TextView und Button. Wenn Sie eine benutzerdefinierte Ansicht erstellen, müssen Sie jedoch berücksichtigen, wie diese benutzerdefinierte Ansicht barrierefreie Funktionen wie gesprochene Beschreibungen von Inhalten auf dem Bildschirm bereitstellt.

In dieser Aufgabe lernen Sie den Android-Screenreader TalkBack kennen und ändern Ihre App so, dass sie für die benutzerdefinierte Ansicht DialView vorlesbare Hinweise und Beschreibungen enthält.

Schritt 1: TalkBack kennenlernen

TalkBack ist der integrierte Screenreader von Android. Wenn TalkBack aktiviert ist, kann der Nutzer sein Android-Gerät bedienen, ohne auf den Bildschirm zu sehen, da Android Bildschirmelemente laut vorliest. Nutzer mit Sehbehinderung verwenden möglicherweise TalkBack, um Ihre App zu nutzen.

In dieser Aufgabe aktivieren Sie TalkBack, um zu verstehen, wie Screenreader funktionieren und wie Sie Apps bedienen.

  1. Rufen Sie auf einem Android-Gerät oder -Emulator Einstellungen > Bedienungshilfen > TalkBack auf.
  2. Tippen Sie auf den Schalter Ein/Aus, um TalkBack zu aktivieren.
  3. Tippen Sie zum Bestätigen der Berechtigungen auf OK.
  4. Bestätigen Sie Ihr Gerätepasswort, wenn Sie dazu aufgefordert werden. Wenn Sie TalkBack zum ersten Mal ausführen, wird eine Anleitung gestartet. Das Tutorial ist möglicherweise nicht auf älteren Geräten verfügbar.
  5. Es kann hilfreich sein, die Anleitung mit geschlossenen Augen zu durchlaufen. Wenn Sie die Anleitung später noch einmal aufrufen möchten, gehen Sie zu Einstellungen > Bedienungshilfen > TalkBack > Einstellungen > TalkBack-Anleitung starten.
  6. Kompiliere und starte die CustomFanController App oder öffne sie über die Schaltfläche Übersicht oder Letzte auf deinem Gerät. Wenn TalkBack aktiviert ist, wird der Name der App sowie der Text des Labels TextView („Lüftersteuerung“) vorgelesen. Wenn Sie jedoch auf die DialView-Ansicht selbst tippen, werden weder Informationen zum Status der Ansicht (die aktuelle Einstellung für das Zifferblatt) noch zur Aktion, die beim Tippen auf die Ansicht ausgeführt wird, ausgegeben.

Schritt 2: Inhaltsbeschreibungen für Zifferblatt-Labels hinzufügen

Inhaltsbeschreibungen beschreiben die Bedeutung und den Zweck der Ansichten in Ihrer App. Mit diesen Labels können Screenreader wie TalkBack von Android die Funktion jedes Elements genau erklären. Bei statischen Ansichten wie ImageView können Sie die Inhaltsbeschreibung mit dem Attribut contentDescription in der Layoutdatei hinzufügen. Bei Textansichten (TextView und EditText) wird der Text in der Ansicht automatisch als Inhaltsbeschreibung verwendet.

Für die Ansicht zur benutzerdefinierten Lüftersteuerung müssen Sie die Inhaltsbeschreibung jedes Mal, wenn auf die Ansicht geklickt wird, dynamisch aktualisieren, um die aktuelle Lüftereinstellung anzugeben.

  1. Deklarieren Sie unten in der Klasse DialView eine Funktion updateContentDescription() ohne Argumente oder Rückgabetyp.
fun updateContentDescription() {
}
  1. Ändern Sie in updateContentDescription() das Attribut contentDescription für die benutzerdefinierte Ansicht in die String-Ressource, die der aktuellen Lüftergeschwindigkeit (Aus, 1, 2 oder 3) zugeordnet ist. Dies sind dieselben Labels, die in onDraw() verwendet werden, wenn das Zifferblatt auf dem Display angezeigt wird.
fun updateContentDescription() {
   contentDescription = resources.getString(fanSpeed.label)
}
  1. Scrollen Sie nach oben zum init()-Block und fügen Sie am Ende dieses Blocks einen Aufruf von updateContentDescription() hinzu. Dadurch wird die Inhaltsbeschreibung initialisiert, wenn die Ansicht initialisiert wird.
init {
   isClickable = true
   // ...

   updateContentDescription()
}
  1. Fügen Sie in der Methode performClick() einen weiteren Aufruf von updateContentDescription() direkt vor invalidate() hinzu.
override fun performClick(): Boolean {
   if (super.performClick()) return true
   fanSpeed = fanSpeed.next()
   updateContentDescription()
   invalidate()
   return true
}
  1. Kompilieren Sie die App, führen Sie sie aus und achten Sie darauf, dass TalkBack aktiviert ist. Tippen Sie, um die Einstellung für die Zifferblattansicht zu ändern. TalkBack gibt jetzt das aktuelle Label (Aus, 1, 2, 3) sowie den Hinweis „Doppeltippen zum Aktivieren“ aus.

Schritt 3: Weitere Informationen für die Klickaktion hinzufügen

An dieser Stelle könnten Sie aufhören. Die Ansicht wäre in TalkBack nutzbar. Es wäre jedoch hilfreich, wenn in der Ansicht nicht nur angezeigt würde, dass sie aktiviert werden kann („Zum Aktivieren doppeltippen“), sondern auch, was passiert, wenn die Ansicht aktiviert wird („Zum Ändern doppeltippen“ oder „Zum Zurücksetzen doppeltippen“).

Dazu fügen Sie einem Objekt mit Informationen zum Barrierefreiheitsknoten über einen Barrierefreiheits-Delegate Informationen zur Aktion der Ansicht hinzu (hier: eine Klick- oder Tippaktion). Mit einem Bedienungshilfedelegaten können Sie die Bedienungshilfefunktionen Ihrer App über Komposition (anstatt Vererbung) anpassen.

Für diese Aufgabe verwenden Sie die Bedienungshilfenklassen in den Android Jetpack-Bibliotheken (androidx.*), um die Abwärtskompatibilität sicherzustellen.

  1. Legen Sie in DialView.kt im Block init einen Barrierefreiheits-Delegate für die Ansicht als neues AccessibilityDelegateCompat-Objekt fest. Importieren Sie androidx.core.view.ViewCompat und androidx.core.view.AccessibilityDelegateCompat, wenn Sie dazu aufgefordert werden. Diese Strategie bietet die größte Abwärtskompatibilität für Ihre App.
ViewCompat.setAccessibilityDelegate(this, object : AccessibilityDelegateCompat() {
   
})
  1. Überschreiben Sie im AccessibilityDelegateCompat-Objekt die Funktion onInitializeAccessibilityNodeInfo() mit einem AccessibilityNodeInfoCompat-Objekt und rufen Sie die Methode des Super-Objekts auf. Importieren Sie androidx.core.view.accessibility.AccessibilityNodeInfoCompat, wenn Sie dazu aufgefordert werden.
ViewCompat.setAccessibilityDelegate(this, object : AccessibilityDelegateCompat() {
   override fun onInitializeAccessibilityNodeInfo(host: View, 
                            info: AccessibilityNodeInfoCompat) {
      super.onInitializeAccessibilityNodeInfo(host, info)

   }  
})

Jede Ansicht hat einen Baum von Barrierefreiheitsknoten, die den tatsächlichen Layoutkomponenten der Ansicht entsprechen können oder auch nicht. Die Bedienungshilfen von Android navigieren durch diese Knoten, um Informationen zur Ansicht zu finden, z. B. vorlesbare Inhaltsbeschreibungen oder mögliche Aktionen, die in dieser Ansicht ausgeführt werden können. Wenn Sie eine benutzerdefinierte Ansicht erstellen, müssen Sie möglicherweise auch die Knoteninformationen überschreiben, um benutzerdefinierte Informationen für die Barrierefreiheit bereitzustellen. In diesem Fall überschreiben Sie die Knoteninformationen, um anzugeben, dass benutzerdefinierte Informationen für die Aktion der Ansicht vorhanden sind.

  1. Erstellen Sie in onInitializeAccessibilityNodeInfo() ein neues AccessibilityNodeInfoCompat.AccessibilityActionCompat-Objekt und weisen Sie es der Variablen customClick zu. Übergeben Sie die Konstante AccessibilityNodeInfo.ACTION_CLICK und einen Platzhalterstring an den Konstruktor. Importieren Sie AccessibilityNodeInfo, wenn Sie dazu aufgefordert werden.
ViewCompat.setAccessibilityDelegate(this, object : AccessibilityDelegateCompat() {
   override fun onInitializeAccessibilityNodeInfo(host: View, 
                            info: AccessibilityNodeInfoCompat) {
      super.onInitializeAccessibilityNodeInfo(host, info)
      val customClick = AccessibilityNodeInfoCompat.AccessibilityActionCompat(
         AccessibilityNodeInfo.ACTION_CLICK,
        "placeholder"
      )
   }  
})

Die Klasse AccessibilityActionCompat stellt eine Aktion für eine Ansicht für Bedienungshilfen dar. Eine typische Aktion ist ein Klick oder Tipp, wie Sie ihn hier verwenden. Andere Aktionen können das Erhalten oder Verlieren des Fokus, ein Zwischenablagevorgang (Ausschneiden/Kopieren/Einfügen) oder das Scrollen in der Ansicht sein. Der Konstruktor für diese Klasse erfordert eine Aktionskonstante (hier AccessibilityNodeInfo.ACTION_CLICK) und einen String, der von TalkBack verwendet wird, um anzugeben, was die Aktion ist.

  1. Ersetzen Sie den String "placeholder" durch einen Aufruf von context.getString(), um eine String-Ressource abzurufen. Prüfe für die jeweilige Ressource die aktuelle Lüftergeschwindigkeit. Wenn die Geschwindigkeit derzeit FanSpeed.HIGH beträgt, ist der String "Reset". Wenn die Lüftergeschwindigkeit einen anderen Wert hat, ist der String "Change.". Diese String-Ressourcen erstellen Sie in einem späteren Schritt.
ViewCompat.setAccessibilityDelegate(this, object : AccessibilityDelegateCompat() {
   override fun onInitializeAccessibilityNodeInfo(host: View, 
                            info: AccessibilityNodeInfoCompat) {
      super.onInitializeAccessibilityNodeInfo(host, info)
      val customClick = AccessibilityNodeInfoCompat.AccessibilityActionCompat(
         AccessibilityNodeInfo.ACTION_CLICK,
        context.getString(if (fanSpeed !=  FanSpeed.HIGH) R.string.change else R.string.reset)
      )
   }  
})
  1. Fügen Sie nach der schließenden Klammer für die customClick-Definition die neue Barrierefreiheitsaktion mit der Methode addAction() dem Knoteninfo-Objekt hinzu.
ViewCompat.setAccessibilityDelegate(this, object : AccessibilityDelegateCompat() {
   override fun onInitializeAccessibilityNodeInfo(host: View, 
                            info: AccessibilityNodeInfoCompat) {
       super.onInitializeAccessibilityNodeInfo(host, info)
       val customClick = AccessibilityNodeInfoCompat.AccessibilityActionCompat(
           AccessibilityNodeInfo.ACTION_CLICK,
           context.getString(if (fanSpeed !=  FanSpeed.HIGH) 
                                 R.string.change else R.string.reset)
       )
       info.addAction(customClick)
   }
})
  1. Fügen Sie in res/values/strings.xml die String-Ressourcen für „Ändern“ und „Zurücksetzen“ hinzu.
<string name="change">Change</string>
<string name="reset">Reset</string>
  1. Kompilieren Sie die App, führen Sie sie aus und achten Sie darauf, dass TalkBack aktiviert ist. Die Aufforderung „Zum Aktivieren doppeltippen“ lautet jetzt entweder „Zum Ändern doppeltippen“ (wenn die Lüftergeschwindigkeit niedriger als „Hoch“ oder „3“ ist) oder „Zum Zurücksetzen doppeltippen“ (wenn die Lüftergeschwindigkeit bereits „Hoch“ oder „3“ ist). Der Hinweis „Doppeltippen zum…“ wird vom TalkBack-Dienst selbst bereitgestellt.

Laden Sie den Code für das fertige Codelab herunter.

$  git clone https://github.com/googlecodelabs/android-kotlin-drawing-custom-views


Alternativ können Sie das Repository als ZIP-Datei herunterladen, entzippen und in Android Studio öffnen.

Zip herunterladen

  • Wenn Sie eine benutzerdefinierte Ansicht erstellen möchten, die das Aussehen und Verhalten einer View-Unterklasse wie EditText übernimmt, fügen Sie eine neue Klasse hinzu, die diese Unterklasse erweitert, und nehmen Sie Anpassungen vor, indem Sie einige der Methoden der Unterklasse überschreiben.
  • Wenn Sie eine benutzerdefinierte Ansicht mit einer beliebigen Größe und Form erstellen möchten, fügen Sie eine neue Klasse hinzu, die View erweitert.
  • Überschreiben Sie View-Methoden wie onDraw(), um die Form und das grundlegende Erscheinungsbild der Ansicht zu definieren.
  • Verwenden Sie invalidate(), um das Zeichnen oder Neuzeichnen der Ansicht zu erzwingen.
  • Um die Leistung zu optimieren, sollten Sie Variablen zuweisen und alle erforderlichen Werte für das Zeichnen und Malen bevor zuweisen, Sie sie in onDraw() verwenden, z. B. bei der Initialisierung von Membervariablen.
  • Überschreiben Sie performClick() anstelle von OnClickListener(), um das interaktive Verhalten der benutzerdefinierten Ansicht zu definieren. So können Sie oder andere Android-Entwickler, die Ihre benutzerdefinierte Ansichtsklasse verwenden, mit onClickListener() zusätzliches Verhalten bereitstellen.
  • Fügen Sie die benutzerdefinierte Ansicht einer XML-Layoutdatei mit Attributen hinzu, um ihr Erscheinungsbild zu definieren, wie Sie es auch bei anderen UI-Elementen tun würden.
  • Erstellen Sie die Datei attrs.xml im Ordner values, um benutzerdefinierte Attribute zu definieren. Anschließend können Sie die benutzerdefinierten Attribute für die benutzerdefinierte Ansicht in der XML-Layoutdatei verwenden.

Udacity-Kurs:

Android-Entwicklerdokumentation:

Videos:

In diesem Abschnitt werden mögliche Hausaufgaben für Schüler und Studenten aufgeführt, die dieses Codelab im Rahmen eines von einem Kursleiter geleiteten Kurses durcharbeiten. Es liegt in der Verantwortung des Kursleiters, Folgendes zu tun:

  • Weisen Sie bei Bedarf Aufgaben zu.
  • Teilen Sie den Schülern/Studenten mit, wie sie Hausaufgaben abgeben können.
  • Benoten Sie die Hausaufgaben.

Lehrkräfte können diese Vorschläge nach Belieben nutzen und auch andere Hausaufgaben zuweisen, die sie für angemessen halten.

Wenn Sie dieses Codelab selbst durcharbeiten, können Sie mit diesen Hausaufgaben Ihr Wissen testen.

Frage 1

Welche Methode überschreiben Sie, um die Positionen, Dimensionen und alle anderen Werte zu berechnen, wenn der benutzerdefinierten Ansicht zum ersten Mal eine Größe zugewiesen wird?

▢ onMeasure()

▢ onSizeChanged()

▢ invalidate()

▢ onDraw()

Frage 2

Welche Methode rufen Sie im UI-Thread auf, nachdem sich ein Attributwert geändert hat, um anzugeben, dass die Ansicht mit onDraw() neu gezeichnet werden soll?

▢ onMeasure()

▢ onSizeChanged()

▢ invalidate()

▢ getVisibility()

Frage 3

Welche View-Methode sollten Sie überschreiben, um Ihrer benutzerdefinierten Ansicht Interaktivität hinzuzufügen?

▢ setOnClickListener()

▢ onSizeChanged()

▢ isClickable()

▢ performClick()

Links zu anderen Codelabs in diesem Kurs finden Sie auf der Landingpage für Codelabs zum Thema „Android für Fortgeschrittene mit Kotlin“.