21.06 Ein Low-Level Informations Bildschirm

Als Entwickler möchte man meistens auch weitere Informationen wie bspw. Verwendung der Anwendung, Tastenbelegung, Copyright, … in die eigene Applikation integrieren. In diesem Kapitel lernen Sie eine von Canvas abgeleitete Klasse kennen, in welcher Sie diese Informationen ablegen, und dem User anzeigen können. Da die Programmierung auf Low-Level-Ebene geschieht, kann dieser InfoScreen perfekt an das Design der Anwendung angepasst werden.

Ziel dieses Kapitels ist es, eine Klasse zu schreiben, die folgendes darstellen kann:

  • Mehrzeilige Anzeige von langem Text
  • Unterteilung in einzelne Abschnitte
  • Überschriften für die einzelnen Abschnitte
  • Darstellung eines vertikalen Scrollbalkens, falls der Inhalt nicht komplett auf den Bildschirm passt
  • Steuerung über Handytastatur und Touchscreen

Info Screen

Vorbereitungen

Zuerst muss selbstverständlich eine von Canvas abgeleitete Klasse InfoScreen erzeugt werden.

public class InfoScreen extends Canvas {
// ...
}

Es werden Attribute für die Hintergrundfarbe, die Hintergrundfarbe der Überschriften, die Schriftfarbe, die Schriftfarbe der Überschriften, die Farbe des Scrollbalkens, die Farbe für den Hintergrund des Scrollbalkens, die aktuelle Scrollposition (0 = ganz oben, -x = die oberen x Pixel der Anzeige werden nicht mehr angezeigt (scroll nach unten)), die maximale Höhe des Scrollbalkens, die letzte Druckposition für das Scrollen via Touchscreen, den zu verwendenden Font, die Überschriften und die Texte der einzelnen Abschnitte benötigt. Die Überschriften liegen hierbei in einem String-Array vor, die Abschnitte in einem 2D-String-Array, wobei die erste Dimension dem Abschnitt entspricht, und die zweite Dimension einer einzelnen Zeile im Abschnitt.

private String[] heads;
private String[][] txts;
private Font font;

private int backgroundColor;
private int backgroundHeadColor;
private int foregroundColor;
private int foregroundHeadColor;
private int scrollColor;
private int scrollBarColor;

private int maxHeight; // Maximale Höhe des Scrollbalkens
private int curY; // Aktuelle Scrollposition

private int lastY; // letzte Druckposition

curY, lastY und maxHeight werden von der Klasse selbst bestimmt, der Font, die Überschriften und die Texte der Abschnitte werden einmalig im Konstruktor übergeben. Die restlichen Attribute können via Getter und Setter abgefragt bzw. manipuliert werden:

public int getBackgroundColor() {
  return backgroundColor;
}
public void setBackgroundColor(int backgroundColor) {
  this.backgroundColor = backgroundColor;
}
public int getBackgroundHeadColor() {
  return backgroundHeadColor;
}
public void setBackgroundHeadColor(int backgroundHeadColor) {
  this.backgroundHeadColor = backgroundHeadColor;
}
public int getForegroundColor() {
  return foregroundColor;
}
public void setForegroundColor(int foregroundColor) {
  this.foregroundColor = foregroundColor;
}
public int getForegroundHeadColor() {
  return foregroundHeadColor;
}
public void setForegroundHeadColor(int foregroundHeadColor) {
  this.foregroundHeadColor = foregroundHeadColor;
}
public int getScrollBarColor() {
  return scrollBarColor;
}
public void setScrollBarColor(int scrollBarColor) {
  this.scrollBarColor = scrollBarColor;
}
public int getScrollColor() {
  return scrollColor;
}
public void setScrollColor(int scrollColor) {
  this.scrollColor = scrollColor;
}

Der Konstruktor

Zur Initialisierung des InfoScreens werden zwei String-Arrays benötigt, wovon eines die Überschriften enthält, und das Andere die Texte/Abschnitte zu den Überschriften. Diese werden zusammen mit einem Font, der zur Anzeige der Texte verwendet werden soll, im Konstruktor übergeben.

public InfoScreen(String[] heads, String[] txts, Font font) {
// ...
}

