c't 9/2018
S. 180
Know-how
Android-Views programmieren
Aufmacherbild

App-Sichten

Eigene Bedienelemente für Android-Apps programmieren

Von Android-Apps erwartet man ein so intuitives Interface, dass man keine Anleitung braucht, um es zu benutzen. Die in Android integrierten Bedienelemente reichen dafür nicht immer aus. Wir zeigen am Beispiel eines Auswahlfelds für die Stärke eines zufällig erzeugten Passworts, wie Sie eigene Bedienelemente programmieren und was Sie dabei beachten sollten.

Lange Passwörter mit vielen Sonderzeichen sind sehr sicher; kurze Passwörter aus wenigen Zeichen wie eine vierstellige PIN ziemlich unsicher. Das weiß jeder. Aber wie sicher sind Passwörter mittlerer Länge, in denen nur Buchstaben und Ziffern vorkommen? Ab welcher Länge ist auch ein Passwort nur aus Kleinbuchstaben im grünen Bereich? Beim Bedienen einer Passwortmanager-App stellen sich dem Android-Nutzer solche Fragen – und er erwartet, dass das Interface ihm die richtige Antwort gibt. Dafür reichen die Bedienelemente, die Android von Haus aus mitbringt, oft nicht aus, sodass man eigene programmieren muss. Viele GUI-Toolkits nennen solche Bedienelemente Widgets, Android nennt sie Views.

Wir zeigen am Beispiel einer View zum Auswählen der Stärke generierter Passwörter, wie Sie eigene Bedienelemente schreiben und in eine App einbauen. Das Beispiel entstand als Teil von c’t-SESAM, unserem Passwort-Manager und Generator [1, 2]. Der „Smart-Selector“ unseres Beispiels besteht aus einem rechteckigen Feld, unterteilt in 7 Zeilen und 28 Spalten. Die Zeilen stehen jeweils für einen Satz an Zeichen, die im Passwort vorkommen können. In der untersten Zeile nur Ziffern, in der Zeile darüber nur Kleinbuchstaben, darüber Kleinbuchstaben und Ziffern und so weiter. Die oberste Zeile steht für Passwörter mit Ziffern, Buchstaben und Sonderzeichen. Die Spalten stehen für die Passwortlänge von 4 bis 32 Zeichen. Tippt man mit dem Finger auf eines dieser Felder, wählt man gleichzeitig den Zeichenvorrat und die Länge des Passworts aus.

c't-SESAM fragt zuerst nach einem Masterpasswort und entschlüsselt mit dem einen Schlüssel, aus dem es andere Passwörter generiert.

Damit Nutzer dabei nicht versehentlich unsichere Passwörter wählen, färbt der Smart-Selector jedes Feld entsprechend der Passwortstärke ein. Das Feld für ein Passwort mit 7 Ziffern färbt er rot, da sich ein solches Passwort noch mit relativ wenig Rechenleistung per Brute-Force ausprobieren ließe. Ein Passwort aus 22 Groß- und Kleinbuchstaben sowie Ziffern bekommt dagegen ein grünes Feld, da auch ein Supercomputer bereits Jahre daran herumrechnen müsste. Akzeptable, aber nicht übermäßig lange Passwörter bekommen die Farbe Gelb, da sie sowohl farblich als auch von der Sicherheit in der Mitte liegen.

Für diese Kombination aus Eingabefläche und Diagramm muss die View grafische Elemente auf den Bildschirm zeichnen. Das sollte dem Smartphone möglichst wenig Arbeit machen, da es den Bildschirminhalt ständig neu zeichnen muss und jede zusätzliche Berechnung beim Zeichnen den Akku leersaugt. Trotzdem muss der Inhalt stets aktuell bleiben, beispielsweise damit der Nutzer beim Berühren des Touchscreens sofort sieht, was er ausgewählt hat. Und wie üblich sollte das Objekt ein einfaches Interface zur Verfügung stellen, damit sich mit dem Smart-Selector genauso einfach programmieren lässt wie mit den von Android mitgelieferten Views.

View-Anatomie

