Garminウォッチ用ウォッチフェイスを作る(その7)

実機で動かすと予期せぬ結果に

前回ローパワーモードでも秒の更新を行うべくパーシャルアップデート処理を行うやり方について書きました。
alasixosaka.hatenablog.com
シミュレータでうまく動いたので、出来た出来たと喜んでいたのですが、実機(Forerunner255)で動かしてみるととんでもない事に。
ローパワーモードに入ってフルアップデートからパーシャルアップデートに移るところまでは良かったのですが、そこから秒が00になってフルアップデートが実行されると画面が真っ暗に。そしてパーシャルアップデートに再び移って、秒だけが表示され続けるという現象になってしまいました。
実機での動作をシミュレータで再現できなければシミュレータの意味がないと思うんですが、バグなんでしょうか?

原因を探る

とにかく大事なのは実機の動作なので何とかしなければいけません。
思い当たるのは、秒表示をレイアウトを使った表示からdrawText()を使った表示に変えたときに、フォントのバックグラウンドが青で、フォアグラウンドが黒になってしまっていたことで、どこかで指定してやらなければ同じようなことが起こっていてフォアグランドが黒で表示されてしまったのかと。
そこで、またサンプルプログラムを見直して、onUpdate()の中に下記のような記述があることを見つけました。

dc.clearClip();
dc.setColor(bgColor,bgColor);
dc.clear();
dc.setColor(timeColor,Graphics.COLOR_TRANSPARENT);

APIを読むとclearClip()というのは、パーシャルアップデートの所で指定した更新するための矩形の領域を全画面に戻す関数のようです。
ですので、全画面に戻して、黒を指定して、画面を全クリアして、もう一度カラーを指定する。という動作のようです。
よくわからないのは、doTime()関数の中で、パーシャルアップデートだろうが、フルアップデートだろうが通る処理の所で、上の最後の行と同じ処理をしていて、色の指定はできているはずと思うのですが...。とにかく、この部分をonUpdate()の最初の部分に入れたら実機でもちゃんと動くようになりました。

最終版のプログラム

ということで、ようやく実機でもパーシャルアップデートをうまく動かすことができるようになったので、本チャンのプログラムを修正します。

パーシャルアップデートする部分としない部分を切り分ける

以前作った本チャンのウォッチフェイスでは、月、日、曜日、時、分、秒、心拍数、歩数、バッテリー残量、通知の有無、スマホとの接続の有無を表示していました。
alasixosaka.hatenablog.com
このうち、月、日、曜日、時、分については1分に1回の更新で問題ないのでパーシャルアップデートの対象としません。また、バッテリー残量も毎秒更新する必要もないのでこれも対象外とします。そして、歩数も時計を着けてじっとしているときや時計をはずしているときなどローパワーモードに入るシチュエーションでは更新されないので対象外で問題ないと思います。また、通知に関しても通知が来ればアクティブモードに入るのでパーシャルアップデートは不要と考えます。そうすると、残りは、秒と心拍数とスマホとの接続の有無ということになります。この3つだけをパーシャルアップデートの対象としました。
パーシャルアップデートでは更新する領域を矩形で指定しないといけないので、この3つを1列に並べるように配置を変えます。つまり、秒の位置を少し下げて、やや左に寄せ、その左に心拍数、秒の右にスマホとの接続を示すブルートゥースマークを表示するようにしました。また、心拍数用のハートマークは更新の必要がありませんので矩形表示の外に置いておくことにしました。
それぞれの表示座標は、ハートマークが(90, 150)、心拍数の値が(130, 160)、秒の表示が(180, 160)、ブルートゥースマークが(210, 150)としました。括弧の中はそれぞれのx座標とy座標です。グラフィックスとテキストの表示のy座標が10ピクセルずれているのはテキスト表示が縦方向にセンタリングしていて、グラフィックスはしていないためです。
更新する矩形の設定は、左上の座標が(130, 148)で幅と高さはそれぞれ、100と26です。これでパーシャルアップデートの30ms以内に処理が収まりました。

心拍数への対応

