月別: 2015年4月

AsyncTaskLoaderの動きを確認中 その2

この記事は最終更新から3ヶ月以上が経過しています。情報が古い可能性があります。

前回の続きでAsyncTaskLoaderを使ったサンプルを作って、Loaderの動きを確認していたのですが、1つの問題点にぶち当たりました。

initLoaderでLoaderを動かす分にはとてもスッキリしたのですが、restartLoaderを使うと非同期処理がイメージ通りに動きませんでした。

それは以前の非同期処理が終わらないと、restartLoaderで新しく動かす非同期処理が始まらないということです。

私が作ったサンプルでは、指定した数字までカウントアップを行う非同期処理をしています。しかし非同期処理中にrestartLoaderを呼び出すと、今動いている非同期処理が終わらないと新しい非同期処理が動いてくれないのです。

restartLoaderを呼んだら今動いている非同期処理には停止してもらい、すぐに新しい非同期処理が始まって欲しいです。使いもしない非同期処理の終了を待つのは時間の無駄ですし、使いもしない処理にリソースを割くのももったいないです。

## ソースコードを読んで分かったこと

現在進行形で格闘しているので、まとまっていないですがこんな感じ。

  • onStopLoadingはActivityがonStopになったときに呼ばれる(画面回転時は除く)
  • キャンセルの処理(restartLoader実行時)は、まずonCancelLoadが呼ばれる
  • AsyncTaskLoader.onCancelLoadでLoaderの状態に合わせてキャンセル処理を行う
  • 実際のキャンセル処理はcancelLoadInBackgroundメソッドで行われる
  • しかしAsyncTaskLoader.cancelLoadInBackgroundでは何もしていない
  • すなわち実際にloadInBackgroundの処理を止めるのは自分で実装しなければならない
  • AsyncTaskLoader.onCancelLoadを経ていれば、loadInBackgroundの処理結果は最終的にonCanceledに通知される
  • LoaderManagerがうまいこと管理してくれているので、restartLoader呼んだ数だけ非同期処理が乱立するわけではない(それでもいくつか並行して走るけれども)

そもそもLoaderManagerは何している?

LoaderManagerはrestartLoaderが呼ばれた時に何をしているのかも、同時進行で読み解いています。

LoaderManagerはLoaderをmLoadersとmInactiveLoadersという2つのリストで管理しています。

mLoadersでは現在実行中のLoaderを、mInactiveLoadersでは以前実行されていたLoaderを管理しています。mInactiveLoadersはLoaderを破棄するためのもののようです。おそらく。

restartLoaderをすると、LoaderManagerはLoaderの状態によってあれやこれやしながら新しいLoaderを作成します。現在実行中のLoaderがあればキャンセル処理を行いますが、新しいタスクはmPendingLoaderに登録します。

mPendingLoaderが何者かというと、その名が表すように次に実行される非同期処理のタスク(Loader)です。このmPendingLoaderがいつ実行されるのかというと、今実行されているタスクのloadInBackgroundが終了した時です。

そのため実行中のタスクが終わらないと、restartLoaderで作られた新しいタスクが始まらないのです。

AsyncTaskLoader上のキャンセル処理

Loaderの非同期処理が実行されているときにキャンセルがかかると、以下の場合にonCanceledが呼ばれます。

  • Loaderが非同期処理実行中の間に、Activity等でinitLoader().forceLoad()をしたとき
  • Activity等でrestartLoaderを呼んだ時(非同期処理が実行中かは問わない)

AsyncTaskLoader.onCancelLoad()でキャンセル関連の処理が行われているためsuper.onCancelLoad()を呼ぶ必要があります。

ただしやってるのはLoaderの管理情報の更新だけで、実行中のloadInBackgroundを止めるような処理は何もしていません。

具体的には、、現在実行中の非同期処理があるか確認(mTask != null)し、タスクがなければキャンセル対象がないので何もしません。

ある場合には、キャンセル処理中の非同期処理があるかを確認します(mCancellingTask != null)。

mCancellingTaskがある場合、onCancelLoadが呼び出されたLoaderがPendingTaskなら破棄します(mTask.waiting == true)。これは実行待ち状態のLoaderをキャンセルすることを意味しています。実行待ちのタスクはまだ開始されてないから破棄するだけでいいわけです。

