ライブ壁紙で世界の時間を表示する

 androidにはライブ壁紙という機能があって、プログラムによって動的な壁紙を作ることができる。キャラクターを画面上で動かすこともできるし、時間ごとに表示内容を変えたりもできるのだ。
 今回は、画面上の地図に地球の昼と夜を描きながら、世界の時間を表示する世界時計壁紙を作ろうと思う。

地球のどの部分が太陽に照らされているか

 地球の昼と夜はどのようにして描かれるのか。
 前の記事でも引用した「日の出・日の入りの計算」(長沢工・著、地人書館)の記述によると、計算する緯度を決めたうえで、太陽の赤経、赤緯、出没する高度を求め、そこから時角を求める。その時角と、赤経、恒星時から、日の出、日の入りの経度が求まるということである。詳しくは同書を参照されたい。
 まずはこの計算がうまくいくかどうかを確かめた。
 世界地図には、NASAからダウンロードした地球の写真をつかう。

 ここに、計算した日の入りと日の出の経度から、夜の側に色を付けると、下の画像のようになった。

 うまく夜と昼を描き分けられているようだ。それでは、Androidのライブ壁紙に表示してみよう。

ライブ壁紙を作る

 ライブ壁紙はWallPaperServiceを継承したクラスを作り、WallpPaperService.Engineを使って描画する。
 流れとしては、アプリが起動するとWallpPaperServiceのonCreateEngineが呼ばれるので、WallpPaperService.Engineを継承したクラスを生成してそのインスタンスを返す。このクラスはSurfaceの生成・破棄やタッチイベントの発生などのタイミングでその呼ばれるので、その都度処理を行うのだ。

public class WorldClock extends WallpaperService
 {

  @Override
  public void onCreate() { }

  @Override
  public Engine onCreateEngine()
   {
    //このメソッドが呼び出されたら、WallpaperService.Engineを生成して返す
    return new WorldClockEngine();
   }

  @Override
  public void onDestroy() {}
  

  class WorldClockEngine extends Engine
   {
    //描画には、Runnableを実装したクラスなどを用意しておく
    private MapRenderer renderer; 

    @Override
    public void onCreate(SurfaceHolder surfaceHolder)
     {
      //SurfaceHolderを渡して描画クラスを生成。
      renderer = new MapRenderer();

      //時刻の変化はBroadcastReceiverを使うことにした
      registerReceiver(renderer, new IntentFilter("android.intent.action.TIME_TICK"));

      //別のスレッドで実行する
      renderthread = new Thread(renderer);
      renderthread.start();
     }

    //適宜、必要なメソッドをオーバーライドして、挙動をコントロールする
    @Override
    public void onDestroy() {}

    @Override
    public void onSurfaceCreated(SurfaceHolder holder) { }

    @Override
    public void onSurfaceChanged(SurfaceHolder holder, int format, int width, int height) {}

    @Override
    public void onSurfaceDestroyed(SurfaceHolder holder) { }

    @Override
    public void onDesiredSizeChanged(int desiredWidth, int desiredHeight) { }

    @Override
    public void onOffsetsChanged(float xOffset, float yOffset, float xStep, float yStep, int xPixels, int yPixels) { }

    @Override
    public void onVisibilityChanged(boolean visible)
     {
      //描画クラスに適当なメソッドを用意して、画面のON/OFFを通知する。
      if (!visible)
       {
        if (renderthread != null) renderer.suspendTask();
       }
      else
       {
        if (renderthread != null) renderer.resumeTask();
       }
     }

    @Override
    public void onTouchEvent(MotionEvent event) {}
    }
  }

 ライブ壁紙の描画の処理は、別のスレッドで行われるのが一般的のようだ。WallpPaperService.EngineのonCreateが呼ばれる時にSurfaceHolderが渡されるので、それを描画を行うクラスに渡して、別のスレッド上で実行するようにする。
 上記の地図をその描画クラスで書かせれば、壁紙に表示できるようになるだろう。このテストはJavaで書き、コンソールから実行したのだが、Androidでは少し変更が必要だった。
 PCではBufferedImageを使用したが、これは使えないので、読み込んだイメージはBitmapを使って利用する。考慮しなければいけないのはサイズ。PCでは幅1280で描画したのだが、同じサイズではAndroidではうまく動かない。大きすぎるのだろう。細かく描いても、実用上は必要ないので、幅640、高さ320でイメージを作り、画面に引き延ばす形で描画するようにした。それを実行した結果が下の画像になる。

 十分実用に耐えるものになっている。

