Suche
Android am Arm

Android Wear Watchfaces selber programmieren

Peter Friese

© Shutterstock / tanuha2001

Egal ob Pebble, Android Wear oder Apple Watch: Nichts regt die Kreativität der Entwicklergemeinde so an wie die Aussicht auf die Entwicklung eigener Watchfaces.

Die Anzeige der aktuellen Zeit (und gegebenenfalls des aktuellen Datums) ist eine der Hauptfunktionen einer Uhr und war somit für lange Zeit der eigentlich Grund, warum wir uns ein Stück Metall (oder Plastik) mit einem Lederband um den Arm geschnallt haben. Die sprunghafte Verbreitung von Smartphones, die so viel mehr können als nur zu telefonieren, hat allerdings eine Phase eingeleitet, in der deutlich weniger Menschen als zuvor eine Uhr tragen. Wozu eine Uhr tragen, wenn man durch einen Blick auf den Lockscreen des Smartphones schnell die aktuelle Zeit sehen kann? Smartphones sind eben nicht nur „a touchscreen musicplayer, a phone, and an internet communication device“, sondern so viel mehr. Die Bedeutung von Uhren als Zeitmessinstrumente hat damit spürbar abgenommen, für viele ist das Tragen einer Uhr zu einem Fashion Statement geworden.

Doch was, wenn man Uhren auf die gleiche Weise zu multifunktionalen Informationsgeräten umgestalten würde, wie dies für Mobiltelefone geschehen ist? Die Integration von Sensoren wie GPS, Schrittsensor, Magnetometer, Herzfrequenzsensor und einem Mikrofon in Verbindung mit der Tatsache, dass eine Uhr direkt am Körper getragen wird, macht Smartwatches zu mobilen Sensoren-Arrays und ermöglicht Anwendungsfälle, die mit Smartphones alleine nur umständlich umzusetzen wären.

Android Wear ist seit seiner Einführung vor etwas mehr als einem Jahr kontinuierlich weiterentwickelt worden. War es in der allerersten veröffentlichten Vorabversion gerade einmal möglich, Benachrichtigungen von einem Smartphone auf die gekoppelte Smartwatch zu senden, so wurde mit dem offiziellen Release die Möglichkeit eröffnet, Apps zu schreiben, die direkt auf der Smartwatch laufen und mittels des Wearable Data Layer APIs mit der Außenwelt kommunizieren können. Vor einigen Monaten wurde dann ein offizielles Watchface API veröffentlicht, das die Entwicklung von energieeffizienten und ressourcenschonenden Watchfaces ermöglicht. Pünktlich zur Google I/O 2015 wurde dann mit _Always On_  ein Feature eingeführt, das die Entwicklung von Apps mit länger laufenden Interaktionen (z. B. in Fitness- oder Shopping-Apps) erleichtert. Android Wear bietet somit eine Vielzahl von Möglichkeiten, Informationen darzustellen (Notifications, Microapps, Watchfaces).

Dieser Artikel ist Teil einer Serie von Artikeln, die sich mit den vielfältigen Möglichkeiten von Android Wear beschäftigen und anhand von Beispielen zeigen, wie diese in eigenen Apps eingesetzt werden können. Im vorliegenden Artikel beschäftigen wir uns mit der Entwicklung von Watchfaces. Neben einer Einführung in das Watchface API und einigen Tipps und Tricks zur Entwicklung von energieeffizienten Watchfaces gehen wir auch auf den kreativen Prozess ein. In den nachfolgenden Artikeln der Serie beschäftigen wir uns dann mit der Entwicklung von Apps für Android Wear sowie dem Zugriff auf die unterschiedlichen Sensoren von Android Wearables und die Möglichkeiten der Spracheingabe.

Architektur

Bevor wir uns Gedanken über Gestaltung und Implementierung eines eigenen Watchface machen, werfen wir zunächst einen Blick auf die Architektur von Android Wear Watchfaces. Neben einem besseren Verständnis des später folgenden Quellcodes ermöglicht uns dies, die Möglichkeiten des APIs und die daraus resultierenden Rahmenbedingungen für das Design einzuschätzen.

Android Wear Watchfaces werden als Service implementiert und registriert. Zur Implementierung eines Watchface müssen die Klassen CanvasWatchfaceService und die darin enthaltene innere Klasse CanvasWatchfaceService.Engine abgeleitet und implementiert werden (Abb. 1).

Wenn ein Watchface aktiv ist, ruft das System die Methoden der von CanvasWatchfaceService.Engine abgeleiteten Klasse auf, wenn sich z. B. die Zeit oder der Zustand der Uhr geändert hat (etwa wenn sie vom Interactive Mode in den Ambient Mode gewechselt hat).

