見出し画像

盆栽愛好者向けスマートウォッチを探す旅18 あれからずいぶん経ちました

前回記事

では悪戦苦闘しながら、自作のウオッチフェイス「Bonsai Face 2」をGoogle Play ストアにて公開することができました。

現在のGoogle Play ストアでのステージは、「早期テスト」です。ここで、たくさんのユーザーに使ってもらい、改良点をもらいながら良くしていくことになるのですが、実際にどれぐらいの方が使ってくださるのかがわかりません。しばらくは、このままにしておき、様子を見たいと思っています。

これからの取り組みについて

前々回の記事では、有料版を作っていこうかと考えていたのですが、有料版を公開するには、開発者情報で住所を公開しなければならないため、二の足を踏んでいます。

あくまでも、趣味者としてのスタンスでアプリ作成を行っていきたい。という思いから、当面は有料版の作成は考えないことにします。

ということで、前回記事からずいぶんと時間が経ってしまいました。というのも、ある程度、自分で満足できるウオッチフェイスができてしまい、テストを兼ねて使っているうちに、とりあえず使えるレベルになってしまったからです。

とはいえ、激しくダメダメの問題点もあり、そこはそこで直さなければならないのですが、直して公開するよりも、今のままでいいか。となってしまっています。

ではなぜ、この記事をアップするのか?

理由は新しくやりたいことができたからです。
Maker Faire 2023 Kyotoに初めて参加しました。

想像を超えるたくさんの方々の出展があり、ひとつずつ丁寧に見て回ったつもりですが、それでも回り切れていない感があります。

ただ、その中で多くの学びがあり、そこへ向かってチャレンジしてみたいと思うようになりました。

現時点でバッファされている記事をフラッシュするという感じなのです。

Bonsai Face 2 のブラッシュアップを目指します。

そのためには、現在公開中のBonsai Face 2のレビューが必要です。

Bonsai Face 2 のレビュー

いいところ

・かわいい
・時間を見るのが楽しい
・よく見ないと時間がわからないところが楽しい

現在の課題

・時刻がわかりにくい
・長針と短針がずれている?
・重なり順はこれでいい?
・アプリアイコンが標準のまま
・Playストアのコピー、画像も凝りたい

どんな機能が必要か?

・時刻表示
・日時表示
・ドッキング
・タップに対応する対応

これらを解決するために

私の中では一つの方針がありました。
Bonsai Face 2の下敷きとなったプログラムは、Android Studioのテンプレートでした。
このサンプルは極力シンプルに書かれていると思うので、自力で機能アップさせる必要がありますが、GitHubのWearOS用のサンプルが更新されていて、WatchFaceについても新しくなっていました。

の中のWatchFaceKotlin

https://github.com/android/wear-os-samples/tree/main/WatchFaceKotlin#readme

以前も確かチャレンジした記憶があるのですが、ビルドできなかったか、何かであきらめていたサンプルです。
これをベースにすれば、新しい機能も労せずして手に入れることができるのではないか。
そう思っているのです。

ちょっと気になるのは、どのような工程で進めればよいかです。
サンプルをベースにして、Bonsai Face 2の描画部分を持ってくるとします。
プログラムは大きなトラブルなくできそうですが、そうやって作ったアプリをGoogle Play ストアはバージョンアップとしてみなしてくれるのか。そこが心配です。

と思いながら、サンプルのプロジェクトを眺めていますが、BonsaiFace2で利用したサンプルとかなり変わっています。
Playストアの審査を恐れるよりも、まずは目の前のアプリをきちんと作れるかを考えたほうがよさそうです。

サンプルのプロジェクトを「BonsaiFace2B」に名称変更しました。

このあたりのネーミングセンスがどうしようもない所なのですが、嘆いても仕方ないのでそのまま進めていきます。
複数の環境にソースプログラムを置いてますがGitやGitHubをきちんと理解しないまま使っているので、「BonsaiFace21」、「BonsaiFace22」のようなフォルダがたくさんできています。

