AsyncTaskLoaderの動きを確認中 その2

前回の続きで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だけ)

重い腰をあげて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がつくのに別物と

Read full post gblog_arrow_right

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

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の世界ではどれもプロジェクトと呼ぶそうです。勉強になりました。

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

2019/05/18追記: この記事の情報は古いので公式ドキュメントを参照してください。


データのやりとりは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を取りに行くのもありかもしれません。

DroidKaigiに参加してきました

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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()を明示的にコールした際に呼ばれます。

Read full post gblog_arrow_right

Androidアプリを開発する上で賢いLogの出力方法(とその確認の仕方)

今までずっとLog.d("test",”デバッグメッセージだよ”);みたいな感じでLogを出力し、Logcatで確認しながらプログラミングしていたのですが、とあるサンプルを見ていた時にこんなコードに出くわしました。

    private static final String TAG = "DigitalWatchFaceConfig";

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

プログラムを実行しても、このログはlogcatに出力されません。

「なんでだ?」と思って調べているうちに、この方法はAndroidアプリ開発していく上で賢い選択なのだなということが分かってきました。

ベストプラクティスなのかどうかまでは分かりませんが、少なくともいきなりLog.d()で出力したり、アプリ内でprivate boolean isDebug = true;みたいにしてデバッグログを出力させるよりは賢いなと思いました。

参考サイト

[Log.isLoggable – API Refference](https://developer.android.com/reference/android/util/Log.html#isLoggable(java.lang.String, int))

ログレベルを制御する – TechBooster

ログの出力はアプリのパフォーマンスを下げる

ログの出力はアプリのパフォーマンスに影響します。少なくともStringオブジェクトを作ってそれを出力するわけですからね。

リリース時にはLog出力する部分を全部削除するのが一番いいのでしょうが、さすがにそれは手間が大きすぎます。それにリリースしたからといって開発が終わるわけでもなく、メンテのためにまた1からLogを出力するように直すのはあまりにも馬鹿らしいです。

Logを使わず開発するのはそもそも無理です。

Log.isLoggableによるチェック

Log.isLoggableによるチェックは、端末に設定されているログ出力レベルを判定しています。デフォルトでは全てのタグについてINFOが設定されています。

つまり最初のコードのようなLog.DEBUGでチェックをかけるとfalseが返ってくるのでログが出力されません。

ログ出力レベルの変更

ではどうやってログが出力されるようにすればいいのかというと、ターミナルでadb shell setpropコマンドを使います。

adb shell stop
adb shell setprop log.tag.設定したいタグ名 ログレベル
adb shell start

最初の例のログを出力させようと思ったらadb shell setprop log.tag.DigitalWatchFaceService DEBUGとターミナルから打ち込んでやればOKです。(ちなみにadb shellで端末にログインしてからであれば、いきなりsetpropから初めてOKです)

ログ出力レベルの確認

タグごとのログ出力レベルを確認するには、adb shell getprop log.tag.タグ名を使います。何も設定していない状態であれば空白が返って来ます。setpropで設定してやると、現在設定されているログ出力レベルが返って来ます。

Read full post gblog_arrow_right

MatrixのpostScaleで画像を拡大縮小させる

ImageViewなり、自分で作ったCustom Viewなりで表示させる画像を、動かしたり拡大縮小させたりするのに使えるMatrixをいじって学んだことのメモです。

特にpostScaleを使った拡大縮小がイメージ通りに動かなくてハマってしまいました。

ちなみにMatrixクラスを使ってBitmapを加工する – Techoboosterを参考に始めました。

使い方

ImageViewに設定するには、setImageMatrixメソッドでMatrixを渡してやるといいです。

Matrix matrix = new Matrix();
ImageView.setImageMatrix(Matrix);

CustomViewで使う場合は、オーバーライドしたonDraw()で描画するときにMatrixを渡せばいいです。

   @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        canvas.drawBitmap(mImage, mMatrix, mPaint);
    }