mCancellingTaskがない場合は、onCancelLoadが呼び出されたLoaderがPendingTaskか確認します。上と同じことをやっていますが、mCancellingTaskがない場合、このLoaderをキャンセルされたタスクとして退避させる必要があるので条件分岐されてます。

で、PendingTaskであればLoaderをそのまま破棄します。まだ非同期処理が始まっていないのでそのまま破棄するだけでいいからです。

PendingTaskでないのであれば、このLoaderは現在稼働中の非同期処理ということになります。そこでこれをキャンセルし、mCancellingTaskへと退避します。その上でcancelLoadInBackgroundを呼び出します。

そのため、Loaderをキャンセルするための処理は、cancelLoadInBackgroundで実装すればいいことになります。

cancelLoadInBackgroundで何をすればいいか

Loaderをキャンセルするための処理を実装するといっても、具体的にどう実装すればいいかというとよく分かりません。

このメソッドの中からloadInBackgroundの処理を停止させることはできないでしょう。むしろこのメソッドは、メインスレッド(Activityとか)からLoaderを停止させるためのメソッドのような気がします。

しかし直接ActivityからこのcancelLoadInBackgroundを呼ぶと、LoaderManagerの管理下から外れた動きをすることになって、変なことになりそうな気がします。

結局のところ、loadInBackgroundの中でisLoadInBackgroundCancelled(Loaderがキャンセルされたらtrueになる)をチェックして非同期処理を途中で止めるように実装するしかなさそうです。

新たな謎

今気づいたんですが、ActivityからLoaderのforceLoadを呼んだ後でrestartLoaderすると、前の非同期処理完了を待つことなくrestartLoaderした処理が走っていることに気づきました。前の処理は走ったままなので、2つの非同期処理が並列で走ってますけども。

この違いはいったいどこからやってくるのか・・・。

ちなみにサンプルはGitHubで公開中です。

重い腰をあげてLoaderを使ってみた(とりあえずinitLoaderだけ)

この記事は最終更新から3ヶ月以上が経過しています。情報が古い可能性があります。

重い腰をあげてLoader触ってみました。

これまでもAsyncTaskはやめろ、Loader(AsyncTaskLoader)使えっていう話は知ってはいたんですが、Loader使い方よく分からんって敬遠してたんですよね。

とりあえずAsyncTaskLoaderを使ってみてわかったこと、感じたことを書いてみたいと思います。

参考にしたところ

サンプルはGitHubに上げてます。

未だによく分かってないところもあるんですが(キャンセル処理についてはまだ手を付けていない)、とりあえず現状で分かったことを書いてまとめます。

ちなみにソースコード読んで動きを把握したいなら、サポートパッケージではなくandroid.app.LoaderManager、android.content.Loaderを使った方がいいと思います。

LoaderManagerの動きを知る

getLoaderManager.initLoaderを呼ぶより前に、LoaderManager.enableDebugLogging(true);を実行すると、LoaderManagerがログを出力してくれるようになるので便利。

LoaderManagerがLoaderの状態を管理しているので、ActivityやAsyncTaskLoaderは非同期処理がどんな状態にあるのか気にしなくて済むのがいいですね。

ただしちゃんと動くようにするためには、Loder側でどういう状態の時にどのメソッドが呼び出されるのかを理解しておく必要があります。そのためにはLoaderManagerの動きを知っておかないとわけが分からないというわけです。

ついでに言うと、メソッド名から想定したイメージと実際の動きの間が、私の感覚と違っていて余計に混乱したというのもあります。

getLoaderManager().initLoader

getLoaderManager(もしくはgetSupportLoaderManager).initLoaderは指定したIDのLoaderがなければLoaderを作成、既に存在していればActivityへのCallbackを設定します。

Loaderを初期化するメソッドというより、コールバックを更新するものと思った方が理解しやすい気がします。私はずっとこのメソッドでLoader作って非同期処理を開始するものだとばかり思っていて、ずっと混乱していました。

指定されたIDのLoaderがまだ存在しない場合は、ActivityのonCreateLoaderにコールバックを行い、ここでLoaderを作ります。

Loaderを作るのはActivityのお仕事です。initLoaderだけなら、onCreateLoaderが呼ばれるのはActivityがonCreateされたとき(画面回転時は除く)だけです。