Abb. 1: Architektur eines Android Wear Watchfaces

Abb. 1: Architektur eines Android Wear Watchfaces

Gestaltung eines eigenen Watchface

Vor der Implementierung eines Watchface sollte man sich Gedanken über die kreative Gestaltung des Zifferblatts sowie die Informationsarchitektur machen: Welche Informationen sollen dargestellt werden, in welcher Beziehung stehen sie zueinander? Wie soll das Ziffernblatt gestaltet sein – soll beispielsweise ein klassisches Uhrenmodell nachgeahmt oder das Branding einer bekannten Marke umgesetzt werden? Es versteht sich von selbst, dass die Rechte der entsprechenden Markeninhaber beachtet werden müssen. Für die Entwicklung eigener Ideen empfehlen sich analoge Werkzeuge wie Papier und Stift ebenso wie digitale Tools wie Photoshop, Illustrator oder Sketch. Photoshop-Benutzer können die kostenlose Android-App Pixl Preview verwenden, um bereits während des Designs eine Livevorschau auf der Uhr begutachten zu können. Unser Watchface soll neben den Zeigern für Stunden, Minuten und Sekunden auch das aktuelle Datum anzeigen – in Abbildung 2 ist das Ergebnis zu sehen.

Abb. 2: Das fertige Watchface

Abb. 2: Das fertige Watchface

Designrichtlinien für Android Wear

Android Wear Devices gibt es in vielen verschiedenen Formen und Technologien. Beim Design eines Watchface müssen daher ein paar Dinge beachtet werden:

  • Bildschirmform: Es gibt nicht nur Android Wear Devices mit runden oder eckigen Bildschirmen – einige runde Bildschirme haben ein so genanntes Kinn am unteren Bildschirmrand. Bekanntestes Beispiel für diese Bauform ist die Moto 360.
  • Active vs. Ambient Mode unterstützen: Im aktiven Zustand können die Anzeigeelemente des Watchface in voller Farbauflösung dargestellt und animiert werden. Im Ambient Mode wird die Leuchtkraft des Displays verringert, und Watchfaces sollten in diesem Modus auf die Verwendung von Farbe verzichten. Standardmäßig erhalten Watchfaces in beiden Modi mindestens einmal pro Minute die Gelegenheit, den Bildschirminhalt zu aktualisieren. Wenn es erforderlich ist, den Bildschirminhalt häufiger zu aktualisieren (z. B., um einen Sekundenzeiger darzustellen), muss man selbst einen entsprechenden Timer konfigurieren. Der Ambient Mode wird von Android Wear verwendet, um Energie zu sparen und trotzdem die wichtigsten Informationen darzustellen – man sollte in diesem Modus also tunlichst auf häufige Aktualisierungen verzichten und nicht benötigte Bildschirmelemente ganz ausblenden.
  • In den verschiedenen Android-Wear-Uhren kommen unterschiedliche Displaytechnologien mit unterschiedlichen Energieverbrauchscharakteristiken zum Einsatz. Generell gilt die Empfehlung, im Ambient Mode auf die Verwendung von Farben zu verzichten und dafür zu sorgen, dass 90 % oder mehr des Bildschirms schwarz sind. Auf Uhren mit OLEDs verschiebt Android Wear den Bildschirminhalt in regelmäßigen Abständen um ein paar Pixel, um ein Einbrennen des Bilds zu verhindern. Dies gilt es beim Design zu beachten: Gegebenenfalls sollte man ein paar Pixel Abstand vom Rand des Displays einplanen. Einige Geräte versetzen das Display im Ambient Mode in einen so genannten „Low-bit“-Zustand, um durch die Reduktion des Farbraums Energie zu sparen. Auf solchen Geräten sollte im Ambient Mode das Anti-Aliasing abgeschaltet werden. Ebenso sollte auf die Verwendung von ausgefüllten Flächen im Ambient Mode verzichtet werden.
  • Platz für System-UI-Elemente vorsehen: Android Wear verwendet einige Icons und Labels, um dem Benutzer wichtige Zustände des Systems zu signalisieren, unter anderem für den Ladezustand der Batterie, den Airplane-Modus sowie den „OK Google“ Hotword Prompt. Nicht zu vergessen die am unteren Bildschirmrand eingeblendeten Benachrichtigungskarten. Die Positionierung bzw. farbige Hinterlegung dieser Elemente kann über entsprechende Methodenaufrufe beeinflusst werden, sodass sie möglichst wenig mit den Elementen des Zifferblatts im Konflikt stehen.

Vom Design zur Implementierung