こんな感じ。mImageはBitmapオブジェクトで、mMatrixとmPaintはそれぞれnew Matrix(),new Paint()したものを渡してます。

今回は下のCustom ViewでMatrixを操作していて分かったことを書きます。

タッチ操作で動かす

画面上を指でなぞると、その動きに応じて画像も移動するようにする場合はこうすればOK。

mMatrix.postTranslate(float X移動量,float Y移動量);

移動量を取得するにはTouchEventを自分で判定するなり、GestureDetectorを使うなりして取得します。

GestureDetectorの使い方はDetecting Common Gestures – Android Developers参照。また別途記事書こうと思います。

移動に関しては特に難しくはありませんでした。ただし、移動制限を設けようとするとこれはこれでまた大変そうです。

こちらの記事が移動制限を実装するのに非常に役立ちそうな予感です。実装できたらまた記事書きたいと思います(そればっか)。

拡大縮小

ピンチイン・アウトで画像を拡大縮小させる場合がクセモノでした。

mMatrix.postScale(float X拡大率, float Y拡大率, float 拡大の起点X, float 拡大の起点Y);

ハマったポイントはここで渡す拡大率の扱いです。

postScaleに渡す拡大率は、Matrixを指定した拡大率に変形させるのではありません。現在のMatrixを渡した拡大率で拡大縮小させます。Matrixの拡大率が0.1のときにpostScale(0.1f,0.1f)するとMatrixの拡大率は0.01になります。

画像が過剰に縮小・拡大されないように渡す拡大率の値を制限したとしても、制限した値をそのまま渡してしまったら制限が効きません。

指定した拡大率に画像を変形させたい場合は変化量を計算して渡すようにします。

ScaleGestureDetectorを使って拡大縮小させていて、頭の中がこんがらがっていました(現在進行形ですけど)。ScaleGestureDetectorを使うと、onScaleメソッド内でdetector.getScaleFactor()を使うことでピンチイン・アウトによる拡大率を取得することができます。

この拡大率は、ピンチ操作が始まった段階では1.0から始まります。そのためこの値をそのままMatrixのpostScaleに渡すと、拡大縮小の開始時に一旦元の縮尺に戻ってしまいます。そのこととごっちゃになっていて間違ったこと書いてました。

float deltaScale = targetScale / nowScale;
mMatrix.postScale(deltaScale, deltaScale);

postScaleではなくsetScaleを使う方法もあるのかもしれませんが、動き始めに画像が元のサイズに戻ってしまうため、この方法がスマートな気がします。

Read full post gblog_arrow_right

Intentを発行して画像を選択orカメラで撮影して、画像を表示させる

端末内に保存されている画像を表示したり、もしくはその場でカメラで撮影した画像を表示させる方法です。

例えばSNSへ投稿する画像を選択したりするのに使うことが考えられますかね。

やり方としてはIntentを発行して、startActivityResult()で結果を受け取って表示させるようになります。

画像の選択とカメラでの撮影は異なるアクションなので、1つのIntentで表現するにはIntent.createChooser()で複数のIntentをひとまとめにして発行することになります。

やってみると、カメラで撮影した画像を受け取るのにちょっと工夫が必要なだけで、割と簡単に実装できました。

Getting a Result from an Activity – Android Developers

Intentの発行

画像を選択するIntent

        Intent pickPhotoIntent = new Intent()
                .setType("image/*")
                .setAction(Intent.ACTION_GET_CONTENT);

カメラで撮影するIntent

カメラで撮影する場合、以下のIntentでも撮影→その画像を受取ることができますが、そのままでは画像サイズがとても小さくなってしまいます。(サムネイルサイズの小さな画像が返ってくる)

        Intent takePhotoIntent = new Intent()
                .setAction(MediaStore.ACTION_IMAGE_CAPTURE);

複数のIntentを埋め込む

        Intent chooserIntent = Intent.createChooser(pickPhotoIntent, "画像を選択");
        chooserIntent.putExtra(Intent.EXTRA_INITIAL_INTENTS,new Intent[]{takePhotoIntent});

