Android-Programmierung GUI: View
Willemers Informatik-Ecke

Anlegen einer View

Eine View ist eine Klasse, die sich von der Klasse android.view.View ableitet. Man kann die Klasse einfach selbst erstellen. Es entsteht eine neue Datei mit einer leeren Klasse.
package de.willemer.testview;

public class MeineView {
}
Zunächst wird an den Klassennamen extends View angehängt, was eine View ja schließlich ausmacht. Dafür wird der entsprechende Import von android.view.View benötigt. Die Basisklasse View besitzt keinen Standardkonstruktor. Darum mus mindestens einer der Konstruktoren mit Parametern überschrieben werden.

Der Parameter ist ein Context. Typischerweise liefert den die Activity, in die die View eingebunden ist.

package de.willemer.testview;

import android.content.Context;
import android.view.View;

public class MeineView extends View {
    public MeineView(Context context) {
        super(context);
    }
}
Soll die View später auch in eine XML-Datei eingebunden werden, muss sie einen weiteren Parameter AttributeSet bekommen. Der dritte Konstruktor hat als weiteren Parameter eine int-Variable für den defStyle, die benötigt wird, wenn innerhalb der XML-Datei ein Attribut style vergeben wird. Im folgenden Beispiel überschreiben wir alle drei denkbaren Konstruktoren und rufen die Gegenstücke der Basisklasse auf.
package de.willemer.testview;

import android.content.Context;
import android.view.View;

public class MeineView extends View {
    public MeineView(Context context) {
        super(context);
    }
    public MeineView(Context context, AttributeSet attrs) {
        super(context);
    }
    public MeineView(Context context, AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);
    }
}
Um die View erkennen zu können, zeichnen wir eine Diagonale:
public class MeineView extends View {

    // ...

    @Override
    public void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        Paint paint = new Paint();
        canvas.drawLine(0, 0, getWidth(), getHeight(), paint);
    }
}

Einbindung einer View

Die View benötigt eine Activity, um dargestellt zu werden.

Einbindung in das Layout der Activity

Die eigene View kann wie ein normales Kontrollelement in die Layout-XML-Datei einer Activity eingebunden werden.

Unter app/res/layout wird die Datei activity_main.xml doppelt angeklickt. Auf der rechten Seite erscheint XML in einem Editor (Code), eine Darstellung des Displays in zwei Rechtecken (Design) oder eine Zwischenform (Split). Zwischen den drei Zuständen kann rechts oben umgestaltet werden.

Im Modus Design wird auf der linken Seite die Palette sichtbar. Hier zieht man einfach ein beliebiges Element, beispielsweise eine TextView auf das Feld und ändert es im Modus Code in eine View. Dazu wird der Bezeichner TextView in den Klassennamen der View inklusive des Packages aufgeführt.

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
...
    <TextView
     ... />
    ...
    <de.willemer.testview.MeineView
        android:id="@+id/bermudaview"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        />
...
</LinearLayout>
Die Änderung von layout_width und layout_height von match_parent auf wrap_content ist nicht sinnvoll, da die View in der Regel nicht weiß, welchen Raum sie benötigt.

Parameter der XML-Datei

In der Layout-XML-Datei können Parameter für die View angegeben werden, die von der View in Java gelesen werden können.

    <de.willemer.testview.MeineView
     style="@style/Widget.Theme.Testview.MyView"
     android:layout_width="300dp"
     android:layout_height="300dp"
     android:paddingLeft="20dp"
     android:paddingBottom="40dp"
     app:exampleDimension="24sp"
     app:exampleDrawable="@android:drawable/ic_menu_add"
     app:exampleString="Hello, MineSweeperView" />