Da die Darstellung einiger Watchface-Elemente vom aktuellen Status der Uhr abhängig sind, hat es sich als hilfreich herausgestellt, die einzelnen Elemente bei der Implementierung des Watchfaces separat zu betrachten. Für unser Watchface ergeben sich die folgenden Hauptkomponenten:

  • Die Hintergrundgrafik. Diese wird nur im interaktiven Modus dargestellt und im Ambient-Modus ausgeblendet, um möglichst viele schwarze Pixel (und somit ein möglichst geringe Energieaufnahme des Displays) zu erzielen.
  • Die Stunden- und Minutenmarkierungen auf dem Rand des Zifferblatts. Diese müssen einerseits an die Bildschirmgeometrie (rechteckig bzw. rund oder rund mit Kinn) angepasst werden, andererseits kann es sinnvoll sein, die Anzeige im Ambient-Modus zu vereinfachen und nur die Stundenmarkierungen darzustellen.
  • Die Zeiger: Stunden- und Minutenzeiger werden sowohl im Interactive- als auch im Ambient-Modus dargestellt, der Sekundenzeiger wird nur im Interactive-Modus angezeigt. Auch hier hat es sich als sinnvoll erwiesen, die Darstellung im Ambient-Modus zu vereinfachen. Im Interactive-Modus ist eine grafisch aufwändige Darstellung der Zeiger (mit Verzierungen und Schatten) sinnvoll, im Ambient-Modus dagegen empfiehlt es sich, nur den Umriss der Zeiger darzustellen.
  • Auch die Datumsanzeige kann im Interactive-Modus aufwändiger dargestellt werden, im Ambient-Modus reicht es aus, einen dezenten Rahmen um das Datum zu zeichnen.

Im weiteren Verlauf gehen wir auf einige Aspekte des Quellcodes des Beispiel-Watchface ein, um die o. g. Techniken exemplarisch zu beleuchten.

Lifecycle eines Watchface

Android Wear Watchfaces haben sechs wichtige Lifecycle-Methoden, die sich in drei Bereiche gruppieren lassen: das initiale Set-up eines Watchface findet in onCreate statt – hier können Ressourcen geladen und Paint-Objekte konfiguriert werden, die später zum Zeichnen der Zifferblattelemente verwendet werden. Änderungen am Zustand eines Watchface werden über die Methoden onPropertiesChanged, onAmbientModeChanged und onVisibilityChanged kommuniziert. Die dritte Gruppe schließlich dient der Aktualisierung des Zifferblatts selbst: onDraw wird aufgerufen, um das Zifferblatt zu zeichnen, onTimeTick, um dem Watchface mitzuteilen, dass sich die Zeit geändert hat. Schauen wir uns im Folgenden die einzelnen Methoden an.

Set-up

Die Methode onCreate() einer WatchFaceEngine wird ganz am Anfang des Lebenszyklus eines Ziffernblatts aufgerufen und dient der Initialisierung der benötigten Ressourcen sowie der Konfiguration der WatchfaceEngine. Hier sollten eventuell verwendete Hintergrundgrafiken geladen sowie die Paint-Objekte für die einzelnen Zifferblattelemente (Zeiger, Datumsanzeige, Zifferblattmarkierungen) initialisiert werden. Android Wear verfügt über etliche System-UI-Elemente, die es auf dem Zifferblatt darstellt, wie z. B. die Ladezustandsanzeige, eine digitale Zeitanzeige und die so genannte Peek Card. Dabei handelt es sich um die zuletzt empfangene Benachrichtigungskarte, die halb angeschnitten am unteren Rand des Bildschirms angezeigt wird. Die Positionierung und Darstellung dieser UI-Elemente kann mithilfe eines WatchfaceStyles konfiguriert werden, um so besser mit dem Zifferblatt zu harmonieren. Für unser Zifferblatt ist es zum Beispiel nicht sinnvoll, die digitale Zeitanzeige darzustellen. Der in Listing 1 dargestellte Ausschnitt aus der onCreate()-Methode zeigt, dass wir darüber hinaus dafür sorgen, dass nur wichtige Benachrichtigungen dargestellt werden, und zwar in einer verkürzten Fassung der Peek Card.

setWatchFaceStyle(
    new WatchFaceStyle.Builder(ClassicWatchFace.this)
      .setCardPeekMode(WatchFaceStyle.PEEK_MODE_SHORT)
      .setBackgroundVisibility(
        WatchFaceStyle.BACKGROUND_VISIBILITY_INTERRUPTIVE)
      .setShowSystemUiTime(false)
    .build());
  ... 
  mMinuteTickPaint = new Paint();
  mMinuteTickPaint.setColor(r.getColor(R.color.analog_ticks));
  mMinuteTickPaint.setStrokeWidth(
    r.getDimension(R.dimen.analog_ticks_minute_stroke));
  mMinuteTickPaint.setAntiAlias(true);
  mMinuteTickPaint.setStrokeCap(Paint.Cap.SQUARE)