Eigene Views leiten von der Klasse View aus dem Paket android.view ab. Die Klasse sollte die Methoden onLayout() und onMeasure() implementieren, damit das Layout, in das der Smart-Selector eingebunden wird, weiß, wie viel Platz das Bedienelement beanspruchen wird. onMeasure() verwendet dabei die Funktion resolveSizeAndState(), mit der View und Layout aushandeln, wie groß das Element tatsächlich wird. Die Funktion akzeptiert eine minimale Breite oder Höhe als ersten Parameter. Ob sie die Breite oder Höhe ausrechnet, hängt davon ab, ob man widthMeasureSpec oder heightMeasureSpec als zweiten Parameter übergibt. Diese beiden Variablen werden onMeasure() beim Aufruf übergeben. Um Padding muss sich die View selbst kümmern, sodass man die Abstände an dieser Stelle mit einrechnen muss. Die berechneten Maße legt setMeasuredDimension() fest:

@Override

protected void onMeasure(

int widthMeasureSpec,

int heightMeasureSpec) {

int minw = getPaddingLeft() +

getPaddingRight() +

getSuggestedMinimumWidth();

int w = resolveSizeAndState(minw,

widthMeasureSpec, 1);

int minh = getPaddingBottom() +

getPaddingTop() +

7 * tileHeight + 1;

int h = resolveSizeAndState(minh,

heightMeasureSpec, 1);

setMeasuredDimension(w, h);

}

Während onMeasure() dem Layout, in das die eigene View eingebunden wird, die Größe mitteilt, dient onLayout() dazu, das Layout innerhalb der View festzulegen. Eine View kann nämlich andere Views enthalten, sodass man sich aus bestehenden Bedienelementen sauber gekapselte Gruppen bauen kann. Der Smart-Selector braucht allerdings keine eingebetteten Views, sodass die Methode leer bleiben kann:

@Override

protected void onLayout(

boolean changed, int l,

int t, int r, int b) {

super.onLayout(changed, l, t, r, b);

}

App-Zeichenbrett

Implementiert eine View die Methode onDraw(), wird die jedes Mal aufgerufen, wenn Android den Bildschirm neu zeichnet. Das passiert ziemlich oft, sodass man in der Methode möglichst wenig berechnen sollte. Vor allem sollte man teure Operationen wie das Anfordern neuen Speichers beim Betriebssystem vermeiden. Deswegen übergibt Android das Zeichenflächen-Objekt canvas mit bereits reserviertem Speicher.

Android-Studio bringt ein Template für eigene Views mit. Das enthält unnötigen Beispielcode, der weg kann.

Auf das zeichnet man mit Paint-Objekten Formen und Text. Der Smart-Selector übermalt beispielsweise zuerst seinen Hintergrund (definiert durch das Rect-Objekt wholeCanvasRect) mit Schwarz (ein Paint-Objekt namens backgroundPaint):

canvas.drawRect(wholeCanvasRect,

backgroundPaint);

Zum Testen bietet es sich an, den View als einziges Element in eine leere Activity zu integrieren.

Damit der Smartphone-Akku möglichst lange hält, instanziiert man backgroundPaint nicht in onDraw(), sondern direkt beim Erzeugen als Membervariable der View:

backgroundPaint = new Paint(

Paint.ANTI_ALIAS_FLAG);

backgroundPaint.setColor(0xff000000);

backgroundPaint.setStyle(

Paint.Style.FILL);

Die zweite Membervariable wholeCanvasRect berechnet der Smart-Selector beim Festlegen seiner Größe:

int contentWidth = getWidth() -

getPaddingLeft() - getPaddingRight();

int contentHeight = getHeight() -

getPaddingTop() - getPaddingBottom();

wholeCanvasRect = new Rect(0, 0,

contentWidth, contentHeight);

Für die 196 farbigen Felder des Smart-Selector wäre es unvernünftig, 196 Paint-Objekte zu erzeugen. Wir haben daher nur die Farbwerte vorher berechnet und im zweidimensionalen Array colorMatrix gespeichert. In den for-Schleifen zum Zeichnen der Felder stellt der Code nur die Farbe eines einzigen Paint-Objekts um:

for (int i = 0;

i < maxLength-minLength+1; i++) {

for (int j = 0;

j < colorMatrix[0].length; j++) {

tilePaint.setColor(colorMatrix[i][

colorMatrix[0].length-1-j]);

canvas.drawRect(1+i*tileWidth,

1+j*tileHeight,

1+i*tileWidth+tileWidth-1,

1+j*tileHeight+tileHeight-1,

tilePaint);

}

}

Neben drawRect() bietet ein Canvas auch Methoden, um Linien, Kreise, Kreisteile, Ovale, Bitmaps und Text zu zeichnen. Beim Smart-Selector haben wir diese nicht gebraucht. Die englische Dokumentation erklärt aber recht anschaulich, wie sie funktionieren (siehe ct.de/yp6m).

Touch-Ereignisse

Um auf Berührung zu reagieren, muss die View die Methode onTouchEvent() implementieren. Sie bekommt ein MotionEvent-Objekt übergeben, das die Koordinaten der Berührung, aber auch den Typ enthält. Den erfährt man mit event.getActionMasked(). Eine switch-Anweisung unterscheidet, wie die View auf die Interaktionen reagiert:

@Override

public boolean onTouchEvent(

MotionEvent event) {

int action=event.getActionMasked();

switch(action) {

case MotionEvent.ACTION_DOWN:

case MotionEvent.

ACTION_POINTER_DOWN:

case MotionEvent.ACTION_MOVE:

float x = event.getX(0);

float y = event.getY(0);

selectTile(x, y);

postInvalidate();

performClick();

break;

case MotionEvent.ACTION_POINTER_UP:

case MotionEvent.ACTION_UP:

case MotionEvent.ACTION_CANCEL:

break;

}

return true;

}

ACTION_DOWN wird nur beim ersten Finger ausgelöst. Kommen bei Multitouch-Gesten weitere Finger dazu, lösen sie ACTION_POINTER_DOWN aus. ACTION_MOVE löst bei jeder Bewegung eines Fingers aus. Beim Abheben der Finger meldet Android erst ACTION_POINTER_UP-Ereignisse und erst beim letzten Finger ACTION_UP. ACTION_CANCEL passiert, wenn der Nutzer die Interaktion mit einem Hardwarebutton unterbricht.

Der Smart-Selector zeigt die gewählte Passwortstärke als weißes Feld an. Tippt man auf ein anderes der bunten Felder, wählt er die passende Stärke aus und meldet die an die App.

Bei neuen Berührungen und Bewegungen der Finger wählt der Smart-Selector stets das Feld unter dem ersten Finger aus, beim Abheben von Fingern tut er nichts. Android nummeriert die Finger anhand der zeitlichen Reihenfolge, sodass event.getX(0) die X-Koordinate des Fingers zurückgibt, der zuerst den Touchscreen berührt hat.

Für den Smart-Selector genügt die Position des ersten Fingers. Wer Views programmiert, die Multitouch-Gesten verstehen, sollte aber einen Blick auf den in Android integrierten GestureDetector werfen.

Für die Auswahl des Felds unter dem Finger reicht es, die Koordinaten durch die Größe zu teilen und abzurunden:

private void selectTile(float x,

float y) {

float tileWidth = (float)

(contentWidth-1) /

(maxLength-minLength+1);

selectedLength = (int)

(x / tileWidth);

selectedComplexity = (int)

(y / tileHeight);

}

onDraw() fragt selectedLength und selectedComplexity ab, um das gewählte Feld weiß statt bunt einzufärben. Damit Android den Bildschirm dafür auch neu zeichnet, ruft onTouchEvent() die Methode postInvalidate() auf.