Loader.onReset

名前からして再稼働させた時に呼ばれるのかと思って混乱しました。いまだによく分かっていません。

onResetが呼ばれたLoaderは再利用されることはない・・・であってると思うんですけど、自信がありません。

LoaderManagerは、LoaderのIDごとに現在動いているLoader、以前に使ってたLoaderを管理しているだけなのようです。だから以前使ったLoaderのインスタンスを再活用したりはしてないと思います。

Loaderで使ってたリソースを解放してねってタイミングのようです。

バックグラウンド処理を引き継げる

とりあえずやってみて感じたのは、AsyncTaskと違ってバックグラウンドの処理を引き継げることがいいなと感じました。

AsyncTaskだと画面回転したらまた最初からやり直しになってたものが、そのままバックグラウンド処理は続いてくれるし、結果もそのまま受け取れるのが素敵。

Loader側でキャッシュ機構を持たせることで、ムダな非同期処理を防ぐことができるのもいいなと思います。

Loader側が非同期処理だけに専念できる

AsyncTaskと違って呼び出し元のActivity(Fragment)が生きてるかどうかを確認しなくていいのが想像以上にやりやすいです。

AsyncTaskだとonPostExecuteで処理結果をUIに反映します。バックグラウンド処理をしている最中に画面回転が生じるとUI更新しようとするものの、対象のActivityは既にお亡くなりになっているせいでアプリが落ちてました。

LoaderではUIの更新について何1つ考える必要がないので、とてもスッキリします。

でも途中経過を伝えられない

AsyncTaskLoaderには途中経過を通知するメソッドが標準で用意されていません。そのためバックグラウンド処理の途中経過を表示することができません。同じAsyncTaskがつくのに別物と

対策としてはAsyncTaskLoaderにActivityへの参照を持たせて通知するみたいなやり方がありましたけど、そんなことするとLoader使う意味が無い気がします。せっかくの非同期処理とUI処理を分離するための機構が台無しです。

LocalBroadcastで対応するっていう策も見つけましたが、こっちの方がまだましかなと思います。

しかし、いっそのことプログレス表示しないという選択肢もありなのかもしれません。データが全部で10件あって、そのうち◯件処理済みとか表示するのではなく、データ読み込み中ですよって表示するだけ。そういう方向での切り替えをした方が通知のための仕組みを実装するより安全な気がします。

Android StudioでQuick Documentationを活用する

この記事は最終更新から3ヶ月以上が経過しています。情報が古い可能性があります。

Android Studioでコーディングしていて、「このメソッドどういう処理するんだっけ?」と思った時に便利なQUICK DOCUMENTATIONがあります。

F1押すと出てきますが、標準だとFloating Modeで表示されて邪魔です(設定によって違うかもしれませんけど)。

Floatingモードで表示されて邪魔

これは右上の歯車マークをクリックして、Floating Modeを押して解除してやれば他の枠に収まってくれます。

Floating Modeを解除

私の場合毎回右側にDocumentationが収まるのですが、右側は個人的に見づらい。このDocumentationはドラッグすることで表示位置を移動することができます。

個人的には右下に置いておくのが好みです。

表示位置は調整可能

こうすることでQuick Documentation機能がぐっと使いやすくなります。

これでめでたしめでたしなんですが、このままではDocumentationの表示位置は現在開いているプロジェクト固有の設定にしかなりません。

つまり、別のプロジェクトを作成してQuick Documentationを利用した時に、再びFloating Modeで開かれてしまいます。プロジェクトごとに毎回この設定をするのは面倒なので、これを標準のレイアウトとして設定してしまいましょう。

方法はAndroid StudioのWindowメニュー→Store Current Layout as Defaultを選べばOKです。

標準のレイアウト設定にしてしまう

これで毎回設定せずともすみます。

Android Wearアプリを開発するときはversionCodeなどを一元管理すると便利

この記事は最終更新から3ヶ月以上が経過しています。情報が古い可能性があります。

Android Wearアプリ(WatchFaceも)をGoogle Playで公開するときにbuild.gradleの共通化をやっておいた方がいいと思います。

Android Wearアプリプロジェクトを作成すると、標準ではmobileモジュールとwearモジュールが作成され、それぞれのモジュールにbuild.gradleが作成されます。