Da jeder Abschnitt einer Überschrift zugeordnet ist, und jede Überschrift auch zugehörigen Text beinhalten muss, müssen diese beiden Arrays auch gleich lang sein. Ansonsten wird eine Exception geworfen:

if (heads.length != txts.length) {
  throw new IllegalArgumentException("heads.length and txts.length must be equal");
}

Anschließend können die Überschriften und der Font direkt den jeweiligen Attributen zugewiesen werden. Die Abschnitte müssen jedoch zeilenweise in ein 2D-String-Array umgewandelt werden. Dabei entspricht eine Zeile genau dem Text, der mit dem gegebenen Font gerade so auf die definierte Bildschirmbreite passt. Als Bildschirmbreite wird hier die Breite des Canvas (getWidth()) - 15 veranschlagt. Die 15 setzt sich wie folgt zusammen: fünf Pixel Abstand zum rechten Rand, fünf Pixel Abstand zum linken Rand und fünf Pixel für die Scrollbar. Natürlich kann dieser Wert je nach Anforderung individuell angepasst werden.

Um nun einen String zeilenweise für eine bestimmte Breite zu trennen, kann eine aus dem Kapitel 21.05.02 Texte und Bilder zeichnen bereits bekannte Methode verwendet werden. Die genaue Funktionsweise lesen Sie bitte in diesem Kapitel nach, hier nur noch einmal zur Wiederholung der Code:

package de.jbb.info;

import javax.microedition.lcdui.Font;

public class TextTools {

  public static String[] getViewableTextLines(String text, Font f, int width) {

    String[] ret = new String[f.stringWidth(text) / (width - f.stringWidth("012345")) + 1];
    if (ret.length == 1) {
      return new String[]{text};
    }
    int arPos = 0;
    for (int curPos = 0, lastCut = 0, lastAcceptPos = 0, lastWord = 0;;) {
      if (arPos >= ret.length) {
        String[] tmp = new String[ret.length + 1];
        System.arraycopy(ret, 0, tmp, 0, ret.length);
        ret = tmp;
      }
      lastWord = curPos;
      curPos = text.indexOf(' ', curPos + 1);
      if (curPos < 0) {
        if (f.substringWidth(text, lastCut, text.length() - lastCut) < width
            || lastCut == lastAcceptPos) {
          ret[arPos] = text.substring(lastCut);
          break;
        }
        else {
          ret[arPos++] = text.substring(lastCut, lastAcceptPos);
          lastCut = lastAcceptPos;
          curPos = lastCut;
        }
      }
      else {
        if (f.substringWidth(text, lastCut, curPos - lastCut) < width) {
          lastAcceptPos = curPos + 1;
        }
        else if (lastAcceptPos != lastCut) {
          ret[arPos++] = text.substring(lastCut, lastAcceptPos);
          lastCut = lastAcceptPos;
        }
        else {
          ret[arPos++] = text.substring(lastCut, lastWord);
          curPos = lastWord;
          lastAcceptPos = curPos + 1;
          lastCut = lastAcceptPos;
        }
      }
    }
    if (arPos + 1 != ret.length) {
      String[] tmp = new String[arPos + 1];
      System.arraycopy(ret, 0, tmp, 0, tmp.length);
      ret = tmp;
    }
    return ret;
  }
}

Damit die maximale Höhe der Scrollbar berechnet werden kann, muss noch die Zeilenanzahl mitgezählt werden.

this.font = font;
this.heads = heads;
this.txts = new String[txts.length][0];
int txtLineCount = 0;
for (int i = 0; i < txts.length; i++) {
  this.txts[i] = TextTools.getViewableTextLines(txts[i], font, getWidth() - 15);
  txtLineCount += this.txts[i].length;
}

Die maximale Höhe der Scrollbar berechnet sich dann wie folgt:

(Höhe der Schriftart + Zeilenabstand) * (Anzahl der Überschriften + Anzahl der Zeilen)

this.maxHeight = (font.getHeight() + 2) * (heads.length + txtLineCount);

Der Abstand zwischen den Zeilen wird mit zwei Pixeln angenommen. Auch dieser Wert kann individuell angepasst werden.

Die Zeichenfunktion

Jetzt, nachdem alle Vorbereitungen getroffen sind, können die Daten gezeichnet werden.