Exemplarisch ist auch die Konfiguration des Paint-Objekts für den Minutenzeiger dargestellt. Hier werden unter anderem die Strichdicke und die Farbe festgelegt.

Displaytechnologie

Die in Android-Wear-Uhren eingesetzte Displaytechnologie unterscheidet sich teilweise erheblich zwischen den einzelnen Uhrenmodellen. Hersteller haben so die Möglichkeit, unterschiedliche Aspekte ihres Uhrenmodells in den Vordergrund zu stellen, z. B. Brillanz, Energieaufnahme, Ablesbarkeit bei Sonnenlicht. Diese Faktoren müssen bei der Entwicklung von Zifferblättern berücksichtigt werden. Android Wear ruft zu Beginn des Lebenszyklus eines Zifferblatts die Methode onPropertiesChanged(Bundle properties) auf. Die unterschiedlichen Aspekte des verwendeten Displays lassen sich mittels der Konstanten PROPERTY_BURN_IN_PROTECTION und PROPERTY_LOW_BIT_AMBIENT aus dem übergebenen Bundle ermitteln (Listing 2).

@Override
  public void onPropertiesChanged(Bundle properties) {
    super.onPropertiesChanged(properties);
    mLowBitAmbient = 
      properties.getBoolean(PROPERTY_LOW_BIT_AMBIENT, false);
    mBurnInProtection =
      properties.getBoolean(PROPERTY_BURN_IN_PROTECTION, false);
  }

Die Unterschiede wirken sich vornehmlich im Ambient-Modus aus:

Displays mit Low-Bit-Modus verwenden weniger Bits für die Darstellung von Farben im Ambient-Modus, folglich sollte man im Ambient-Modus auf die Verwendung von Graustufen und Anti-Aliasing verzichten.

Displays mit Burn-in Protection verschieben den Bildschirminhalt in regelmäßigen Abständen um ein paar Pixel. Es empfiehlt daher, einen gewissen Abstand zwischen dem Bildschirmrand und dem Zifferblatt zu lassen und auf die Verwendung von ausgefüllten Flächen zu verzichten.

Statusänderungen

Der Übergang zwischen Interactive-Modus und Ambient-Modus wird über die Methode onAmbientModeChanged(boolean inAmbientMode) signalisiert. Dies ist der richtige Zeitpunkt, um abhängig von der Displaytechnologie das Antialiasing ein- oder auszuschalten. Um diese Änderungen zu reflektieren, sollte am Ende der Methode ein Redraw des Zifferblatts angefordert werden.

Mit der Zeit haushalten

Je nach Zifferblatt sind die Anforderungen an die Granularität der dargestellten Uhrzeit recht unterschiedlich. Die meisten Zifferblätter werden zumindest Stunde und Minuten der aktuellen Zeit anzeigen, im Interactive Mode wahrscheinlich auch die Sekunden. Demzufolge reicht es im Ambient Mode aus, die Anzeige einmal pro Minute zu aktualisieren, wohingegen man im Interactive Mode eine sekundengenaue Aktualisierung implementieren sollte. Für so genannte „Sweeping“ Watchfaces, bei denen der Sekundenzeiger nicht diskret von einer Sekunde zur nächsten springt, ist sogar eine höhere Aktualisierungsfrequenz erforderlich, um die Illusion eines sich kontinuierlich bewegenden Sekundenzeigers zu erzeugen. Um Energie zu sparen, sollte die Anzahl der Bildschirmaktualisierungen so gering wie möglich sein, sodass der Prozessor in den Schlummermodus gehen kann.

Aus diesem Grund ruft Android Wear nur einmal pro Minute die Methode onTimeTick() auf. Dies reicht aus, um im Ambient Mode dafür zu sorgen, dass die aktuelle Uhrzeit minutengenau dargestellt werden kann. In unserer Implementierung rufen wir einfach invalidate() auf der WatchfaceEngine auf, um einen Redraw-Zyklus zu erzwingen (Listing 3).

  @Override
  public void onTimeTick() {
    super.onTimeTick();
    invalidate();
  }