Google Playにアプリを公開する場合、build.gradleで指定するversionCodeとversionNameはmobile,wearモジュールで共通にしなければなりません。

初回アップロード時は両方同じ値なので問題ありませんが、アプリをバージョンアップする際に2つのファイルをいじらないといけないのは面倒くさいと思います。(というか絶対に忘れる)

そのためversionCodeなどは、一箇所直せばmobileとwearのどちらにも適用されるようにしてやるといいと思います。

私はmobile,wearのbuild.gradleで共通して利用する部分を、別ファイルにして読み込ませるようにしてみました。

QiitaのAndroidの署名情報(signingConfigs)を外出しようを参考にさせていただきました。

/mobile/buildConfig.gradle

defaultConfig {
    applicationId "jp.gcreate.product.customphotowatch"
    minSdkVersion 18
    targetSdkVersion 21
    versionCode 3
    versionName "1.0.2"
}
```

/mobile/build.gradle

apply plugin: ‘com.android.application’

android {
 apply from: ‘configBuild.gradle’, to: android
 compileSdkVersion 21
 buildToolsVersion “21.1.2”
}

〜dependenciesは省略


/wear/build.gradle

apply plugin: ‘com.android.application’


android {
 apply from: ‘../mobile/configBuild.gradle’, to: android
 compileSdkVersion 21
 buildToolsVersion “21.1.2”

 defaultConfig{
 minSdkVersion 20
 }
}

〜dependenciesは省略
“`

上記では省略しましたが、buildTypeも外部ファイルに出して両者で同じ設定が適用されるようにしてます。

やってて未だに不安なのが、ちゃんと正しく設定できているのか、確認の仕方がいまいち分からず不安だということでしょうか・・・。

先日のDroidKaigiで発表のあった、つかえるGradleプロジェクトの作り方のやり方も参考になります。

こちらのスライドでの方法は、versionCode等の値を/build.gradleで定義し、各々のプロジェクトその値を参照することで共通化するやり方です。

こちらのやり方のほうが分かりやすいなぁって発表聞いてて思いました。

ちなみにAndroid Studioではルートのことをプロジェクト、mobileとかwearのことをモジュールと呼びますが、Gradleの世界ではどれもプロジェクトと呼ぶそうです。勉強になりました。

DroidKaigiに参加してきました

この記事は最終更新から3ヶ月以上が経過しています。情報が古い可能性があります。

DroidKaigiに行ってきました。飛行機使って前泊での参加です。

満員でしたね。熱気がすごい。

今日ほど分身の術が使えたらと思うことはなかったでしょう。それくらい、全部のセッション聞きたかったです。

今後のアプリ開発に役立つ情報がいっぱいでした。現状動いてはいるけど、ちゃんとできてなかったところとか、「そうなんだ」っていう気付きもあって有意義でした。

とりあえず面倒臭がらずに少しずつテストを書くことから始めようかなと思います。

テストを動かす環境づくりがよくわからないとか、テストの書き方がよく分からないとか、個人でやってると仕様の変更で対応してしまってテストまで書き換える必要が出てきて面倒くさいとかで敬遠してたんですけど、つべこべ言わずにテスト書こうと思いました。

一方で、周りの空気に飲まれて受け身になりすぎたのが反省点です。なんかもうちょっと攻めの姿勢で聞けたら良かったのにと、振り返って思います。

質問とかできたら良かったんですけど、頭働かなくてそれどころではありませんでした。

言い訳ですけど、参加してる間はそれどころではなかったんですよね・・・。周りスゲーし、人はいっぱいだし、席の確保も大変だし、スケジュールは過密だしで話を聞くのが精一杯でした。

幸い皆さんスライドを公開してくださっている上に、スライド見るだけでも話の内容がある程度分かるようになってるので、後でじっくり復習したいと思います。

個人でアプリ作ってると、こういった周りの開発者さんたちの空気感とか全くわからないので、参加してよかったなと思います。わざわざ岡山から出張った甲斐はあったかな・・・。

運営の皆さん、発表者の皆さん、参加者の皆さん、お疲れ様でした。

Master of Fragmentの更新を首を長くして待ってます。

Android Wearでwearとスマホ間でデータをやりとりする話