private void calculateColorMatrix() {
  int[] complexity = new int[]{
    digitCount, lowerCount, upperCount,
    digitCount + lowerCount, lowerCount + upperCount,
    lowerCount + upperCount + digitCount,
    lowerCount + upperCount + digitCount + extraCount};
  colorMatrix = new int[maxLength-minLength+1][complexity.length];
  for (int i = 0; i < maxLength-minLength+1; i++) {
    for (int j = 0; j < complexity.length; j++) {
      double s = 20;
      double tianhe2_years = (Math.pow(complexity[j],
                              (i+minLength))*0.4/3120000)/(60*60*24*365);
      double stren_r = 1-s/(s+Math.log(tianhe2_years+1)/Math.log(50));
      double stren_g = 1-s/(s+Math.log(tianhe2_years+1)/Math.log(1.2));
      int redValue = (int) Math.round(215*(1-stren_r));
      int greenValue = (int) Math.round(190*stren_g);
      colorMatrix[i][j] = Color.argb(0xff, redValue, greenValue, 0);
    }
  }
}
Für die Farbwerte schätzt der Smart-Selector die Rechenzeit für einen Brute-Force-Angriff auf dem Supercomputer Tianhe-2, dem aktuell zweitschnellsten Rechner der Welt. Die berechneten Werte sind 32-Bit-Integer, bei denen je ein Byte für Transparenz und die drei Farbwerte steht.

Auswahl-Signal

Die Auswahl eines Felds ist das einzige Ereignis, das der Smart-Selector an den Rest der App melden soll. Dafür definiert die View-Klasse ein interface:

public interface

OnStrengthSelectedEventListener {

void onStrengthSelected(int length,

int complexity);

}

Dieses Interface kann danach jedes Objekt der App implementieren, um die ausgewählte Länge und Passwortkomplexität zu erfahren. Die View speichert die Implementierung in der Variable StrengthSelectedListener. Um sie zu setzen, bietet die View eine Methode an:

public void

setOnStrengthSelectedEventListener(

OnStrengthSelectedEventListener

eventListener) {

this.StrengthSelectedListener =

eventListener;

}

Über die Variable löst man das Ereignis an beliebigen Stellen aus, beispielsweise in selectTile():

if (StrengthSelectedListener!=null) {

StrengthSelectedListener.

onStrengthSelected(

minLength + selectedLength,

colorMatrix[0].length - 1 -

selectedComplexity);

}

Die Überprüfung, ob die Variable null ist, darf man dabei keinesfalls weglassen, da eine View auch dann funktionieren sollte, wenn die App nicht alle ihre Signale empfängt.

Activity-Einbau

Eigene View-Klassen wie den Smart-Selector bindet man anschließend wie jedes andere Bedienelement ins XML-Layout ein:

<de.pinyto.ctSESAM.SmartSelector

android:layout_width="match_parent"

android:layout_height="wrap_content"

android:id="@+id/smartSelector" />

Im Code implementiert man nur noch den EventListener, um auf Eingaben zu reagieren:

smartSelector = (SmartSelector)

fLayout.findViewById(

R.id.smartSelector);

smartSelector.

setOnStrengthSelectedEventListener(

new SmartSelector.

OnStrengthSelectedEventListener() {

@Override

public void onStrengthSelected(

int length, int complexity) {

// hier PasswordSetting anpassen

}

});

c’t-SESAM-Android

In der c’t-SESAM-App zeigt der Smart-Selector die Passwortstärke und erleichtert die Eingabe bei neuen Passwörtern. Die Klasse kapselt ganz im Sinne objektorientierter Programmierung Anzeige und Interaktion in einer Komponente. Sollten Sie diese View mal in einer eigenen App brauchen, kopieren Sie einfach SmartSelector.java in Ihr Projekt und können sie sofort wie andere Views benutzen. Das dürfen Sie auch, da c’t-SESAM unter der GPL steht. Den Quellcode der kompletten App finden Sie im Repository auf GitHub unter ct.de/yp6m.

Die App ist kompatibel zu Qt-SESAM, unserem Passwortmanager für den Desktop (siehe ct.de/yp6m). Mit unserer ebenfalls frei verfügbaren Sync-App können Sie die Passwort-Einstellungen zwischen Smartphone und Desktop übertragen. Sie brauchen dafür nur unseren c’t-SESAM Sync-Server. Sobald die Sync-App installiert ist, zeigt die c’t-SESAM-App in der Titelleiste einen Knopf zum Synchronisieren an. Die Einstellungen für Ihren Sync-Server tragen Sie im Interface der c’t-SESAM-Sync-App ein. (pmk@ct.de)