心拍数の取得については、再三紹介している参考サイトに書いてあったのですが、
take4-blue.com
本来はActivity.getActivityInfo().currentHeartRateでとってきた値が現在の心拍数ですが、この方の使っているForAthlete 45ではActivityMonitor.getHeartRateHistoryでとってきた履歴の値でしかうまくいかなかったとのことで、始めはその通りにしていたのですが、実機で試してみるとリアルタイムでの更新ができていないようでした。
そこで、ここのブログに書いてある通り、まず、Activity.getActivityInfo().currentHeartRateを試み、取れなかったときにActivityMonitor.getHeartRateHistory、それでもダメな時は"---”を返すようにdoHeartrate()という関数を作りました。これでリアルタイムに心拍が更新されるようになりました。ウォッチフェイスでの心拍数の毎秒更新にも機種依存があるようです。上のブログではForAthlete 45ではリアルタイムの心拍更新はアクティビティ中でしか行えないとのこと。
主な修正箇所を書いておきます。まず、onUpdate()から

    function onUpdate(dc as Dc) as Void {
        System.println("full update!");
        dc.clearClip();
        dc.setColor(bgColor,bgColor);
    	dc.clear();
    	dc.setColor(timeColor,Graphics.COLOR_TRANSPARENT);
        // Get and show the current time
        var clockTime = System.getClockTime();
        var now = Time.now();
        var nowM = Time.Gregorian.info(now, Time.FORMAT_MEDIUM);
        var nowS = Time.Gregorian.info(now, Time.FORMAT_SHORT);
        var batt = System.getSystemStats().battery;
        //var hearts = ActivityMonitor.getHeartRateHistory(1, true).next().heartRate;
        var info = System.getDeviceSettings();
        var timeString = Lang.format("$1$:$2$", [clockTime.hour, clockTime.min.format("%02d")]);
        var timeString2 = Lang.format("$1$", [clockTime.sec.format("%02d")]);
        var dayString = Lang.format("$1$/$2$ $3$", [nowS.month, nowS.day, nowM.day_of_week]);
        var battString = Lang.format("$1$%", [batt.format("%02d")]);
        //var heartString = Lang.format("$1$", [hearts]);
        var stepstring = Lang.format("$1$", [ActivityMonitor.getInfo().steps.format("%5d")]);
        //System.println (hearts);
        var view = View.findDrawableById("TimeLabel") as Text;
        //var view2 = View.findDrawableById("Label") as Text;
        var view3 = View.findDrawableById("DayLabel") as Text;
        var view4 = View.findDrawableById("BatLabel") as Text;
        //var view5 = View.findDrawableById("HeartLabel") as Text;
        var view6 = View.findDrawableById("shoeLabel") as Text;
        view.setText(timeString);
        //view2.setText(timeString2);
        view3.setText(dayString);
        view4.setText(battString);
        //if (hearts != ActivityMonitor.INVALID_HR_SAMPLE){
        //    view5.setText(heartString);
        //}else{
        //    view5.setText("---");
        //}
        //if (hearts == ActivityMonitor.INVALID_HR_SAMPLE){
        //    heartString="---";
        //}
        var heartString=doHeartrate();
        view6.setText(stepstring);
        //BAT1.draw(dc);
        // Call the parent onUpdate function to redraw the layout
        View.onUpdate(dc);
        //if (info.phoneConnected) {
        //    dc.drawBitmap(50,195, BT);
        //}
        doTime(dc,timeString2,heartString,info.phoneConnected,true);
        if (info.notificationCount > 0) {
            dc.drawBitmap(80,195, mes);
        }
        //if (batt>=50){
            //dc.drawBitmap(140,210, BAT4);
        //}else{
            //dc.drawBitmap(140,210, BAT1);
        //}
        dc.setColor(Graphics.COLOR_WHITE, Graphics.COLOR_BLACK);
        dc.drawRectangle(140, 202, 20, 10);
        dc.fillRectangle(140, 202, 20*batt/100, 10);
        dc.drawBitmap(90,150, heart);
        dc.drawBitmap(80, 220, shoe);
    }