この記事は最終更新から3ヶ月以上が経過しています。情報が古い可能性があります。

データのやりとりはWearable.DataApiを使うことでやりとりできます。Wearable.MessageApiを使うことでもできます。

両者の違いはこんな感じ。

DataApi

  • 接続が切れていても送信できる
  • データはonDataChangedで受け取る
  • 送れるデータは100KBまでだが、Assetを使うことで大きいデータも送れる
  • データを送信するというより、DataItemを更新して、その更新を通知するイメージ

MessageApi

  • 現在接続中のノードに対してデータを送信することができる
  • データはonMessageReceivedで受け取る
  • データを送る際にはノードを指定する必要がある

ノードIDについて

当たり前ですが、ノードIDはAndroid Wear端末とスマホで異なります。

WatchFaceを作成して、その設定画面を用意している場合、WearのノードIDはスマホ側では簡単に取得できます。

mobile側の設定画面となるActivityでgetIntent().getStringExtra(WatchFaceCompanion.EXTRA_PEER_ID)とするとWearのノードIDが取得できます。これは設定画面の起動がAndroid Wearアプリ経由で行われるためです。

一方Wearから、もしくはmobileからでもAndroid Wearアプリを経由しない起動の仕方をするアプリの場合はこの方法では取得できません。

その場合はNodeApi.getConnectedNodesを使うことで、接続されている端末のノードリストを取得することができます。現状スマホとAndroid Wearは1:1でペアリングされるはずなので、これで相手方のノードIDを取得できるでしょう。(将来的に複数ペアリングできるようになったらどうやればいいんでしょうね?)

DataApiを使ったデータ送信の注意点

[DataApi – Android Developers]のConcurrencyにありますが、DataApiを使ったからといってmobileとwearでDataItemが全く同じになるわけではありません。

onDataChanged()内であれば正しいデータが参照できます。これは変更があったDataItemが通知されてきているからです。

しかしAndroid Wear端末を再起動した後に、DataApiを使って送信された設定データを読み込もうとした時に問題が生じます。

wear側からDataItemを識別するUriでデータを取りに行っても、mobile側の設定と齟齬が生じている可能性があります。

その理由はDataItemが以下のように識別されているからです。

wear://ノードID/パス

mobileで作ったDataItemはmobileのノードIDで識別されます。同じパス文字列で識別しているからといって、勝手にwearのノードIDのものが変更されるわけではありません。

これはWatchFaceのサンプルコード(com.example.android.wearable.watchface)を見ると何となく分かると思います。サンプルコードでは、mobileからのDataItemの変更を受け取ると、wear側で同じデータを上書きする処理を行っています。

サンプルのようにmobileからもwearからもデータを送り合うようなアプリの場合、どちらのノードのDataItemも常に同じ状態にするように配慮しないと齟齬が生じて困ることになると思います。

片側からしか送らないというのであれば、ノードIDを指定してDataItemを取りに行くのもありかもしれません。

UndoBarを使った削除処理を考える

この記事は最終更新から3ヶ月以上が経過しています。情報が古い可能性があります。

UndoBar – GitHub

削除処理を行って、それを取り消し可能なようにする実装を考えます。例えばGmailのアプリでメッセージをアーカイブしたときなどに表示されるあれです。

削除する際に「本当に消しますか?」みたいな確認ダイアログを表示するのはナンセンスっぽいです。

なぜならその確認ダイアログは、データが消えてアクセスできなくなるという責任をユーザーに転嫁してるだけだということです。そもそも削除したいだけなのにいちいち確認されるのもうっとおしいだけではないかという理由もあります。

それならば、もし消したくないデータを誤って削除してしまったとしても、それを復元できるようにするのがユーザー目線でいいだろって話です。

この辺りの話はSmashing Android UI レスポンシブUIとデザインパターンという本で知りました。

では実際に取消可能な削除機能を実装するためにはどうすればいいかというと、UndoBarを利用するのが簡単そうな気がします。Crouton使おうかと思ったけども、いざ使おうと思ったらレイアウトを実装したりするのが~~面倒~~大変だったので、シンプルなUndoBarを使いました。ソースコードはCroutonの方が読みやすかったけど。

UndoBarの表示