Im Interactive Mode reicht das jedoch nicht aus, hier wollen wir die Uhrzeit sekundengenau darstellen und den Sekundenzeiger diskret von Sekunde zu Sekunde springen lassen. Um das zu erreichen, richten wir während der Initialisierungsphase der WatchfaceEngine einen eigenen Timer ein (Listing 4).

  private class Engine extends CanvasWatchFaceService.Engine {
    // ...
    final Handler mUpdateTimeHandler = new EngineHandler(this);
  
    @Override
    public void onDestroy() {
      mUpdateTimeHandler.removeMessages(MSG_UPDATE_TIME);
      super.onDestroy();
    }
  
    private void updateTimer() {
      mUpdateTimeHandler.removeMessages(MSG_UPDATE_TIME);
      if (shouldTimerBeRunning()) {
        mUpdateTimeHandler.sendEmptyMessage(MSG_UPDATE_TIME);
      }
    }
  
    private boolean shouldTimerBeRunning() {
      return isVisible() && !isInAmbientMode();
    }
  
    private void handleUpdateTimeMessage() {
      invalidate();
      if (shouldTimerBeRunning()) {
        long timeMs = System.currentTimeMillis();
        long delayMs = INTERACTIVE_UPDATE_RATE_MS
            - (timeMs % INTERACTIVE_UPDATE_RATE_MS);
        mUpdateTimeHandler
          .sendEmptyMessageDelayed(MSG_UPDATE_TIME, delayMs);
      }
    }
  }

  private static class EngineHandler extends Handler {
    private final WeakReference mWeakReference;

    public EngineHandler(ClassicWatchFace.Engine reference) {
      mWeakReference = new WeakReference<>(reference);
    }

    @Override
    public void handleMessage(Message msg) {
      ClassicWatchFace.Engine engine = mWeakReference.get();
      if (engine != null) {
        switch (msg.what) {
          case MSG_UPDATE_TIME:
          engine.handleUpdateTimeMessage();
          break;
        }
      }
    }
  }

Da der Timer nur laufen soll, wenn das Zifferblatt sichtbar ist und wir uns nicht im Ambient Mode befinden, müssen wir den Timer entsprechend aktivieren bzw. deaktivieren. Dazu bieten sich die Methoden onAmbientModeChanged(boolean inAmbientMode) und onVisibilityChanged(boolean visible) an – sie werden immer dann aufgerufen, wenn das Zifferblatt zwischen Ambient Mode und Interactive Mode wechselt bzw. wenn es aktiviert/deaktiviert wird (Listing 5).

@Override
  public void onAmbientModeChanged(boolean inAmbientMode) {
    super.onAmbientModeChanged(inAmbientMode);
    if (mAmbient != inAmbientMode) {
      mAmbient = inAmbientMode;
      if (mLowBitAmbient) {
        mHandPaint.setAntiAlias(!inAmbientMode);
      }
      invalidate();
    }

    updateTimer();
  }

  @Override
  public void onVisibilityChanged(boolean visible) {
    super.onVisibilityChanged(visible);

    if (visible) {
      registerReceiver();
      mTime.clear(TimeZone.getDefault().getID());
      mTime.setToNow();
    } else {
      unregisterReceiver();
    }

    updateTimer();
  }

Zeitzonen

Da es durchaus sein kann, dass der Benutzer mit der Uhr in eine andere Zeitzone reist, müssen wir auch diesem Umstand Rechnung tragen. Zu diesem Zweck richten wir einen BroadcastReceiver ein (Listing 6) und registrieren ihn als Receiver für Zeitzonen-Intents. Um das System nicht unnötig zu belasten, registrieren wir diesen Receiver nur, wenn das Watchface sichtbar ist.

final BroadcastReceiver mTimeZoneReceiver = 
  new BroadcastReceiver() {

    @Override
    public void onReceive(Context context, Intent intent) {
      mTime.clear(intent.getStringExtra(“time-zone”));
      mTime.setToNow();
    }
  };
  boolean mRegisteredTimeZoneReceiver = false;

  private void registerReceiver() {
    if (mRegisteredTimeZoneReceiver) {
      return;
    }
    mRegisteredTimeZoneReceiver = true;
    IntentFilter filter = new
      IntentFilter(Intent.ACTION_TIMEZONE_CHANGED);
    ClassicWatchFace.this.registerReceiver(
      mTimeZoneReceiver, filter);
  }

  private void unregisterReceiver() {
    if (!mRegisteredTimeZoneReceiver) {
      return;
    }
    mRegisteredTimeZoneReceiver = false;
    ClassicWatchFace.this.unregisterReceiver(mTimeZoneReceiver);
  }

Das Zifferblatt malen