本当は「BonsaiFace2」のまま管理したいのですが、今の知識ではできないため、かといって枝番をつけるのも以前のフォルダとバッティングしてしまう可能性がありできません。「BonsaiFace2B」の「B」は困った末の「A」、「B」、「C」、...の「B」です。

ソースを読み込んでいきます。

数か月の勉強の成果で、Androidアプリのスタート地点がどこにあるか感覚的にわかるようになってきました。
AnalogWatchFaceService.ktファイルの中で定義しているAnalogWatchFaceServiceクラスがメインになります。

私の中では、今やオブジェクト指向プログラミングでのクラス定義の解読は「絡み合ったタコ」をほどく作業ととらえています。
たくさん伸びた足のうち、細いものはあまり気にせず、頭になる部分と太い足を見つけ出すことを行います。

AnalogWatchFaceService.kt:
AnalogWatchFaceServiceクラスの中でAnalogWatchCanvasRendererクラスを使用しています。

AnalogWatchCanvasRenderer.kt:
AnalogWatchCanvasRendererクラスの中でrender関数をオーバーライド。その中でdrawClockHands、recalculateClockHands、createClockHandと渡り描画しています。

引数canvasを用いて、針の描画を行っています。
今の所、あまり深入りせずこれぐらいでやめておきます。

今度は以前作ったBonsaiFace2のプロジェクトファイルを見て内容を思い出します。

MyWatchFace.kt:
MyWatchFaceクラスの中で、Engineを呼び出しています。
EngineクラスでinitializeWatchFace関数、onDraw関数をオーバーライドしており、drawWatchFace関数で描画しています。

ということは、drawWatchFace関数を持っていき、drawClockHands関数から呼び出せばよい。

ということがなんとなくわかりました。
実際には、きれいにモジュール化していないので、後から付け足す箇所が大量にあると思いますが、今回の改造のポイントが見えてきました。

実際にやってみます。

まず、何も考えずに、drawWatchFace関数をAnalogWatchCanvasRendererクラスの最後に付け加えます。

変数mCenterX、mCenterY、mTickAndCirclePaint、mCalendar、sHourHandLength、mBranchSBitmap、mBackgroundPaint、sMinuteHandLength、mBranchLBitmap、mMatsubaLBitmap、mMatsubaSBitmap、mAmbient、mMatsubaBitmapが赤く表示されました。
drawWatchFace関数の外で宣言された変数などです。
これらの部分を一つずつ丹念に縫合していきました。

まず、最初にコンパイルが通るようにエラーをなくしていきますので、自分の直感を信じて、変数の宣言、初期化、使用を行います。
できるだけ、コピペで済ませるようにしていますが、スコープの違いで書き換えが必要な個所が出てきます。

ひとまず、エラーの個所を5か所にすることができました。
ここまでのペンディング事項は、initializeBonsaiWatchFace関数をどこから呼ぶかと、変数mCalendarの置き換えです。
元のソースではonCreate関数から呼び出されていましたが、新しいサンプルでは、onCreate関数が見当たりません。
適切な場所から呼び出す必要があります。

また、変数mCalendarの置き換えについては、Engineクラスの中で以下のように使われていました。

private lateinit var mCalendar: Calendar
    :
mCalendar.timeZone = TimeZone.getDefault()
    :
mCalendar = Calendar.getInstance()
:
mCalendar.timeInMillis = now
:
val seconds = mCalendar.get(Calendar.SECOND) + mCalendar.get(Calendar.MILLISECOND) / 1000f 
val secondsRotation = seconds * 6f 
val minutesRotation = mCalendar.get(Calendar.MINUTE) * 6f 
val hourHandOffset = mCalendar.get(Calendar.MINUTE) / 2f 
val hoursRotation = mCalendar.get(Calendar.HOUR) * 30 + hourHandOffset

時間を表す重要な変数のようです。
新しいサンプルではどのようなものを用いているか調べました。

drawClockHands関数内で計算が行われていました。