今回私がやりたかったのは、データベースに保存してあるデータを消すという例です。データベースに保存されているデータをListViewで表示している。そのListViewのうちの1つのアイテムを選んで削除をするというパターン。

実装方法としては削除の操作を行った時(UndoBarを表示させる時点)でどうするかによって、2パターンに分岐するかと思います。

  1. この時点で実際にDB上のデータを消してしまう
  2. この時点ではDBは触らず、見た目上のデータを消す

どちらのパターンにせよ、この時点でListViewに表示されてるデータを消すのは共通です。データベースのデータを消すタイミングがここかどうかです。

1.のパターンは見た目と実装がそのままリンクしています。Listから消えたらデータベースからも消える、シンプルです。

このパターンの場合は操作の取消を選んだら、消したデータを復元する必要があります。そのためどうやってデータをロールバックするのかに頭を悩ませることになります。データベースの構造が複雑だと元に戻すのが大変かもしれません。

2.の場合はUndoといいつつ、実際には削除処理を猶予しているだけです。このパターンの場合、取消操作はデータベースからデータを削除するのを取り消す処理(ややこしい)になります。

こちらは復元処理を考える必要がありません。一方でUndoBarが消える前にアプリを終了したり、画面が回転したときどうすんのっていう新たな問題が生じます。

UndoBarからのコールバック

UndoBarController.AdvancedUndoListenerを使うことで3つのタイミングによるコールバックを受け取ることができます。

この3つのコールバックをうまいこと利用して削除機能を実装していくことになります。

onUndo()

取消ボタンを押した際に呼ばれます。Undo時の処理をここで行うと良いです。

パターン1のときならここで復元処理を行うことになります。

onHide()

取消ボタンが押されず、UndoBarが消える際に呼ばれます。UndoBarに設定した表示時間が経過した後でコールされるわけです。UndoBar表示中に画面回転した場合には呼ばれません。

ちなみに画面回転に対応するには、自分でハンドリングしてやらないといけないそうです。今回私はそこまで踏み込みませんでした。

onClear()

UndoBar.clear()を明示的にコールした際に呼ばれます。

単に画面回転させたら呼ばれるのかというとそうではありません。画面回転時にコールバックを受け取りたいなら、Activity#onDestroy()(もしくはonStop(),onPause())でclear()してやる必要があります。

そのためにはUndoBarのインスタンスをフィールド変数として保持しておかなりといけません。サンプルのように、単にnew UndoBar()して表示させていると、表示したUndoBarを識別できないので、clear()することができません。

ただし単にUndoBarを消したいときとごっちゃになってややこしくなるので、画面回転時にclearするのはよくないかもしれません。回転時は自分でハンドリングして再表示してやるのが一番いいと思います。

削除機能を実装する

1.実際に削除するパターン

onUndo()で削除した対象のデータを元に戻します。このパターンだとここがややこしいです。

消すデータをメモリ上に確保しておいて、Undoされたら消したデータを再インサートする。データベースに削除フラグ持たせてそれを立てる。退避用テーブルにデータを移しておく。・・・どういう方法を取るのがベストなんでしょうかね?

ちなみに私はメモリ上にデータを持たせて、Undoされた際に再登録する方法を選びました。

onHide()は何もしないでいいです。UndoBar表示時点でデータは消えてますので。

onClear()はonHide()と同じ処理でいいでしょう。

2.Undoされなかったら消すパターン

onUndo()はListViewを元に戻すだけでいいです。データベースは変更されていないので、DBから読みなおすなり、消したデータだけAdapterに追加するでもいいと思います。

onHide()で実際にデータベースからデータを消します。

onClear()はonHide()と同様にデータベースからデータを消します。でないとUndoBar表示中に追加で削除処理が実行された際に困ります。

UndoBar表示中に追加の削除処理を行ったとき

UndoBar表示中に更に削除処理を行い新しいUndoBarを表示させたらどうなるか。表示されているUndoBarは以前のもののままです。新しいUndoBarは古いUndoBarの表示時間が過ぎた後で表示されます。

これでは削除対象と表示されてるUndoBarがリンクしないのでよろしくありません。今削除したものを取り消したつもりが、以前処理した奴が取り消されてしまったということになります。

そのため削除処理を行ったら、現在表示中のUndoBarはclear()で消すべきでしょう。

