カテゴリー: Android

Androidのプログラミングについての情報を取り扱っています。

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件あって、そのうち◯件処理済みとか表示するのではなく、データ読み込み中ですよって表示するだけ。そういう方向での切り替えをした方が通知のための仕組みを実装するより安全な気がします。

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

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

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

今までずっと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](http://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で設定してやると、現在設定されているログ出力レベルが返って来ます。

ログレベルの優先度

VERBOSE > DEBUG > INFO > WARN > ERROR > ASSERTとなっています。デフォルトではINFOになっているので、isLoggable()はINFO,WARN,ERROR,ASSERTでtrueを返します。

setpropでVERBOSEを設定するとあらゆるレベルでtrueが返るようになります。setprop SUPPRESSとすると逆にあらゆるレベルでfalseが返るようになります。

再起動したら元に戻る

ちなみに設定する端末が開発専用であれば問題ないでしょうが、日常的に使っている端末の場合はログ出力レベルをINFOに戻すのを忘れないようにしましょう。(パフォーマンスに影響するため)

別に忘れても再起動すれば元に戻る(設定が消える)ので気にしなくてもいいかもしれません。

ちなみに「再起動の度に設定するのは面倒くさい」という場合には、/data/local.propで設定することもできるようですが、どうやって設定するのかは分かりません。多分root権限ないとできないんじゃないかと思います。(やってみたけどPermission deniedって言われました)

リリース予定がないならいきなりLog.d()でも構わないのでしょうが、リリースを視野に入れているアプリならこういった方法でログを出力するように設定しておくとユーザに優しいと思います。

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

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

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を使う方法もあるのかもしれませんが、動き始めに画像が元のサイズに戻ってしまうため、この方法がスマートな気がします。

ちなみにピンチイン・アウトを検出するにはScaleGestureDetectorが使えます。Dragging and Scaling – Android Developers

拡大縮小の起点を指定する場合、detector.getFocusX()を使うとイメージに近い動きになりました。ただし画像の範囲外でやると当然ながら動きがおかしくなるので、画像が画面の範囲外に移動できないように制限を実装しないとダメそうです。

現在の拡大率を取得するには?

Matrixに設定された現在の拡大率を取得するのも一工夫必要です。getScaleXというような、Marixの現在の拡大率を直接取得するメソッドはありません。

Matrixは9つの値を保持していますが、その値を参照するためにはfloatの配列を渡してコピーしてもらうしか方法がありません。

float[] values = new float[9];
mMatrix.getValues(values);
nowScale = values[Matrix.MSCALE_X];

ちなみにAPIリファレンスにある並びで並んでいるわけではないので、定数を使ってアクセスしましょう。

ちなみに0から順にMSCALE_X,MSKEW_X,MTRANS_X,MSCALE_Y,MSKEW_Y,MTRANS_Y,MPERSP_0,MPERSP_1,MPERSP_2の順に並んでます。

なんで変な並びになってるのかと思ったら、英語のWikipedia見たらその意味が分かるかもしれません。Transformation matrix – Wikipedia

Android特有の概念ではなく、Matrixを使った変形の概念があるんですね。数式だらけでサッパリ分かりませんが。

日本語で解説してあるサイトもありますが、いずれにしても奥が深そうで難解です・・・。

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

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

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

例えば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)に渡しています。

一方でカメラで撮影した画像は、直接画像データがdata.getExtras().get("data")で取得できます。ただしこの画像はサムネイル用の小さな画像です。

オリジナルの画像データは大きすぎて、Intentを経由しての受け渡しができないからです。カメラで撮影したオリジナルサイズの画像を受け渡しするには一工夫必要です。長くなるので別に書きます。

adbコマンドを使ってアプリを手動で削除する

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

adbコマンドを使ってアプリを手動で削除するのは、スマホのアプリを作成する上では必要ないと思っていましたが、Android Wearアプリを作る場合には覚えておいた方がいいです。

というのもWearに一度インストールしたアプリは、Wear上での操作では削除することができないからです。

スマホ経由でインストールしたアプリの場合、スマホ側でアプリを削除すればWearにインストールされたアプリも一緒に消えてくれますので、一般的にはあまり問題にならないのかもしれません。

しかしWearアプリを開発していると、デバッグのためなどでWearに直接アプリをインストールすることが多くなります。こうなるとWearがデバッグ用のアプリで埋め尽くされる事態になってくるのです。

インストールされているパッケージの一覧を確認する

adb shell pm list package

ターミナルからコマンドを入力すると、対象デバイスにインストールされているパッケージの一覧が表示されます。

adb shell pm list package

アプリを削除するためにはパッケージ名が必要になるので、このコマンドで確認しましょう。

ちなみにパソコンに複数の端末が接続されている場合、adbコマンドを送るデバイスを指定する必要があるので注意しましょう。

アプリを手動で削除する

adb uninstall アプリのパッケージ名

adb uninstall

Wearのアプリを削除する場合は、コマンドの結果が返ってくるまで時間がかかります。

Wearの初期化のほうが早いかもしれない

ちなみに、Wearを初期化してしまえば済む話でもあるので、手動で消さなくてもなんとかなる話ではあります。Wearは初期化したところでスマホとペアリングし直すだけでほぼ元に戻ると言っても過言ではないため、初期化したほうが手っ取り早いのかもしれません。

ただ、サンプルアプリを1個インストールする度に初期化するのも馬鹿らしいので、コマンドを使えば簡単にアプリを消せるということを知っておくと何かと便利かもしれません。

Android StudioでButterKnifeを使う

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

ButterKnifeはViewのインジェクションに特化したライブラリです。