上にも書いたように、はじめに矩形で指定していたクリップを解除して全画面に戻して、消去、カラーの指定を行います。
また、パーシャルアップデートで毎秒更新する秒、心拍数、スマホとの接続に関する表示はコメントアウト(これらの処理はdoTime()関数にうつしているため)。
心拍数はdoHeartrate()という関数を呼び出して結果をhartString に格納しています。
また、doTime()関数を呼び出して、秒、心拍数、ブルートゥースマークの表示を行います。それ以外の表示はこのonUpdate()の中で処理しています。

次にonPartialUpdate()です。

    function onPartialUpdate(dc as Dc) as Void {
        System.println("partial update!");
        var clockTime = System.getClockTime();
        var timeString2 = clockTime.sec.format("%02d");
        var heartString=doHeartrate();
        var info = System.getDeviceSettings();
        doTime(dc,timeString2,heartString,info.phoneConnected,false);
    }

まず、秒の文字列をtimeString2に代入し、次にdoHeartrate()を呼び出して、心拍数をhaertStringに代入、ifnoにシステムの状態を代入し、それらを引数にdoTime()を呼び出しています。

次は、そのdoTime()です。

    function doTime(dc,time,hart,phone,isFull) {
     	//here is where real things happen.
     	//if it's a full update, just carry on, but if it's a partial, use setClip

     	if(!isFull) {
  			//set the clip so it's just the time.  To keep it simple, I do the entire screen width
  			dc.setClip(130, 148, 100, 26);
  			dc.setColor(bgColor,bgColor);
  			//clear anything that might show through from the previous time
  			dc.clear();     	
     	} 
     	dc.setColor(timeColor,Graphics.COLOR_TRANSPARENT);
     	dc.drawText(180,160,timeFont,time,Graphics.TEXT_JUSTIFY_CENTER | Graphics.TEXT_JUSTIFY_VCENTER);
     	dc.drawText(130,160,hartFont,hart,Graphics.TEXT_JUSTIFY_CENTER | Graphics.TEXT_JUSTIFY_VCENTER);
        if (phone) {
            dc.drawBitmap(210,150, BT);
        }
        //doing the clearClip() here doesn't work, so I did it in onUpdate()
     	//instead!
     	//dc.clearClip();
     }

まず、パーシャルアップデートの場合、消去する領域をsetClip()で指定し、消去するためsetColor()で黒を指定、clear()で消去しています。
その後は、パーシャルアップデート、フルアップデート共通で、setColor()で色の指定を元に戻し、drawText()で秒の表示、同じくdrawText()で心拍数を表示しています。また、phoneがTrueならブルートゥースマークを表示してスマホと接続されていることを示すようにしています。

最後がdoHeartRate()です。

     function doHeartrate() {
    	var currentHeartrate = Activity.getActivityInfo().currentHeartRate;
    	if (currentHeartrate == null) {
			currentHeartrate = ActivityMonitor.getHeartRateHistory(1, true).next().heartRate;
    	}
		if (currentHeartrate != null && currentHeartrate != ActivityMonitor.INVALID_HR_SAMPLE) {
			return currentHeartrate.format("%d");
		}
		else {
			return "---";
		}
    } 

ここでの処理は、上にも書いたように参考サイトの本来のやり方で記述されている通りです。
繰り返しになりますが、まず、Activity.getActivityInfo().currentHeartRate で現在の心拍数を取りに行きます。結果はcurrentHeartrate に格納されます。中身が空だった場合は、履歴の心拍を読みに行きます。ActivityMonitor.getHeartRateHistory(1, true).next().heartRate で履歴の心拍が得られます。ここでも得られなかったら"---"を返します。
こんな感じで表示されます。

ようやく完成

これで一応満足のいくウォッチフェイスができました。
それにしても、結果的にレイアウトをほとんど使わずに直接描画というやり方になってしまいました。たぶんパーシャルアップデートを使ってもレイアウトを使った表示をすることはできるのではないかと思います。今回は、とにかく完成させるのを優先したためにサンプルプログラムのやり方を踏襲してやってしまいました。あまりスマートでないような気がします。特に多数のウォッチに対応させるならレイアウトを使った方がスマートにできると思います。この辺はアンドロイドのプログラムに慣れていれば割と簡単に対応できるのではないかと思いました。
この次は、データフィールドにチャレンジしてみたいと思います。日本語の参考になるサイトがほとんどないのでどうなるかわかりませんが。