簡単そうでいざ実装してみると大変だった

UndoBarを使って取消可能な削除機能を実装してみましたが、やってみると考えることが多くて意外と大変でした。

それでも自力で実装しようと思うともっと大変でしょうから、ライブラリ作者様には頭が上がりません。

UndoBarはbuild.gradleに1行追加するだけで使えるようになるので、削除処理を実装するときには仕様を検討してみてください。

UndoBar – GitHub

カスタムフォトウォッチ

この記事は最終更新から3ヶ月以上が経過しています。情報が古い可能性があります。

カスタムフォトウォッチ

Android Wearを使っていて、背景画像を自分の好きな画像に設定したいと思ったことはないでしょうか。そんな人にちょうどいいのがこのカスタムフォトウォッチです。

作成のきっかけはデフォルトで設定できるウォッチフェイスの中に、日付と曜日を表示するものがなかったことでした。下にスワイプすれば日付は表示されますが、曜日は出ないしいちいちスワイプするのも面倒くさいので、盤面に常に表示してあるのがほしいなと思ったのです。

今のところデジタル時計だけですが、アナログ形式は要望が多ければ追加するかもしれません。(個人的にアナログは見づらいので優先度低めです)

以下の説明はすべてVer1.0.1のものです。

インストールの仕方

アプリはGoogle Playにて公開中です。


Get it on Google Play

カスタムフォトウォッチを使うためには、Android Wear端末とGoogleのAndroid Wearアプリが別途必要です。どちらが欠けていてもカスタムフォトウォッチは利用できません。

カスタムフォトウォッチをスマホにインストールすると、ペアリングしているAndroid Wear端末にも自動的にカスタムフォトウォッチがインストールされます。(この際、インストールにしばらく時間がかかります)

Android Wear端末にもインストールが完了すると、スマホのAndroid Wearアプリを起動すればウォッチフェイスの中にカスタムフォトウォッチが追加されます。Android Wear端末でウォッチフェイスの変更からカスタムフォトウォッチを設定できますが、基本的な設定はスマホから行います。

カスタムフォトウォッチをウォッチフェイスに設定すると、カスタムフォトウォッチのアイコンに歯車マークが出ます。再度タップするとカスタムフォトウォッチの設定画面が起動します。

ウォッチフェイスの選択画面

ウォッチフェイスの選択画面

カスタムフォトウォッチの設定画面を開くと、最初にこの画面が表示されます。

ここでは壁紙の追加と、既に追加した壁紙への切り替えが行えます。

右下の+ボタンを押すと次の画像の編集画面に移行します。

追加済みの画像を単にタップすると、ウォッチフェイスの設定(時刻の表示設定)に移行します。長押しするとポップアップメニューが開き、ウォッチフェイスの設定画面を経ることなく画像の切り替えのみを行ったり、不要になった画像の削除ができます。

画像の編集画面

画像の編集

この画面で実際にAndroid Wear端末に転送する画像を作成します。

左下のアイコンを押すことで、スマホ内に保存されている画像を読み込むことができます。SDカードやGoogleドライブにある画像を指定可能です。

カメラのアイコンを押せばその場で写真の撮影を行い、その画像を読み込むこともできます。

Android Wearに表示されるのは中心の明るい部分です。範囲外の部分は切り取られます。

画面内に表示した画像はスワイプすることで移動が、二本指でピンチイン・アウトすることによって縮小・拡大ができますので、Android Wearに表示させたい部分が中心になるように調整してください。

枠は四角と円形の切り替えが可能です。フレーム枠の変更は右上にあるアイコンをタップすることで行えます。(メニューからでも可)

ちなみに枠はあくまで表示確認のためのものであり、枠を円形にしたとしても画像は四角で切り抜かれます。あくまで円形ディスプレイのAndroid Wear端末でどのように表示されるか確認するためのものだとお考えください。

枠の切り替えボタンの隣は、周りの半透明の部分を黒色で塗りつぶすボタンです。Android Wear端末に表示される部分が明確になるので、実際に表示されるとどう見えるのか確認したいときにお使いください。もう一度タップすれば元の半透明に戻ります。

表示位置が決まれば、右下のハサミのアイコンを押してください。画像が切り抜かれ、ウォッチフェイスの設定画面に移行します。