Android Studioで利用する場合はとても簡単で、app/build.gradleのdependenciesに1行追加するだけです。

app/bulid.gradleに1行追加するだけ

    compile 'com.jakewharton:butterknife:6.1.0'

これだけで使えるようになります。

ButterKnifeを使うことでfindViewById()をコードからなくすことができるので、ActivityやFragmentのコードがすっきりします。

public class MainActivity extends ActionBarActivity {

    @InjectView(R.id.test)
    TextView mTextTest;
    @InjectView(R.id.hello)
    TextView mTextHello;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        ButterKnife.inject(this);

        mTextTest.setText("ButterKnife Sample!!!");
        mTextHello.setText("Next text here!!");
    }
}

ButterKnifeサンプルの実行結果

@InjectView(ViewのID)で、TextViewなどを保持するクラスフィールドを指定してやります。

後はonCreate()内でButterKnife.inject(this)を実行すれば、findViewById()を使うことなく、Viewに対する操作ができるようになります。

扱うViewが多くなればなるほど、その効用が実感できるようになります。

ButterKnifeを使わない場合、以下のようになります。

public class MainActivity extends ActionBarActivity {

    TextView mTextTest;
    TextView mTextHello;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        (TextView) mTextTest = (TextView) findViewById(R.id.test);
        (TextView) mTextHello = (TextView) findViewById(R.id.hello);

        mTextTest.setText("ButterKnife Sample!!!");
        mTextHello.setText("Next text here!!");
    }
}

Android Studioだとbuild.gradleに1行追加するだけで使えるようになるので、とても便利ですね。

GitHub – ButterKnife

Android5.0の端末をUSBデバッグできるようにする

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

Nexus5(Android5.0.1)を購入したのですが、そのままではUSBデバッグができなくて困りました。

パソコンに繋いでもAndroid Studioから端末が認識されません。設定画面を探しまわってもそれらしい設定項目がありませんが、どうもデフォルトでは表示されないようになっているようです。

開発者向けオプション

これを表示させるためには、Androidの設定画面を表示し、一番下にある端末情報を開きます。そして更にその画面の一番下にある「ビルド番号」を連続でタップします。

ビルド番号を連続タップ

そうすることで開発者向けオプションが表示されるようになります。

後は開発者向けオプションの設定メニューから、USBデバッグを有効にするにチェックをつけ、端末をパソコンに接続すればAndroid Studioから認識されるようになります。

USBデバッグを有効にする

認識されるようになったLolipop端末

AnimationDrawable 静止画を使ったアニメーション

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

静止画像(pngなどの画像リソース)を用意してパラパラ漫画の要領でアニメーションさせるには、AnimationDrawableクラスを利用します。

Android APIs Reference – AnimationDrawable

AnimationDrawableのサンプル

文字が変わってるだけですが、3つの画像でアニメーションしてます。画像を準備するのが面倒くさかったので、文字だけの画像を使いました。

アニメーションに使う静止画像

画像は解像度に合わせてres/drawable/hdpiなどのディレクトリに用意します。

今回はanime_test1.png,anime_test2.png,anime_test3.pngの3つの画像ファイルを用意しました。画像と言いつつ数字の1,2,3が書かれているだけの画像です。

ちなみにファイル名として使えるのは小文字のアルファベット、数字、アンダースコア(_)とドット(.)のみです。それ以外の文字(大文字アルファベットなど)を使うと以下のようにコンパイルエラーとなります。

Invalid file name: must contain only lowercase letters and digits ([a-z0-9_.])

アニメーション設定のXMLファイル

どの画像を何秒間表示させるのかという設定をXMLファイルに記述します。今回はres/drawable/test_animation.xmlというファイル名にしました。

<?xml version="1.0" encoding="utf-8"?>
<animation-list xmlns:android="http://schemas.android.com/apk/res/android"
    android:oneshot="false">
    <item android:drawable="@drawable/anime_test1"
          android:duration="500"/>
    <item android:drawable="@drawable/anime_test2"
          android:duration="500"/>
    <item android:drawable="@drawable/anime_test3"
          android:duration="500"/>
</animation-list>

android:oneshot=trueで、アニメーションを1回のみ再生する設定になります(最後の画像でアニメーションが止まる)。falseだとループ再生されます。

アニメーションを再生する

test_animationは何もしなければ単なる静止画と同じで、Drawableとして扱うことができます。ImageViewのsrc属性に設定したり、TextViewのbackground属性に設定したりすることができます。

今回はImageButtonに上記で作成したdrawableを設定してやり、ボタンを押したらアニメーションが再生されるようにしてみます。

activity_main.xml

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
                xmlns:tools="http://schemas.android.com/tools"
                android:layout_width="match_parent"
                android:layout_height="match_parent"
                tools:context=".MainActivity">

    <ImageButton
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:id="@+id/image_button"
        android:src="@drawable/test_animation"
        />

</RelativeLayout>

APIリファレンスではandroid:background属性に設定していますが、これはandroid:src属性に設定しても動きました。src属性にAnimationDrawableを設定した場合、getBackground()ではなくgetDrawable()でAnimationDrawableを取得します。

MainActivity.java(onCreateを抜粋)

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        ImageButton imageButton = (ImageButton) findViewById(R.id.image_button);
        final AnimationDrawable animationDrawable = (AnimationDrawable) imageButton.getDrawable();

        imageButton.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {

                animationDrawable.start();
            }
        });
    }

AnimationDrawableを取得して、start()メソッドを呼び出せばアニメーションさせることができます。

ただしできるのは再生するか停止するかくらいで、逆再生したりはできないみたいです。