Nachdem die Vorarbeit geleistet wurde, können wir uns dem Zeichnen des eigentlichen Zifferblatts widmen. Das System ruft hierzu die Methode Engine.onDraw(Canvas canvas, Rect bounds) auf und übergibt als Aufrufparameter die Canvas sowie deren Abmessungen. Dies ist der erste Zeitpunkt, an dem die Zifferblattimplementierung die Abmessungen des Bildschirms erhält. Falls eine Hintergrundgrafik dargestellt werden soll, ist somit nun die erste Gelegenheit, die Grafik zu laden und passend zu skalieren. Ein Beispiel hierfür ist in Listing 7 zu sehen.

if (mBackgroundScaledBitmap == null
    || mBackgroundScaledBitmap.getWidth() != width
    || mBackgroundScaledBitmap.getHeight() != height) {
    mBackgroundScaledBitmap =
      Bitmap.createScaledBitmap(mBackgroundBitmap,
        width, height, true);
  }
  canvas.drawBitmap(mBackgroundScaledBitmap, 0, 0, null);

Für das Beispielszifferblatt verwenden wir keine Hintergrundgrafik, sondern Tiefschwarz, sodass der Hintergrund mittels canvas.drawRect(0, 0, canvas.getWidth(), canvas.getHeight(), mBackgroundPaint); gezeichnet werden kann. Für die restlichen Elemente des Zifferblatts müssen nun die jeweiligen Positionen ermittelt werden. Das Zeichnen der Elemente erfolgt in Z-Order, wozu die draw-Methoden der Canvas benutzt werden. Abhängig davon, ob sich die Uhr im Interactive Mode oder im Ambient Mode befindet, werden einige Elemente nicht oder mit einem anderen Paint Style gezeichnet: So werden in unserem Beispiel Sekundenzeiger und Minutenmarkierungen im Ambient Mode nicht gezeichnet.

Exemplarisch sei hier die Implementierung einiger Zifferblattelemente erläutert. Wie bereits erwähnt, überlagern sich die Elemente des Zifferblatts, sodass sie in der richtigen Reihenfolge gezeichnet werden müssen. Als Erstes zeichnen wir die Stunden- und Minutenmarkierungen. Die Stundenmarkierungen sind deutlich markanter als die Minutenmarkierungen. Um das Ablesen des Zifferblatts im Ambient Mode zu erleichtern, und die Anzahl der aktiven Pixel zu verringern, zeichnen wir die Minutenmarkierungen nur im Interactive Mode.

// hour ticks
  float innerTickRadius = centerX - HOUR_TICK_LENGTH;
  for (int tickIndex = 0; tickIndex < 12; tickIndex++) {
    float tickRot = (float) (tickIndex * Math.PI * 2 / 12);
    float innerX = (float) Math.sin(tickRot) * innerTickRadius;
    float innerY = (float) -Math.cos(tickRot) * innerTickRadius;
    float outerX = (float) Math.sin(tickRot) * centerX;
    float outerY = (float) -Math.cos(tickRot) * centerX;
    canvas.drawLine(
      centerX + innerX, 
      centerY + innerY, 
      centerX + outerX, 
      centerY + outerY, 
      mHourTickPaint);
  }
  
  // minute ticks
  if (!mAmbient) {
    innerTickRadius = radius - MINUTE_TICK_LENGTH;
    for (int tickIndex = 0; tickIndex < 60; tickIndex++) {
      if ( (tickIndex % 5) != 0 ) {
        float tickRot = (float) (tickIndex * Math.PI * 2 / 60);
        float innerX = (float) Math.sin(tickRot) * 
          innerTickRadius;
        float innerY = (float) -Math.cos(tickRot) * 
          innerTickRadius;
        float outerX = (float) Math.sin(tickRot) * centerX;
        float outerY = (float) -Math.cos(tickRot) * centerX;
        canvas.drawLine(
          centerX + innerX, 
          centerY + innerY, 
          centerX + outerX, 
          centerY + outerY, 
          mMinuteTickPaint);
      }
    }
  }

Da ein rundes Watchface auf einem eckigen Display ein wenig fehl am Platz wirkt, passen wir die Stunden- und Minutenmarkierungen bei Bedarf an die Displaygeometrie an. Zur Berechnung der Start- und Endkoordinaten der Markierungen kommt ein wenig Trigonometrie zum Einsatz, passend dazu Listing 9. Bei der Verwendung der Winkelfunktionen ist zu beachten, dass die entsprechenden Methoden in Java den Winkel im Bogenmaß erwarten, daher müssen wir den Winkel zunächst mittels Math.toRadians() von Grad in Bogenmaß umwandeln.

Trigonometrie für Watchfaces

Für die Bestimmung der Koordinaten eines rechteckigen Watchface verwenden wir die trigonometrischen Formeln für die Bestimmung der Seitenlänge eines Dreiecks bei Kenntnis nur einer Seite und eines Winkels. Die Grundformeln lauten:

Abb. 3: Trigonometriegrundformen

Abb. 3: Trigonometriegrundformen

Anhand der folgenden Skizzen können wir sehen, dass wir je nach Winkel allerdings an der Länge der Gegenkathete (opposite) bzw. der Kathete (adjacent) interessiert sind:

Abb. 4: Je nach Winkel interessiert die Länge der Gegenkathete oder der Kathete

Abb. 4: Je nach Winkel interessiert die Länge der Gegenkathete oder der Kathete

Daher hier einige Umformungen, um die entsprechenden Werte zu ermitteln. Für die Ermittlung der Länge der Gegenkathete:

Abb. 5: Länge der Gegenkathete

Abb. 5: Länge der Gegenkathete

Und für die Ermittlung der Länge der Ankathete (adjacent):

Abb. 6: Länge der Ankathete

Abb. 6: Länge der Ankathete

 

for (int tickIndex = 0; tickIndex <= 60; tickIndex++) {     if (mAmbient && (tickIndex % 5 != 0)) {       // no tick for minutes in ambient mode     }     else {       Paint paint = (tickIndex % 5 == 0)          ? mHourTickPaint : mMinuteTickPaint;          int tickDegree = tickIndex * 6;       float tickRadians = (float) Math.toRadians(tickDegree);          if (tickDegree > 315 || tickDegree <= 45) {         float oppOuter = (float) (Math.sin(tickRadians)              * (radius + 2) / Math.cos(tickRadians));         float oppInner = (float) (Math.sin(tickRadians)              * (radius - tickLength) / Math.cos(tickRadians));         canvas.drawLine(           centerX + oppOuter, -2,            centerX + oppInner,            tickLength, paint);       }        else if (tickDegree >= 135 && tickDegree <= 225) {         float oppOuter = (float) (Math.sin(tickRadians)              * (radius + 2) / Math.cos(tickRadians));         float oppInner = (float) (Math.sin(tickRadians)              * (radius - tickLength) / Math.cos(tickRadians));         canvas.drawLine(           centerX + oppOuter, height + 2,            centerX + oppInner, height - tickLength,            paint);       }        else if (tickDegree > 45 && tickDegree < 135) {         float adjOuter = (float) (Math.cos(tickRadians)              * (radius + 2) / Math.sin(tickRadians));         float adjInner = (float) (Math.cos(tickRadians)              * (radius - tickLength) / Math.sin(tickRadians));         canvas.drawLine(           width + 2, centerY + adjOuter,            width - tickLength, centerY + adjInner,            paint);       }        else if (tickDegree > 225 && tickDegree <= 315) {
        float adjOuter = (float) (Math.cos(tickRadians) 
            * (radius + 2) / Math.sin(tickRadians));
        float adjInner = (float) (Math.cos(tickRadians) 
            * (radius - tickLength) / Math.sin(tickRadians));
        canvas.drawLine(
          -2, centerY + adjOuter, 
          tickLength, centerY + adjInner, 
          paint);
      }
    }
  }
  canvas.drawRoundRect(
    tickLength -2, tickLength -2, 
    width - tickLength +2, height - tickLength +2, 
    5, 5, mBackgroundPaint);

Zur Anzeige des Datums zeichnen wir ein weiß ausgefülltes, abgerundetes Rechteck ungefähr auf die Drei-Uhr-Position. Das Datum selbst wird mittels canvas.drawText() gezeichnet. Um das Datum im Datumsfenster zu zentrieren, muss vorher noch die Breite des Datums-Strings ermittelt werden, das kann mittels measureText erfolgen (Listing 10).

int right = width - 30;
  int left = right - 40;
  int top = Math.round(centerY) - 13;
  int bottom = Math.round(centerY) + 13;
  Rect innerDateRect = new Rect(left, top, right, bottom);
  canvas.drawRoundRect(left, top, right, bottom, 4, 4, mInnerDateRectPaint);

  Calendar cal = Calendar.getInstance();
  int dayOfMonth = cal.get(Calendar.DAY_OF_MONTH);
  String dayOfMonthStr = String.valueOf(dayOfMonth);

  float mTextWidth, mTextHeight;
  Rect textBounds = new Rect();
  mDateTextPaint.getTextBounds(dayOfMonthStr, 0, dayOfMonthStr.length(), textBounds);
  mTextWidth = mDateTextPaint.measureText(dayOfMonthStr);
  mTextHeight = textBounds.height();

  canvas.drawText(
    dayOfMonthStr,
    innerDateRect.centerX() - (mTextWidth / 2f),
    innerDateRect.centerY() + (mTextHeight / 2f),
    mDateTextPaint
  );

