Garminウォッチフェイスの作成:デジタル時計

今持っているGarminのForeAthlete 45で、気に入ったウォッチフェイスがなかったので、とりあえず作ってみた。

仕様みたいなもの

まず、ウォッチフェイスに必要な情報。

  • 時:分:秒
  • 曜日・日付
  • 通知があるかないか
  • Bluetooth接続状況
  • バッテリー残容量
  • 歩数/歩数ゴール
  • 心拍数

あったらいいなと思う情報

  • 上昇階数
  • 日が出ている時間と今の時間
  • 歩数ゴールまでの到達割合

ちなみに上昇階数は、利用できるSDKのバージョンにより、45では利用できないことが後で分かった。また秒の表示は、45では常時表示できないようだ。

画面構成としては、以下のような感じ。

冬の間、袖の下に時計が隠れ、左手に時計を巻いていると、パッと見た瞬間は右半分しか視認できないので、その部分に特に確認したい項目を配置してみた。

実装内容

ソースコード全体

using Toybox.WatchUi;
using Toybox.Graphics;
using Toybox.System;
using Toybox.Lang;
using Toybox.Time;

class DigitalView extends WatchUi.WatchFace {
    var center;
    var bmBluetooth;
    var bmNotify;
    
    var fnLarge;
    var fnNormal;

    function initialize() {
        WatchFace.initialize();
        
        bmBluetooth = WatchUi.loadResource(Rez.Drawables.BluetoothIcon);
        bmNotify = WatchUi.loadResource(Rez.Drawables.NotifyIcon);
        
        fnLarge = Graphics.FONT_NUMBER_MEDIUM;
        fnNormal = Graphics.FONT_LARGE;
    }

    // Load your resources here
    function onLayout(dc) {
        center = [dc.getWidth()/2, dc.getHeight()/2];
    }

	// 時分を描画する    
    function drawHM(dc, time) {
    	var timeString = Lang.format("$1$:$2$", [time.hour, time.min.format("%02d")]);
    	var wh = dc.getTextDimensions(timeString, fnLarge);
    	dc.drawText(center[0], center[1] - Graphics.getFontAscent(fnLarge),
    		fnLarge, timeString, Graphics.TEXT_JUSTIFY_CENTER);
    }

    // 曜日を描画する
    function drawDayWeek(dc, timeM, timeS) {
    	var dayString = Lang.format("$1$/$2$ $3$", [timeS.month, timeS.day, timeM.day_of_week]);
    	dc.drawText(center[0], center[1]
    		 - Graphics.getFontAscent(fnLarge)
    		 - Graphics.getFontAscent(fnNormal),
    		fnNormal, dayString,
    		Graphics.TEXT_JUSTIFY_CENTER);
    }
    
    // バッテリーパーセンテージを表示
    function drawBattery(dc) {
    	dc.drawText(center[0], dc.getHeight() - dc.getFontHeight(fnNormal),
    		fnNormal, System.getSystemStats().battery.format("%3.0f") + "%",
    		Graphics.TEXT_JUSTIFY_CENTER);
    }

	// 心拍数を求める
    private function retrieveHeartrateText() {
		var currentHeartrate = ActivityMonitor.getHeartRateHistory(1, true).next().heartRate;
		if (currentHeartrate != ActivityMonitor.INVALID_HR_SAMPLE) {
			return currentHeartrate.format("%d");
		}
		else {
			return "---";
		}
    }    
    
    // 歩数・心拍数などの情報を表示
    function drawInformation(dc) {
        var info = ActivityMonitor.getInfo();

		var valueString = info.stepGoal.format("%5d") + "/" + info.steps.format("%5d");

		if(ActivityMonitor has :INVALID_HR_SAMPLE) {
			valueString += "\n" + retrieveHeartrateText();
		}
    	dc.drawText(center[0], center[1],
    		fnNormal, valueString,
    		Graphics.TEXT_JUSTIFY_CENTER);
    }
    
    // Bluetoothとイベント通知アイコンの表示
    function drawNotify(dc) {
    	var info = System.getDeviceSettings();
    	var y = center[1] + dc.getFontHeight(fnNormal) * 1.5;
    	if (info.notificationCount > 0) {
    		// メールアイコンの表示
    		dc.drawBitmap(center[0] * 1.5, y - bmNotify.getHeight() / 2, bmNotify);
    	}
    	if (info.phoneConnected) {
    		// BLEアイコンの表示
    		var wh = [bmBluetooth.getWidth(), bmBluetooth.getHeight()];
    		var tl = [center[0] / 2 - wh[0], y - wh[1] / 2];
    		dc.setColor(Graphics.COLOR_DK_BLUE, Graphics.COLOR_BLACK);
    		dc.fillEllipse(tl[0] + 12, y, wh[0] / 2 - 1, wh[1] / 2 - 1);
    		dc.drawBitmap(tl[0], tl[1], bmBluetooth);
    	}
    }