Zuerst wird eine Variable angelegt. Diese speichert die y-Position für die aktuelle Zeile. Folglich wird diese mit dem Wert curY initialisiert.

protected void paint(Graphics g) {

  int curHeight = this.curY;
// ...
}

Im nächsten Schritt wird die Schriftart gesetzt und der Hintergrund gezeichnet:

g.setColor(this.backgroundColor);
g.fillRect(0, 0, getWidth(), getHeight());
g.setFont(font);

Das Zeichnen einer Überschrift und eines Abschnittes wird jeweils in eine separate Methode ausgelagert, welche zudem die y-Position der nächsten Zeile zurückgibt. Es genügt also eine einfache Schleife, in welcher den entsprechenden Methoden der zu schreibende Inhalt, das Graphics-Objekt und die aktuelle Zeilenposition übergeben wird:

for (int i = 0; i < this.heads.length; i++) {
  curHeight = drawHeadline(g, this.heads[i], curHeight);
  curHeight = drawText(g, this.txts[i], curHeight);
}

In drawHeadline wird zuerst ein Rechteck für den Hintergrund der Überschrift in der entsprechenden Farbe gezeichnet. Anschließend wird die Farbe für die Schrift der Überschrift gesetzt, und diese wie ein gewöhnlicher String gezeichnet. Zum Abschluss muss noch die y-Position der nächsten Zeile berechnet, und zurückgegeben werden.

private int drawHeadline(Graphics g, String head, int curHeight) {

  g.setColor(this.backgroundHeadColor);
  g.fillRect(0, curHeight, getWidth() - 5, g.getFont().getHeight() + 2);
  g.setColor(this.foregroundHeadColor);
  g.drawString(head, 5, curHeight, Graphics.LEFT | Graphics.TOP);
  curHeight += g.getFont().getHeight() + 2;
  return curHeight;
}

Ähnlich sieht es bei der drawText-Methode aus. Nachdem die Textfarbe gesetzt wurde, werden die einzelnen Zeilen in einer Schleife gezeichnet und die Variable mit der aktuellen y-Position jeweils entsprechend erhöht, bevor sie am Methodenende zurückgegeben wird.

private int drawText(Graphics g, String[] text, int curHeight) {

  g.setColor(this.foregroundColor);
  for (int i = 0; i < text.length; i++) {
    g.drawString(text[i], 5, curHeight, Graphics.LEFT | Graphics.TOP);
    curHeight += g.getFont().getHeight() + 2;
  }
  return curHeight;
}

Aber zurück in die paint-Methode. Um das Bild zu vervollständigen, ist es notwendig, noch den Scrollbalken zu zeichnen. Dieser muss jedoch nur gezeichnet werden, wenn die maximale Größe des Scrollbalkens über der Höhe des Canvas liegt:

if (this.maxHeight > getHeight()) {
  // draw scroll
}

Ist dies der Fall, wird zuerst der Hintergrund des Scrollbalkens über die gesamte Höhe gezeichnet.

g.setColor(this.scrollColor);
g.fillRect(getWidth() - 5, 0, 4, getHeight());

Hier treffen Sie wieder auf den Abstand der Scrollbar zum rechten Bildschirmrand (5) und der eigentlichen Breite der Scrollbar (4). Diese Werte können selbstverständlich auch wieder individuell angepasst werden.

Die Bestimmung, an welcher Stelle die Scrollbar gezeichnet werden muss, und wie hoch diese ist, gestaltet sich ein wenig komplexer.

  • Der Abstand zum rechten Rand ist identisch mit dem Hintergrund der Scrollbar, hier können also auch wieder die obigen Werte (getWidth() - 5 und 4) verwendet werden.
  • Um die Start-Y-Koordinate zu bestimmen müssen Sie zuerst errechnen, wie viel Prozent die maximale Höhe (da wir mit Ganzzahlen rechnen: multipliziert mit 100) der Scrollbar größer ist, als die Höhe des Canvas. Dieses Ergebnis dient dann als Divisor für die negierte (also positive) aktuelle Position des Scrollbalkens (ebenfalls multipliziert mit 100).
  • Die Höhe der Scrollbar lässt sich ähnlich berechnen. Es wird wiederum die Prozentzahl benötigt, um die die maximale Höhe der Scrollbar größer ist, als das Canvas. Diese wird diesmal aber als Divisor auf die Höhe des Canvas angewandt.