Die angegebenen Werte können aus der View folgendermaßen gelesen werden.
final TypedArray a = getContext().obtainStyledAttributes(attrs, R.styleable.MineSweeperView, defStyle, 0);
mExampleString = a.getString(R.styleable.MineSweeperView_exampleString);
mExampleColor = a.getColor(R.styleable.MineSweeperView_exampleColor, mExampleColor);
mExampleDimension = a.getDimension(R.styleable.MineSweeperView_exampleDimension, mExampleDimension);
if (a.hasValue(R.styleable.  MineSweeperView_exampleDrawable)) {
    mExampleDrawable = a.getDrawable(R.styleable.  MineSweeperView_exampleDrawable);
    mExampleDrawable.setCallback(this);
}

Eine View vollflächig

Die Kontrollelemente wie Eingabefelder oder Datumspicker sind von Android vorgefertigte Views. Sie haben einen Bereich, in dem sie sich darstellen und in dem sie die Ereignisse überwachen.

Wird außer der View auf dem Display kein weiteres Eingabeelement benötigt, kann die View als Content in der Activity statt der XML-Datei angegeben werden. Dies kommt hauptsächlich bei Spielen in Betracht, die außer dem Spielfeld in der Activity keine Eingabeelemente benötigen.

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        // setContentView(R.layout.activity_main); // nicht mehr nötig!
        setContentView(new MeineView(this));
    }
}
Durch diese Änderung wird die Datei activity_main.xml nicht mehr eingelesen und kann also auch entfernt werden.

Zeichnungen in der Methode onDraw

Die View ist dafür verantwortlich, ihren Inhalt jederzeit darstellen zu können. Dazu ruft das System die Methode onDraw auf. Will die eigene View mehr als einen weißen Bildschirm zeigen, muss sie die Methode onDraw überschreiben.
    @Override
    public void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        Paint paint = new Paint();
        // Darstellung der View
    }
Als Parameter erhält sie ein Objekt von Canvas. Das Canvas stellt die Zeichenfläche dar. Über sie werden die Zeichen-Methoden aufgerufen. Die Zeichenprimitive benötigen neben dem Canvas aber auch ein Objekt der Klasse Paint. Das Paint-Objekt legen Sie selbst an. Es bestimmt die Art der Zeichnung, beispielsweise die Farbe, ob Elemente gefüllt oder umrahmt werden.

float posx = 100, posy = 100;

@Override
protected void onDraw(Canvas canvas) {
    super.onDraw(canvas);
    Paint paint = new Paint();
    paint.setColor(Color.RED);
    canvas.drawCircle(posx, posy, 5, paint);
}

Ereignisse

Wenn Sie eine eigene View schreiben, werden Sie auch auf Ereignisse reagieren wollen.

Ereignisse führen häufig dazu, dass sich auf dem Bildschirm Dinge verändern sollen. Ein direktes Zeichnen ist in solchen Fällen nicht möglich, da dies durch die Methode onDraw umgesetzt wird. Stattdessen werden die Datenstrukturen für onDraw vorgereitet und durch den Aufruf von invalidate ein Neuzeichnen ausgelöst.

Berührung: onTouchEvent

Das typische Ereignis ist die Berührung des Displays mit dem Finger oder einem Stift.
@Override
public boolean onTouchEvent(MotionEvent event) {
    // Lese das Auslösungselement des Events
    int actionPerformed = event.getAction();
    int x = event.getX();
    int y = event.getY();
    // Aktionen aufgrund des Events
    return true; // wenn Ereignis behandelt wurde
}
Eine Berührung liefert in der Regel zwei Ereignisse. Die erste ist das Berühren und das zweite ist das Entfernen des Fingers von der Scheibe. Haben beide Ereignisse unterschiedliche Koordinaten, wollte der Benutzer vermutlich ein Objekt über den Bildschirm verschieben.

Ein kleiner Test

Der folgende Programmausschnitt erzeugt bei jeder Berührung einen Punkt auf dem Display. Da der ganze Bildschirm neu gezeichnet wird, ist immer nur der letzte Berührungspunkt zu sehen. Um Sommersprossen zeigen zu können, müssten die Ereignisse in einer Liste oder einem Array gespeichert werden.
    // globale Variable für den Datenaustausch zwischen onDraw und onTouchEvent
    float posx = -1, posy = -1;

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        posx = event.getX();
        posy = event.getY();
        invalidate();   // Zwingt zum Neuzeichnen der View
        return true;    // Verarbeitung ist hier erfolgt!
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        Paint paint = new Paint();
        // Kontrolle des Eingabepunkts
        if (posx>=0 && posy>=0) {
            canvas.drawCircle(posx, posy, 5, paint);
        }
    }