夜の側の描写を改良する

 ここまでは、夜の側は単純に青色で影を表現していたのだが、もう少し凝ったものにしてみる。夜の地球の画像を使って、夜らしくしよう。

 あらかじめ、明るい画像と夜の画像を読み込んでおき、描画用のBitmapを作る時に明るい画像を描いておく。計算で夜の側と判定された部分は夜の画像からピクセルの色を読み、描画イメージに描くことで、塗分けを行うようにすると、以下のようになった。

 一目で夜とわかる感じ。よく見ると南極の氷の形が夜と昼で変わってしまうのはご愛敬。2枚の画像に差があったためだけど、アイコンなんかで分かりにくい部分であるから、こだわらないことにした。明かりが多くない地域は少しわかりにくいが、地形がわかるように明るめに調整したので、何とか場所を読み取ることができそうだ。
 ここで、もう一つ改良を加えた。太陽が地平線の下にあっても、空は少し明るい。これを薄明というのだけど、夜の描画を変更して、日の出、または日の入り近くの夜の側を完全に暗くせずに、明るい画像と夜景画像の色を混ぜてうす暗い感じを描くようにしてみる。
 太陽の高度は計算からわかるので、8度より地平線より低い場合は夜、0度と8度の間は値に応じて色を混ぜることにした。その結果が下のようになる。

 陸地の部分で影が段階的に暗くなっている様子が見て取れる。でも、海の部分はわかりにくいな。まあ、陸地からの色のつながりで、色が変わっていることはわかるけど。
 これを10秒ごとに地図の表示する部分を動かしてみると、こうなる。

とりあえず、これで地球を表示するのはできるようになった。つぎは、主要都市に時間を表示する番だ。

時計を作る

 次に表示する時計の画像を作る。針で時刻を示すアナログ時計は小さいと見づらいので、デジタル時計にした。

 都市の名前と時刻がわかればいいので、単純なものでいいだろう。これを画面に表示してみる。

 世界時計らしくなってきた。

時刻を表示する

 時刻の表示に必要な文字を作る。

 それぞれの数字は、文字ごとに配置を調整する手間を省くために同じ幅、高さにそろえた。時計の画像はすべての時計で同じものを使うから、左上からの位置は同じものが使える。これを東京の時計に書いてみると

 もう完成じゃないですかね?

世界の主要都市の時計を表示する

 世界の都市の中からいくつかを選んで、表示してみることにする。時刻はCaleandarクラスに現在のミリ秒での時刻を入力して、TimeZoneを変更することで得る。ZonedDateTimeを使うことも考えたけど、Android8.0でないと使えないので、断念した。

 これはアメリカの部分。シカゴとロスアンゼルスを表示させた。きちんと時刻があっている。TimeZoneは夏時間の期間内かを返すメソッドを持っているから、サマータイムには時間が1時間進められるだろう。
 この調子で都市を増やしたいのだけど、コードの中にデータを書くのではなくて、jsonファイルを読み取る形をとった。Androidの場合はorg.jsonパッケージのインポートでjsonが扱える。一つの都市で、名前、タイムゾーン名、位置が必要だから、東京の場合、
{ “cityname”: “tokyo” , “zone” : “Asia/Tokyo”, “positionX”: “1060”, “positionY” : “360”}
となる。時計の画像とこのデータを都市の数だけ用意すればいい。
 時計を増やした画像が下の通り

都市を選ぶ画面を作る

 主要都市がたくさんある地域だと、こんなカオスな感じになってしまった。

 表示する都市を選ぶ画面を作ってみよう。
 AndroidManifest.xmlにリソースのxmlファイルを追加する。ファイル名はwallpaper.xmlとした。

  <meta-data
          android:name="android.service.wallpaper"
          android:resource="@xml/wallpaper" />

 このxmlの中で設定画面を表示するActivityを記述してやればいい。
wallpaper.xml

<wallpaper
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:thumbnail="@drawable/thumbnail"
    android:description="@string/description"
    android:settingsActivity="app.util.livewalllpaper.ClockSelectionActivity" />

 ついでに、説明やアイコン画像も決めておいた。
 すると、壁紙の選択画面で、設定の項目が出現

 タップしてやると、設定の画面が出る、という寸法だ。

 ほかに設定する項目を作らなかったので、画面に都市をリスト表示して、チェックボックスで選択する様にした。この画面は壁紙の実行ではなくプレビュー画面の一部なので、壁紙が実行中の場合は選択が変わったことを知らせてやる必要がある。そこで、選択された都市の名前はJSONファイルとして内部領域にファイルとして保存し、壁紙が地図を更新する時にファイルのタイムスタンプを確認することで、データの再読み込みを行うようにした。

バッテリー消費を抑える

 電源スイッチを押したり、一定時間が経つと画面が消えるが、何もしないままだと壁紙は動き続けてしまうので、動作を停止するようにした。
 WallpaperEngineにあるonVisibilityChangedが画面のOn/Offのたびに呼び出だされるので、そのタイミングで描画を行うスレッドの停止と再開を行う。

 長時間画面が消えた状態でも、バッテリーは減っていない。これなら使えそうだ。

シェアする

  • このエントリーをはてなブックマークに追加

フォローする