g.setColor(this.scrollBarColor);
g.fillRect(
  getWidth() - 5, 
  (-this.curY * 100) / ((this.maxHeight * 100) / getHeight()), 
  4, 
  (getHeight() * 100) / ((this.maxHeight * 100) / getHeight()));

Benutzereingaben

Die Zeichnung ist nun fertig implementiert, jetzt muss der Benutzer nur noch durch den Text scrollen können. Für das Scrollen via Handytastatur wird die Methode doStep(int keyCode) implementiert, die anhand des keyCodes, der von der Methode keyPressed oder keyReleased übergeben wird, die Scrollbar entsprechend verschiebt.

protected void keyPressed(int keyCode) {
  doStep(keyCode);
}

protected void keyRepeated(int keyCode) {
  doStep(keyCode);
}

private void doStep(int keyCode) {
// ...
}

In der doStep-Methode wird zuerst überprüft, ob eine Scrollbar überhaupt angezeigt wird. Falls nicht, ist ein scrollen erst gar nicht möglich.

if (maxHeight > getHeight()) {
  // es kann gescrollt werden
}

Falls auf die zwei oder nach oben gedrückt wurde, wird die aktuelle Position (curY) der Ansicht um die Höhe der verwendeten Schrift (auch diese Größe kann natürlich auch nach Belieben variieren) nach oben geschoben. Natürlich sollten Sie im Anschluss überprüfen, ob die Ansicht hierdurch nicht über den oberen Rand hinaus geschoben wurde. Falls dies der Fall ist, sollten Sie die aktuelle Position auf ihren Maximalwert (0) setzen. Selbiges gilt natürlich umgekehrt auch für die Verschiebung der Scrollbar nach unten. Dieser Vorgang wird durch die Tasten acht oder nach unten angestoßen. Nach beiden Vorgängen muss die Oberfläche selbstverständlich aktualisiert, also neugezeichnet werden.

if (keyCode == KEY_NUM2 || getGameAction(keyCode) == UP) {
  this.curY += font.getHeight();
  if (this.curY > 0) {
    this.curY = 0;
  }
  repaint();
}
else if (keyCode == KEY_NUM8 || getGameAction(keyCode) == DOWN) {
  this.curY -= font.getHeight();
  if (this.curY < getHeight() - this.maxHeight) {
    this.curY = getHeight() - this.maxHeight;
  }
  repaint();
}

Um eine Steuerung via Touchscreen zu realisieren, wird u. a. das Objektattribut lastY verwendet. Jedes Mal, wenn der Anwender auf den Touchscreen tippt, wird lastY mit der Fingerposition des Anwenders abgeglichen - von dieser Position aus finden die weiteren Berechnungen statt.

protected void pointerPressed(int x, int y) {
  this.lastY = y;
}

Die eigentliche Verschiebung findet dann statt, sobald der Finger auf dem Touchscreen verschoben, also pointerDragged aufgerufen wird. Hierbei sollte zuerst überprüft werden, ob sich die Y-Position überhaupt im Vergleich zur zuletzt registrierten Position verändert hat:

protected void pointerDragged(int x, int y) {
  if (this.lastY != y) {
    // Es wurde gescrollt
  }
}

Die neue Y-Position (curY) der Anzeige ergibt sich dann aus der alten Y-Position der Anzeige, addiert zur Differenz der aktuellen Y-Position des Fingers (y) und der alten Y-Position des Fingers (lastY).

this.curY += y - this.lastY;

Auch hier muss noch überprüft werden, ob die neue Y-Position der Anzeige nicht einen Grenzbereich überschreitet. Nach dieser Überprüfung kann die alte Y-Position des Fingers durch die neue ersetzt, und repaint() aufgerufen werden.

if (this.curY > 0) {
  this.curY = 0;
}
else if (this.curY < getHeight() - this.maxHeight) {
  this.curY = getHeight() - this.maxHeight;
}
this.lastY = y;
repaint();

Fertig ist der InfoScreen.

Auf der nächsten Seite finden Sie eine Zusammenfassung des Codes.

Schreibe einen Kommentar