Tasten

Wenn das Android-Gerät über eine Tastatur verfügt, können auch diese ausgewertet werden.
@Override
public boolean onKeyDown(int keyCode, KeyEvent keyEvent) {
    // Aktionen beim Niederdrücken einer Taste
    return true; // wenn Ereignis behandelt wurde
}
@Override
public boolean onKeyUp(int keyCode, KeyEvent keyEvent) {
    // Aktionen beim Loslassen einer niedergedrückten Taste
    return true; // wenn Ereignis behandelt wurde
}

Größenermittlung der View: onMeasure

Das Ereignis onMeasure wird ausgelöst, wenn das System wissen will, welche Platzansprüche die View hat. Das Grundgerüst eine onMeasure-Methode:
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    int w = MeasureSpec.getSize(widthMeasureSpec);
    int h =  MeasureSpec.getSize(heightMeasureSpec);
    int myW = getMeineBreite(w);
    int myH = getMeineHoehe(h);
    setMeasuredDimension(myW, myH);
}
Das System ruft onMeasure auf, um die View zu fragen, welche Größe sie haben will. Die beiden Parameter sind keineswegs Breite und Höhe des Displays. Um den verfügbaren Platz zu ermitteln, werden diese als Parameter an die Methode MeasureSpec.getSize übergeben, die dann je nach Parameter Höhe und Breite ermittelt.

Die View errechnet nun anhand des zur Verfügung stehenden Platzes die eigenen Bedürfnissen. Diese Ausdehnung gibt sie als Parameter an die Methode setMeasuredDimension, die typischerweise immer am Schluss der Methode onMeasure aufgerufen wird.

Ausdehnungsmodus

Android kann verschiedene Anforderungen an die Ausdehnung der View haben. So kann sie vorschreiben, dass der angegebene Platz exakt einzuhalten ist oder sie kann angeben, dass dies der maximal verfügbare Platz ist. Diese Information liefert die Methode MeasureSpec.getMode und sie kann sich für die Höhe und die Breite unterscheiden.
@Override
protected void onMeasure(int wMeasureSpec, int hMeasureSpec) {
    // berechne die Breite und Hoehe
    int w = getMeineBreite(wMeasureSpec);
    int h = getMeineHoehe(hMeasureSpec);
    // onMeasure ruft immer diese Methode
    setMeasuredDimension( w, h );
}
Die Methoden getMeineBreite und getMeineHoehe werden lokal implementiert, wobei sie die in den Spec übergebenen Vorgaben aus dem Layout auswerten.
    private int getMeineBreite(int measureSpec) {
    	int specMode = MeasureSpec.getMode(measureSpec);
    	int specSize = MeasureSpec.getSize(measureSpec);
    	int width = specSize;
    	if (specMode == MeasureSpec.AT_MOST) {
    	} else if (specMode == MeasureSpec.EXACTLY) {
    	} else if (specMode == MeasureSpec.UNSPECIFIED) {
            // hier eine Vorgabe machen, wenn Layout nichts spezifiziert
    	}
    	return width;
    }
    private int getMeineHoehe(int measureSpec) {
    	int specMode = MeasureSpec.getMode(measureSpec);
    	int specSize = MeasureSpec.getSize(measureSpec);
    	int height = specSize;
    	if (specMode == MeasureSpec.AT_MOST) {
    	} else if (specMode == MeasureSpec.EXACTLY) {
    	} else if (specMode == MeasureSpec.UNSPECIFIED) {
            // hier eine Vorgabe machen, wenn Layout nichts spezifiziert
    	}
    	return height;
    }