// Retrieve current time to calculate location/rotation of watch arms.
val secondOfDay = zonedDateTime.toLocalTime().toSecondOfDay()

// Determine the rotation of the hour and minute hand.

// Determine how many seconds it takes to make a complete rotation for each hand
// It takes the hour hand 12 hours to make a complete rotation
val secondsPerHourHandRotation = Duration.ofHours(12).seconds
// It takes the minute hand 1 hour to make a complete rotation
val secondsPerMinuteHandRotation = Duration.ofHours(1).seconds

// Determine the angle to draw each hand expressed as an angle in degrees from 0 to 360
// Since each hand does more than one cycle a day, we are only interested in the remainder
// of the secondOfDay modulo the hand interval
val hourRotation = secondOfDay.rem(secondsPerHourHandRotation) * 360.0f /
    secondsPerHourHandRotation
val minuteRotation = secondOfDay.rem(secondsPerMinuteHandRotation) * 360.0f /
    secondsPerMinuteHandRotation

細かい意味まできちんと調べ切れていないですが、これを元のソース

val seconds =
    mCalendar.get(Calendar.SECOND) + mCalendar.get(Calendar.MILLISECOND) / 1000f
val secondsRotation = seconds * 6f

val minutesRotation = mCalendar.get(Calendar.MINUTE) * 6f

val hourHandOffset = mCalendar.get(Calendar.MINUTE) / 2f
val hoursRotation = mCalendar.get(Calendar.HOUR) * 30 + hourHandOffset

に縫い合わせました。ふう。

仕事でのプログラミングではこんなお気楽さはありません。
すべて内容を理解してから、コードを持ってくるか、もしくは自らの手でコーディングします。

でも、オブジェクト指向プログラミングをするということは、案外こういうことかと腑に落ちたところもあります。
あまり詳細に深入りせず、つなぐという方法があながち間違いではない感じがしてきました。

あとは初期化をどこでするかの問題のみです。

AnalogWatchCanvasRendererクラスの中に、initというメソッドがあったので、そこに突っ込んでみました。

init {
    scope.launch {
        currentUserStyleRepository.userStyle.collect { userStyle ->
            updateWatchFaceData(userStyle)
        }
    }
    initializeBonsaiWatchFace()
}

あとで、デバッガで確認すればいいでしょうか。
これでコンパイルエラーをすべてなくすことができました。

最初の実行を行います。

正常動作しません。

エミュレーターでは時計板を表示しようとして、何度も失敗している感じです。
デバッガでトレースしながら問題を探っていきます。

たぶんここかなという場所を見つけました。

最初にビットマップを描画するときに、落ちている感じがします。

canvas.drawBitmap(mBranchSBitmap, hMatrix, mBackgroundPaint)

mBackgroundPaintに問題がありそうです。

調べてみると、

private lateinit var mBackgroundPaint: Paint

で宣言したものの、初期化をしていませんでした。
initializeBonsaiWatchFace関数内ではmBackgroundPaintを初期化しておりませんでした。

initializeBonsaiWatchFaceBackground関数を作り、initで呼ぶようにしました。

init {
    scope.launch {
        currentUserStyleRepository.userStyle.collect { userStyle ->
            updateWatchFaceData(userStyle)
        }
    }
    initializeBonsaiWatchFaceBackground()
    initializeBonsaiWatchFace()
}

大きさはでたらめでしたが、とにかく動き出しました。

ここから、少しずつ修正していきたいと思います。

背景のビットマップを描画しました。

全体的にサイズがでかいのと、中心がずれているようです。
元のソースを見てみると、ビットマップを読み込んだのち、描画までの間にリサイズの処理が入っていました。

ダメもとで適当に元のソースを引用してビットマップのリサイズを行う関数を一つこしらえ、呼び出してみました。