作成したIntentの1つを元にしてcreateChooser()を呼び出して作成したIntentに、Intentの配列を埋め込みます。

画像を受け取る

    @Override
    protected void onActivityResult(int requestCode, int resultCode, Intent data) {
        if(requestCode == REQUEST_GET_IMAGE && resultCode == Activity.RESULT_OK) {
            if (data != null) {
                Bitmap image = null;
                if (data.getExtras() != null && data.getExtras().get("data") != null) {
                    image = (Bitmap) data.getExtras().get("data");
                    mImageView.setImageBitmap(image);
                } else {
                    try {
                        InputStream stream = getContentResolver().openInputStream(data.getData());
                        image = BitmapFactory.decodeStream(stream);
                        mImageView.setImageBitmap(image);
                    } catch (FileNotFoundException e) {
                        e.printStackTrace();
                    }
                }
            }
        }
        super.onActivityResult(requestCode, resultCode, data);
    }

Intentによって選択されたファイルは、当該ファイルを一意に識別するためのUriがIntentに埋め込まれて返ってきます。これはdata.getExtra()で取得できます。上記の例ではBitmapファイルとして取得して、独自View(mImageView)に渡しています。

Read full post gblog_arrow_right

音声コマンドで自分の作ったアプリのActivityを起動する

Android Wearでアプリを起動するのに音声入力でアプリが起動できるととても便利です。Wear用アプリを作る上では外せない要素だと思います。

Android Developersのトレーニングを見ると、AndroidManifest.xmlでintent-filterかけておくだけでいいということです。activityに設定したlabelをキーワードとして、Activityが起動するようになります。

ぶっちゃけると、ラベルをしっかり設定さえすれば、初期状態で音声入力によってActivityが起動するということであります。

Adding Voice Capabilities – Android Developers

しかしいくらやってみてもうまくいかなくて、何がいけないのかサッパリ分かりませんでした。

単純すぎるのか調べてもなかなかピタリとくる情報もなくて困っていたら、Stack over Flowを見て謎が解けました。

Wear: Open my app with custom voice Start command, not working – Stack Over Flow

つまるところ、mobile側のActivityにintent-filterをつけてないとうまく動作しないのです。

私はデバッグのため、wear側のアプリしか動かしていませんでした。(開発中のサンプルアプリはwearにしかインストールされていない状態)

スマホ側に音声コマンドを受け取るintent-filterがなかったためにWear上のActivityも起動しなかったということなんだと思います。

Wearアプリを開発する場合は、Wearモジュールだけでなく、mobileモジュールも実行してスマホにインストールしておかないと、ちゃんとした動作確認ができないということが分かりました。

実験

構成

Android StudioのNew Projectウィザードで作成した初期状態のままです。

wearモジュールもmobileモジュールも、Hello Worldの文字列を表示するだけのMainActivityがあるだけの状態です。

mobileのラベルとwearのラベルを同じにする

mobile側のActivityのラベルに「サンプル」と設定して、wear側のActivityのラベルにも「サンプル」と設定します。

この状態でOK Googleから「サンプル開始」と言うと、WearのMainActivityが起動します。スマホのMainActivityは起動しません。

mobileのラベルとwearのラベルを異なるものにする

mobile側のActivityのラベルに「サンプル」と設定して、wear側のActivityのラベルに「テスト」と設定します。

この状態でOK Googleから「サンプル開始」というと、スマホ側のMainActivityが起動します。(Wear端末には「アプリを開いています・・・」というメッセージが表示され、スマホ側で指定したActivityが起動します)

一方で「テスト開始」というと「テスト開始」でWeb検索を行った結果がWear端末に表示されます。

どちらにせよWearのMainActivityは起動しませんでした。

スマホ側での音声入力

ちなみにスマホ側でOK Googleからの音声入力を行った場合は、全てWeb検索として扱われてしまい、MainActivityは起動しませんでした。

Read full post gblog_arrow_right