Der Minutenzeiger besteht aus einer einfachen Linie, deren Start- und Endpunkt wir mittels der Winkelfunktionen ermitteln. Wir lassen den Zeiger nicht am Mittelpunkt des Zifferblatts beginnen (auf diese Weise kann der Mittelpunkt des Zifferblatts nicht einbrennen), sondern zeichnen einen Kreis mit Radius 10 in der Mitte des Zifferblatts. Aus diesem Grund muss der Startpunkt der Zeiger ebenfalls mittels der Winkelfunktionen ermittelt werden (Listing 11).

int minutes = mTime.minute;
  float minRot = minutes / 30f * (float) Math.PI;
  // ...
  float minutesOuterX = (float) Math.sin(minRot) * 
    minLength + INNER_CIRCLE_RADIUS;
  float minutesOuterY = (float) -Math.cos(minRot) * 
  minLength + INNER_CIRCLE_RADIUS;
  float minutesInnerX = (float) Math.sin(minRot) *
    INNER_CIRCLE_RADIUS;
  float minutesInnerY = (float) -Math.cos(minRot) *
    INNER_CIRCLE_RADIUS;

  canvas.drawLine(
    centerX + minutesInnerX, 
    centerY + minutesInnerY, 
    centerX + minutesOuterX, 
    centerY + minutesOuterY, 
    mHandPaint);

  canvas.drawCircle(
    centerX, 
    centerY, 
    INNER_CIRCLE_RADIUS, 
    mInnerRingPaint);

Zur Konstruktion des Sekundenzeigers verwenden wir einen Path, den wir anschließend mittels einer Matrixoperation rotieren. Die Bounding-Box des Dreiecks wird durch die Matrixoperation um den Drehpunkt in der Mitte der Basis des Dreiecks rotiert. Anschließend kann der rotierte Pfad mittels canvas.DrawPath gezeichnet werden (Listing 12)

if (!mAmbient) {
    Path triangle = new Path();
    triangle.moveTo(centerX, 20);
    triangle.lineTo(
      centerX - (SECOND_HAND_TRIANGLE_HEIGHT / 2), 
      radius - INNER_CIRCLE_RADIUS);
    triangle.lineTo(
      centerX + (SECOND_HAND_TRIANGLE_HEIGHT / 2), 
      radius - INNER_CIRCLE_RADIUS);
    triangle.close();

    Matrix matrix = new Matrix();
    RectF triangleBounds = new RectF();
    triangle.computeBounds(triangleBounds, true);
    matrix.preRotate(
      secDegree, 
      triangleBounds.centerX(), 
      triangleBounds.bottom + 10);

    triangle.transform(matrix);
    canvas.drawPath(triangle, mSecondHandPaint);
    }

Fazit

Android Wear bietet viel Flexibilität bei der Entwicklung von Watchfaces. Durch die Möglichkeit, den kompletten Bildschirm flexibel zu nutzen, sind der Kreativität kaum Grenzen gesetzt, wie die Vielzahl der im Play Store verfügbaren Watchfaces zeigen: Von Nachahmungen bekannter Uhrenmarken über Merchandising-orientierte Watchfaces bis hin zu kreativen Experimenten ist alles Erdenkliche verfügbar. In Verbindung mit den derzeit verfügbaren Android Wear Smartwatches haben Konsumenten so die Möglichkeit, sich ein individuelles Zeitstück zu konfigurieren.

Bei der Implementierung müssen etliche Aspekte beachtet werden, wie wir in diesem Artikel gesehen haben. Glücklicherweise wird der Löwenanteil des notwendigen Boilerplate Codes vom Android Studio Wizard generiert, sodass Entwickler sich auf die Implementierung der grafischen Gestaltung des Watchface in der onDraw-Methode konzentrieren können.

Aufmacherbild: Android von Shutterstock / Urheberrecht: tanuha2001

Verwandte Themen:

Geschrieben von
Peter Friese
Peter Friese
Peter Friese arbeitet als Developer Advocate bei Google im Developer Relations Team in London. Er trägt regelmäßig zu Open-Source-Projekten bei, spricht auf Konferenzen und bloggt unter http://www.peterfriese.de. Auf Twitter findet man ihn unter @peterfriese und auf Google+ unter +PeterFriese.
Kommentare

Hinterlasse einen Kommentar

1 Kommentar auf "Android Wear Watchfaces selber programmieren"

avatar
400
  Subscribe  
Benachrichtige mich zu:
Nicolas
Gast

Toller Artikel, vielen Dank!