ウォッチフェイスの設定(時刻の表示設定)

ウォッチフェイスの設定

この画面ではAndroid Wearに表示される時刻や日付の表示設定が行えます。

変更できるのは文字の色、フォント、サイズです。日付に関しては表示形式も変更できます。

画像によっては色だけでは文字の識別が難しいと思います。そのような場合は縁取りを有効化してください。文字が黒で縁どりされるので視認性が向上します。

文字の設定は都度スマホの画面に反映されます。画像と文字の視認性のバランスをとりながら設定することができます。

文字の表示設定が決まればメニューのチェックマークを押せば設定が完了し、画像がAndroid Wear端末に転送されます。(端末のバックキーを押しても同様です)

ただしアプリをインストールした直後やAndroid Wear端末を再起動した後など、スマホで選んだ画像がAndroid Wear端末に転送されないことがあります。その場合、1分程度間を開けてから再度画像の切り替えを行ってみてください。

Google Playにて公開中

カスタムフォトウォッチ


Get it on Google Play

WatchFaceのサンプルを実行する

この記事は最終更新から3ヶ月以上が経過しています。情報が古い可能性があります。

Android Wearでウォッチフェイスを自作するためには、Creating Watch Faces – Android Developersを見て勉強するといいです。

なんかややこしそうな気がするかもしれませんが、そこまで複雑でもないです。Traningにも書いてありますが、Android StudioでWatch Faceのサンプルを取り込んでやると、どういうことやればいいのかわかると思います。

取り込み方はAndroid StudioのFileメニューからImport Samplesを選び、Wearable > Watch Faceを選択すればOKです。

Watch Faceサンプルを取り込む

サンプルの実行方法

このサンプルを実行する場合、実行環境にバツ印がついています。これはデフォルトで起動するActivityが設定されていないからです。

実行環境にバツ印がついている

そのまま実行しようとすると以下の様な警告メッセージが表示されますが、Continue Anywayを選べばAPKが端末へ転送されます。

気にせず続ける

毎回このメッセージが表示されるのはうっとおしいので、実行環境設定でDo not launch Activityを選んでおいた方がよいでしょう。

Activity起動設定

また、WatchFaceの設定画面を作るなど、デフォルトでは起動しないけど用意してあるActivityを起動したい場合は、「launch」を選んで起動させたいActivityを選んでおくと捗ると思います。(そうしないと、APKの転送→Android Wearアプリの起動→該当のWatchFaceを選択→設定画面を起動という手順を踏む必要があって面倒くさい)

ただ、この手法で起動するとgetIntent().getStringExtra(WatchFaceCompanion.EXTRA_PEER_ID);で、ペアリングしている端末のPeerIdを取得しようとしてもnullになってしまうので、PeerId依存の処理が上手くいかないことに注意しましょう。

WatchFaceの動作確認

アプリ(WatchFace)をどうやって端末に転送するのかというと、mobile(サンプルではApplication)とwearモジュールを両方とも実行(デバッグ)してやります。

そうすればWatch Faceが端末に送信されるので、後はスマホもしくはWear端末からWatch Faceの変更をしてやれば動作確認できます。

デバッグ実行はwearとmobile片方ずつやらないといけない

リリース用の署名をつけたAPKファイルであれば、mobile側の実行(端末へのインストール)さえ行えば、wear用のAPKがペアリングしている端末へ自動的にインストールされます。(mobile側のAPKの中にwear用のAPKが埋め込まれています)

しかしdebug用のAPKはこの自動転送が働かないので、wear用のAPKはwear用のモジュールを選んで実行しないと端末にインストールされません。

開発にあたっては、スマホ側とWear側両方実行しないといけないので若干面倒くさいです。

サンプルで使われているLog.d()について

サンプルでは様々なタイミングでLogにメッセージを流すようになっていますが、そのままではこれを確認することができません。

というのもLogを出力する前にisLoggableでログの出力レベルを設定を確認しているためです。

        if (Log.isLoggable(TAG, Log.DEBUG)) {
            Log.d(TAG, "onConnected: " + connectionHint);
        }

これをLogcatで確認するためには、実行する端末に対してadb shell setprop log.tag.(TAGで指定されている文字列) DEBUGとターミナルから入力してやると、端末のLogcatにログが出力されるようになります。