private fun resizeBonsaiWatchFace(bounds: Rect) {

    /* Scale loaded background image (more efficient) if surface dimensions change. */
    val scale = bounds.width().toFloat() / mBackgroundBitmap.width.toFloat()

    mBackgroundBitmap = Bitmap.createScaledBitmap(
        mBackgroundBitmap,
        (mBackgroundBitmap.width * scale).toInt(),
        (mBackgroundBitmap.height * scale).toInt(), true
    )

    mMatsubaLBitmap = Bitmap.createScaledBitmap(
        mMatsubaLBitmap,
        (mMatsubaLBitmap.width * scale).toInt(),
        (mMatsubaLBitmap.height * scale).toInt(), true
    )
    mMatsubaSBitmap = Bitmap.createScaledBitmap(
        mMatsubaSBitmap,
        (mMatsubaSBitmap.width * scale).toInt(),
        (mMatsubaSBitmap.height * scale).toInt(), true
    )
    mBranchLBitmap = Bitmap.createScaledBitmap(
        mBranchLBitmap,
        (mBranchLBitmap.width * scale).toInt(),
        (mBranchLBitmap.height * scale).toInt(), true
    )
    mBranchSBitmap = Bitmap.createScaledBitmap(
        mBranchSBitmap,
        (mBranchSBitmap.width * scale).toInt(),
        (mBranchSBitmap.height * scale).toInt(), true
    )
    mMatsubaBitmap = Bitmap.createScaledBitmap(
        mMatsubaBitmap,
        (mMatsubaBitmap.width * scale).toInt(),
        (mMatsubaBitmap.height * scale).toInt(), true
    )

}

一回でうまく行ってちょっと怖いぐらいです。

今後は、描画順序が気になります。せっかくのスロットが松の枝で隠されてしまっています。
描画順序を変更して、スロットを最前面に持ってきました。

うーん。やりたいこととちょっと違うなぁ。

松葉を半透明にするのはどうでしょうか。

こんな感じで半透明にしています。
勉強の成果で、少しネットで調べたのち、どこをどのようにいじったらいいのかが少しわかってきたような感じです。
あまり無茶をせずできそうな範囲のことをやる分には少しの変更でできるようです。

すごいなKotlin

今の状態だと、ビットマップが全て半透明になっている感じ。
現実のものに置き換えると、薄いビニールでできた針のようです。
ベースのイラストは透けていなくてもよいと思い、それぞれの透明度を調整してみます。

前のほうが良いような気がしますが、微調整は最後に行うことにします。
今の所、サンプルの描画を消さずに上書きで描画しているため、文字盤は2重に描かれているし、針も重なって表示されています。

BonsaiFace2のコンセプトは、松の枝で長針、短針を表現する。
でしたが、私が日常生活で使用してみて、時間がほとんど分かりません。
結局、スマートウォッチ以外のもので時間を確認しています。
これは大変問題で何とかしたいとは思うものの、そんないい加減なところもとても好きで大事にしたいと思うところです。

そこで考えた解決策は、

文字盤をタップしたら、文字で時間を表示するというものです。
できるかどうかも分かりませんがチャレンジしてみます。

テスト表示はできましたが、時刻の表記がうまくできるかはまだ分かりません。
漢数字表示を目指していますが、できなければ、あっさりアラビア数字でもよいでしょう。

余計な表示を取り除き、描いてみました。

さいごに

実はここまでの記事の原稿を書いていて、実際に稼働テストをし始めて、そ
のまま気に入って使っています。

少し気に入らない点もあるのですが、直すには調べることが多すぎてやる気がなくなってきています。

設定画面では、図柄がバラバラに表示される不具合を抱えるため、ソースの公開もストアの更新も行っていません。

一時期は今、スマートウオッチの全てを知って、もっとセンサー類を使いこなし、おもしろいアプリを作りまくるぞと意気込んでいた時もありましたが、今苦労しなくても、そのうちもっといいサンプルが提供されて、それに乗っかっていけばいいかなとも思うようになってきました。Kotlinをかじったので、そのあたりの感覚が芽生えたような気がします。

#GooglePixelWatch #ウオッチフェイス #Kotlin #盆栽柄 #長針短針が松のイラストです


この記事が気に入ったらサポートをしてみませんか?