    // Update the view
    function onUpdate(dc) {
        dc.setColor(Graphics.COLOR_BLACK, Graphics.COLOR_BLACK);
    	dc.clear();
    	
    	var now = Time.now();
        var nowM = Time.Gregorian.info(now, Time.FORMAT_MEDIUM);
        var nowS = Time.Gregorian.info(now, Time.FORMAT_SHORT);
        dc.setColor(Graphics.COLOR_WHITE, Graphics.COLOR_BLACK);
        drawHM(dc, nowS);
        drawDayWeek(dc, nowM, nowS);        
        drawInformation(dc);
        drawNotify(dc);
        
        dc.setColor(Graphics.COLOR_GREEN, Graphics.COLOR_BLACK);
        drawBattery(dc);
    }
}

注意点

月の表示

ソース中では、

var nowS = Time.Gregorian.info(now, Time.FORMAT_SHORT);

をの結果を使っている。はじめTime.FORMAT_MEDIUMの結果を使っていたのだが、実機では、「1月」と「月」という文字まで表示されてしまっていた。シミューレーターでは「月」は入っていなかったのだが。
そのため、Time.FORMAT_SHORTの結果を使うようにした。

時の表示

時計の設定を12時間表示にしても、そのままではAM/PM等の表示はされなかった。
この表示は、ウォッチフェイスのプログラム中で、System::DeviceSettings::is24Hourの値を見て、自前で表示を切り替えないといけないようだった。
自分は、24時間表示のほうを使っているので、今回のプログラムではそのまま表示している。

フォント

Garminウォッチフェイス作成:フォントサイズで調べた結果、全機種で共通で使えるようないいフォントがなかったので、自前で作成するか、機種依存にしてしまうか悩むところがある。

今回、自分が持っている機種でのみ利用することにしているので、その機種に合わせてフォントの調整をした。

心拍数の表示

	// 心拍数を求める
    private function retrieveHeartrateText() {
		var currentHeartrate = ActivityMonitor.getHeartRateHistory(1, true).next().heartRate;
		if (currentHeartrate != ActivityMonitor.INVALID_HR_SAMPLE) {
			return currentHeartrate.format("%d");
		}
		else {
			return "---";
		}
    }    

当初は、ActivityMonitor.getHeartRateHistory(null, false)を使っていたのだが、いろいろなソースを参照した結果、(1,true)が一番多かったので、そちらに合わせた。
また、時計を付けていない期間が長いと、取り出した値にINVALID_HR_SAMPLE(255)が設定されるので、その場合には無効な値として”—“を表示するようにした。

ビットマップの描画と色の設定

Bluetoothアイコンは、当初、青と白の色で作成した。
青色は、ドキュメントに書かれていた16色パレットの中の0x00AAFFを指定したのだが、実際に表示されたときには、0x00FFFFと白とのデザリング色になってしまった。

色に関して簡単に調べたのだが、この時点ではそれ以上調べることができず、結局青には0x00FFFFだと明るすぎたので0x0000FFを使うようにした。

SDKのバージョンにより利用可能な機能の制限

ForeAthlete 45の前は、Vivoactive HR Jを使っていた。そちらのウォッチフェイスでは、階段上下階数や秒単位での表示更新なども、どの機種でもできると思っていたのだが、SDKにより利用できる機能にかなりの制限があることが分かった。

ちなみに、どの機種がどのバージョンなのかは、ドキュメントをざっと見た限りではわからなかった。
プロジェクトのマニフェストからビルドターゲットを選び、実行の構成でターゲット機種を選択した時に表示されるターゲットSDKバージョンで判断するしかなかった。


上のVenuはVersion 3台が使えるようだ。

APIがどのバージョンで利用可能かどうかは、ヘルプに記載がある。

実行結果

最後に

とりあえず、自分用にはいい感じのものができたと思う。

作成に要した時間は、途中、フォントや色やビットマップ作成の方法に試行錯誤したが、実質3日ぐらいだろうか。

 

コメント

タイトルとURLをコピーしました