Goは二面性の塊だ一覧Glassや時計の話題のみ表示しています。前回は環境構築とチュートリアルに従って雛形を作成しました。Faceが動いているか読み解いていきます。wearモジュールを開くとjavaファイルとしてMyWatchFaceが1つ作成されています。この1つのファイルにWearアプリを作るために必要な殆どの内容が入っていて、初期にテンプレートとして作成されるファイルにしてはかなり大きなものになっています。コメントも全文英語なのでちょっと分かりづらいですね。それでは1つずつ分解してみていきましょう。classであるEngineがあるため実際は3つのクラスから構成されています。MyWatchFaceはWatchFaceの本体となるクラスです。EngineはWatchFaceを作る上で最も重要なクラスでライフサイクルや描画などさまざまな処理を取り扱います。EngineHandlerは時計で大切な一定時間後にEngineの処理を呼びます。MyWatchFaceの処理MyWatchFaceはその名の通り、WatchFaceの本体ですが、実際のところ殆どの処理はEngineに記載されており、Innerクラスを排除するとこれだけコンパクトになります。書き換え間隔を秒で指定しています。ここでは1秒が指定されています。実際に格納される値はミリ秒なので1000が設定されます。TimeUnit.SECONDS.toMillis(1);ハンドラー送信元を特定するためのIDを指定ハンドラーの送信元を特定するためのIDを指定しています。ここでは0が設定されていますが送信側と受信側が同じ値であればどんな値でも良いです。0;Engineの作成Engineのインスタンスを作成して返すことで、WatchFaceのライフサイクルでEngineを呼び出させることが出来ます。}Engineの処理EngineはWatchFaceの多くの処理を行います。インスタンス変数として次のような値を持ちます。mTapCount;mUpdateTimeHandlerには一定時間ごとのイベントを取り扱うためのEngineHandlerを持ちます。mRegisteredTimeZoneReceiverはタイムゾーンの変更時のReceiverが登録されているかどうかを持ちます。mBackgroundPaintは背景を描画するため、mHandPaintは針を描画するためのPaintです。mAmbientは現在がAmbientモードになっているかどうかを持ちます。mTimeは時間関係を取り扱うためのクラスです。Timeは扱える範囲が狭く2032年以降が取り扱えないので、本当ならこの実装はあまり良くない気がします。余裕がある人はCalendarに書き換えてください。mTimeZoneReceiverはタイムゾーンが変更された時のReceiverです。mTimeのタイムゾーンを設定して時間を置き換えています。このため、海外旅行などでタイムゾーンが変更された場合でも、自動的に新しいタイムゾーンに移行させることが出来ます。mTapCountはタップされた回数を記録しています。これは背景をタップするたびに色を切り替えるために使っているだけで、WatchFaceの作り次第で普通は不要になると思います。mLowBitAmbientはAmbient時に白黒2値をサポートするかどうかを格納します。onCreate()の処理Engineが作成された時に行う処理を記載します。Faceは実行時間がかなり長くなるのと、普段使用時の消費電力を極端に抑える必要が有ることから、1回で済む処理は全てここで記載してしまい、あとから行う処理を最小限にします。WatchFaceのスタイルを設定します。CardPeekModeは通知が来た時にどのようにWatchFace上に通知を見せるかを指定します。BackgroundVisibilityは通知が来た時に背景を表示するかどうかを指定します。ShowSystemUiTimeはシステムが提供する標準的なデジタルの時刻表示を行うかどうかを指定します。WatchFaceだけでは時刻がわかりにくい場合などにもシステムの時刻が表示されます。AcceptsTapEventsではユーザーがタップした時に処理を行うかどうかを指定します。.build());描画を行うのに必要な準備を行います。実際の描画は後で行いますが描画処理は何度も呼び出されるのでインスタンスの生成のような重たい処理は出来るだけonCreateのタイミングで行ってしまい、描画処理の負荷を下げます。Time();onDestroy()の処理onDestroy()では一定時間ごとに処理を行うmUpdateTimeHandlerのメッセージを削除して次回以降の処理が行われないようにします。}onPropertiesChanged()の処理onPropertiesChanged()はプロパティーが変更された時に呼び出されます。ここではAmbient時に白黒2値をサポートしているかを判別し保存しています。}onTimeTick()の処理onTimeTick()は一定時間ごとに呼び出されます。invalidate()を呼んで画面の再描画を促します。ここで呼び出されるのは最短でも1分に一度だけです。そのため、秒針などを表現したい場合は、mEngineHandlerを使って別に呼び出します。一定時間操作を行わないと、AmbientModeというバックライトが消えて色数が少ない省電力モードに移行します。AmbientModeになった後は画面の色数を減らして、画面描画を最短で1分ごとに抑えます。AmbientModeに入るときはinAmbientModeにtrueが、AmbientModeから出るときはinAmbientModeにfalseが渡されます。AmbientModeが切り替わった時は最新のAmbient状態をmAmbientに保存します。つぎに、白黒2値のAmbientModeに対応している場合は灰色が表現できなくなるためPaintのAntiAliasをAmbient時にfalse、Ambientから出て通常状態になる時にtrueに書き換えます。白黒2値のAmbientModeに対応していない場合はAntiAliasはtrueのままです。これにより白黒2値に対応していてAmbientModeに入る時だけAntiAliasがfalseとなり、それ以外ではtrueとなります。AntiAliasとは半透明の色を使うことで、線をなめらかに見せる機能ですが、その分余計に必要な処理が増えます。白黒2値の場合半透明が使用できずせっかくAntiAliasを計算しても白か黒に丸められてしまうので、無駄な処理を行わないようにしているというわけです。invalidate()を呼んで画面の再描画をリクエストして、次の1分まで待たずにAmbient状態に応じた描画を行います。updateTimer()はAmbient状態と通常状態に応じて画面の描画間隔を切り替えるためにタイマーを設定します。画面をタップした時はonTapCommand()が呼び出されます。引数tapTypeはどのようにタップしたかについて、画面をタップし始めた時はTAP_TYPE_TOUCHユーザーがタップ操作を取りやめたり別のジェスチャーに切り替えた時はTAP_TYPE_TOUCH_CANCELユーザーがタップ操作を完了した時はTAP_TYPE_TAPが渡されます。yはタップした座標が渡されます。eventTimeはタップされた時間が渡されます。このサンプルでは、タップが行われた場合、タップの回数をカウントし、タップの回数が偶数の時はR.color.backgroundを、奇数の時はR.color.bacground2を背景色に設定しています。最後にタップ操作を即座に反映するためにinvalidate()を呼んで画面の再描画をリクエストします。}onDarw()の処理onDraw()内で実際の画面描画を行います。通常のAndroidアプリでもそうですが、Wearアプリでは特にこのonDraw()内の処理を短く終わらせる必要があります。描画処理を行うための元となる時間を現在時刻にセットします。mTime.setToNow();前回までの描画をリセットするために背景色を塗りつぶします。Ambientモードでは黒一緒、Ambientモード以外ではタップ数に応じてpaintに設定された色で塗りつぶします。Ambientモードでは省電力のため画面を黒多めにすることが推奨されています。これは特に有機ELの液晶を持つ機種において消費電力の節約に役立ちます。}引数で渡されるboundsの半分を取得することで画面中央の座標を取得します。実際には毎回の描画ごと座標を求めるのは非効率なのでインスタンス変数に格納して画面サイズが変わらないうちは再利用した方がいい気がします。2f;次に針の先部分を求めます。これにはいくつかの方法があるのですが、ここでは三角関数を使って針の位置を求めています。通常、角度は0〜360度で求めますが、ここでは三角関数と相性がいい「ラジアン(rad)」と呼ばれる単位を使っています。πなので、半径を1とすると弧の角度(rad)と弧長の長さが同じになります。半径1mの正円では、角度1radの弧長は1m、角度2radの弧長は2mとなる。radの時、円弧の長さはχ×半径となります。秒針の角度を求めます。radを求めています。これが秒針の角度(rad)になります。秒針は1秒毎にカチカチと動くタイプなので秒だけを元に角度を求めています。πの値はMath.PIで求めることが出来ます。Math.PIは64bitの精度を持つdouble型で定義されていますが、そこまでの精度は必要ないのと処理量を抑えるために32bitの精度を持つfloat型に丸めて計算しています。Math.PI;同様に分針の角度も求めます。こちらも、1分ごとに針が動くタイプなので分針だけを元に角度を求めています。Math.PI;次に時針の角度も求めます。時針は12時間で一周します。1時間毎にカチカチと動くのではなく、分も考慮してなめらかに動くようにする必要もあります。60(分)で1(時間)となるように分を60で割ります。この値を時間に足します。これにより例えば4:30なら4.5に、2:15なら2.25になります。この値を6で割ることで12時間で0〜2まで1分ごとに等分に変化する値を求めています。Math.PI;次に針の長さを求めます。画面幅の半分から、秒針は20ひいた値、分針は40ひいた値、時針は80ひいた値としています。80;秒針を描画します。ambientモードの時は1分に1回しか書き換えが行わないため、秒針を描画すると動かない秒針となってしまいますそこで、Ambientモード以外(通常モードの時)だけ秒針を描画します。針の先は三角関数を使って求めています。sin(ラジアン角度)を渡すことで、針先のY座標を求めることが出来ます同様にcos(ラジアン角度)を渡すことで、針先のX座標を求めることが出来ます。ラジアン角度をχ、半径の長さを1とした場合、斜線と円の接する座標は次のように求めることが出来ます。この時に求まる値は3時の方向を基準とした時計回りです。※一般的に数学の説明では3時の方向を基準とした反時計回りで説明されます。Androidをはじめとする多くのコンピューターの世界では下に行くほど数値が大きくなる上下が入れ替わった座標系を使っているため上下が反転されて時計回りとなります。xをcos、yをsinとすると3時を基準とした時計回りとなる。時計の針で欲しいのは12時を基準とした時計回りなので、ちょっと工夫して座標を入れ替えます。xとyを入れ替えます。sinの結果をX、cosの結果をYとすることで、x=-yの斜線を対象とした座標となり6時を基準とした反時計回りになります。xとyを入れ替えると6時を基準とした反時計回りとなる。6時を基準とした反時計回りはちょうど12時を基準とした時計回りを上下反転した形になるので、Y座標に-1をかけて上下反転させます。上下を反転させることで12時を基準とした時計回りとなる。これで求まるのは、半径が1の場合の座標なので、線の長さ(secLength)を掛けて線の長さに応じた位置を求めます。sin、cosで取得される座標は、円の中心が左上(X:0,Y:0)とした場合の場所なので、中心からの位置を足して、中心からの位置に移動してやります。画面中心から、求まった線先の座標までの線をdrawLineで描くことで、秒針が描画できます。}同様に分針と時針も描画します。分針と時針はambientモードにかかわらず描画するため、if文の外で描画します。Faceが表示された瞬間に最新の状態で画面を描画して更新を再開するために、onVisibilityChanged()で表示状態が変更された時の処理を行います。画面が描画された時はタイムゾーンが変更された時の通知を受けるためのReceiverを登録します。画面我猫されていない間にタイムゾーンが変更されている場合に備えて現在のタイムゾーンをmTimeに設定し現在時刻を取得します。mTime.setToNow();画面が非表示になる時にはタイムゾーンの切り替わりによるReceiverを解除します。unregisterReceiver();状態に応じて画面の更新を行い続けるかどうかを指定するためupdateTimer()を呼びます。updateTimer();registerReceiver()の処理registerReceiver()ではタイムゾーンが変更された時に呼び出されるReceiverを登録しています。}unregisterReceiver()の処理registerReceiverで登録したReceiverを解除します。}updateTimer()の処理タイマーを解除した後、1秒毎に更新が必要な場合、タイマーを設定し直します。}shouldTimerBeRunning()の処理1秒ごとに更新が必要かどうかを返します。1秒毎に更新が必要なのは画面が描画されておりambientModeではない時です。次の1秒で呼び出されるようにTimerを設定します。この処理自体が毎回1秒毎に呼び出されます。最初にinvalidate()で画面描画処理のリクエストを行ったら、次の1秒後にまたこの処理が呼ばれるようにTimerを設定します。現在時刻に対して描画間隔を足し、現在時刻から描画間隔のあまりをひいた時間に処理が呼び出されるようにしています。これはなにをやっているかというと、タイマーは1秒毎に設定しても、きっかり1秒毎に呼び出されるとは限りません。単純に現在時刻+1秒とすると、登録する時には既に00分00秒100ミリ秒になっているかもしれません。すると、時間の処理は00分01秒100ミリ秒に呼び出されてしまいます。このまま続けると、次第に時間がずれてきますし、処理に時間がかかった時だけ更新間隔が広がってしまい、ぎこちない動きになってしまいます。なることを防ぐために、登録するとき秒の余分(100ミリ秒)削ることで、00分01秒000ミリ秒に次の処理が呼ばれるようにしています。}EngineHandlerの処理EngineHandlerはEngine#handleUpdateTimeMessageで登録され、毎秒ごとにアラートで呼び出されます。呼びだされたらEngine.handleUpdateTimeMessageを呼ぶことでEngine.handleUpdateTimeMessageとEngineHandlerがお互いを呼び合う形となり、毎秒ごとの処理を行います。一秒とはいえ、未来時間で呼び出されるため、これが呼び出された時には既にWatchFaceは使われていない可能性があります。そのようなときに、EngineHandlerがEngineの参照を保持していると、GC出来なくなってしまうので、EngineHandlerではEngineを弱参照で保持します。メッセージを受け取ったら、弱参照のEngineが破棄されていないか確認し、破棄されていなければ、EngineのhandleUpdateTimeMessage()を呼び、描画と次の1秒への登録を行わせます。}標準サンプルではラインによる描画ですが、これでは個性的な時計を作りづらいです。次回はこのサンプルを元に画像を使ったアナログ時計を作ってみましょう。1の概要を訳してみた。Kenzプライバシーポリシ。