AppBar(Toolbar、ActionBar)の部分が大きめの画像になっていて、コンテンツをスクロールするとそれに合わせて画像が縮んでいき、最終的にToolbarだけが残る(もしくは全部隠れる)みたいなデザインがありますよね。あれを実装しようと思って試行錯誤してみました。
試行錯誤になってしまった原因は、スクロール可能なコンテンツ部分を横着してListViewで作ってしまったからでした。見かけるサンプルはだいたいRecyclerViewを使っていたのですが、使ったことがないため使い慣れているListViewでやろうとしたのが間違いでした。
ListViewで実装すると、ListViewをスクロールしてもAppBarは連動して動いてくれません。AppBarの部分をスクロールすると伸縮してはくれますが、巷にあふれるパララックスAppBarはこんな残念な動きはしていません。
コードで何か手を加えないといけないのだろうかと調べるうちに、なぜListViewではAppBarが連動して動かないのか原因が分かりました。今回はそのお話です。
Patterns– Scrolling techniques
layout.xmlの設定 基本的にパララックスなAppBarを実装するには、レイアウトXMLの記述のみで実装できます。
サンプルコード – GitHub
このMaterial Design(Android desgin support library)による階層構造を初めて見ると、なんだかややこしく感じてしまいますが、1つずつ紐解いていけばそう難しい構造ではありません。
正確にはandroid.support.design.widget.〜とFQCN(パッケージ名を含めたクラス指定)になりますが、ここでは長くなるので省略しています。
CoordinatorLayout ├AppBarLayout │└CollapsingToolbarLayout │ ├ImageView │ └Toolbar ├ListView(などスクロール可能なコンテンツ) └FABなどお好みで 基本的にXML上でちゃんと必要な指定さえ行えば動きます。コードは不要です。
CoordinatorLayout 今回の例ではListViewのスクロールにあわせてAppBarLayoutを伸縮させるために存在しています(FABをToolbarとListViewの中間に配置する役割も担っていますが)。このCoordinatorLayout自体は内包したView同士を連携させたりする単なる入れ物です。全然「単なる」ではないですけど。
AppBarLayout AppBar部分のLayoutを管理するコンテナで、AppBarの部分に表示するViewをこの中に入れてやります。Blank Activityを作成すると、この中にはToolbarだけが入っていると思います。
ここではAppBarの高さを指定してやります。android:layout_height="192dp"。
CollapsingToolbarLayout 折りたためるToolbarのための入れ物です。スクロールによるAppBarの動き方を指定することができます。ここではapp:layout_scrollFlags="scroll|exitUntilCollapsed"と指定しています。
ImageView AppBarが全開のときに表示されるイメージ画像です。コンテンツのスクロールに合わせて縮み、最終的にToolbarだけが残ります。ここではapp:layout_collapseMode="parallax"を指定しています。
Toolbar Toolbarです。ここではapp:layout_collapseMode="pin"を指定しています。この指定でToolbar自体は隠れずに残ります。
ListView よく見かけるサンプルではRecyclerViewやNestedScrollViewが利用されています。しかし私はRecyclerViewの使い方がよくわからなかったのでListViewで代用しています。
ここでは必ずapp:layout_behavior="@string/appbar_scrolling_view_behavior"の指定が必要です。
この@string/〜はAndroid Support Libraryのstringリソースを参照していて、その中身はandroid.support.design.widget.AppBarLayout$ScrollingViewBehaviorとなっています。つまりこのListViewの振る舞いとして、AppBarLayoutのScrollingViewBehaviorを指定しているわけです。
ListViewを使うと、そのままではListViewがスクロールされるだけでAppBarが伸縮しません。ListViewのスクロールと連動させるためには、ListViewにandroid:nestedScrollingEnabled="true"を指定する必要があります。
なぜか。スクロール可能なコンテンツとAppBarの伸縮を連携させるためには、ListViewがスクロールされたということをCoordinatorLayoutに伝える必要があります。RecyclerViewやNestedScrollViewは標準でこれをやってくれるわけですが、ListViewは何もしません。そこでCoordinatorLayoutにスクロールイベントを通知するための設定を有効にしてやる必要があるのです。
android:nestedScrollingEnabled="true"(NestedScrollに関する処理)はAPI21以上のViewに実装されています。
余談:なぜAppBarが動くのか 仕組みを完全に理解したわけではないので、ざっくりとした説明です。
ListViewの上でスクロールを行うと、ListViewの中身がスクロールされます。これはListViewのonTouchEventで処理されています。これだけではListViewの中でスクロールイベントが処理されるだけで、AppBarの変形にはつながりません。
そこで登場するのがCoordinatorLayoutです。こいつが子Viewのスクロールと、別の子Viewを連携させるわけです。
連携させるためにはCoordinatorLayoutにスクロールイベントを通知する必要があり、その仕組がNestedScrollです。RecyclerViewやNestedScrollViewは初めからCoordinatorLayoutと連携する前提で作られていますし、ListViewなどでもSDK21からNestedScrollに関する処理が追加されています。ただし初期状態では無効化されているので、NestedScrollの処理を有効化してやる必要があり、それがandroid:nestedScrollingEnabled="true"になります。
NestedScrollの処理は子ViewのonTouchEvent(onTouchMove)でCoordinatorLayoutに伝わります。CoodinatorLayoutはonNestedPreScroll内でBehaviorが設定されている子Viewを探し、見つかったBehaviorに対してdispatchOnDependentViewChangedを呼び出します。今回の例ではScrollingViewBehaviorです。
最終的にAppBarのサイズを伸縮させる処理は、このBehaviorのonDependentViewChangedで行われているみたいです。
参考 Handling Scrolls with CoordinatorLayout
app/build.gradleのdependenciesにさり気なく書いてあるサポートライブラリ。
dependencies { compile fileTree(dir: 'libs', include: ['*.jar']) compile 'com.android.support:appcompat-v7:22.2.1' } appcompat-v7:22.2.1のこの22.2.1の部分。自分で指定しようと思うと、バージョンいくつが存在しているのかが分からず、どうやって確認すればいいのかも分かりません。とりあえず最新のものが当たればいいやと、+を使ってごまかしたりしてきましたが、最新のものが当たるとそれはそれでうまくいかないことがあったりします(23が出てるんだけど、とりあえずはtargetSDKを22で作ってる今とか)。
要するに、1つ前のバージョンを指定したいのだけど、その1つ前のバージョンというのはいったいいくつなんだというのが困ります。結論から言うと22.2.1なんですけど、じゃあそれをどうやって調べたらいいのかという話です。
Support Library – Android Developersで確認できます。
また、パソコンにインストールしているAndroid SDKのディレクトリを潜って行くことでも調べることはできます。
(Android SDKのインストールディレクトリ)/extras/android/m2repository/com/android/support/appcompat-v7 このディレクトリに、バージョンごとのフォルダがあるのでそこでも確認可能です。
そもそも指定できるバージョンの候補を出してくれると楽なんですけどね。
greenrobot/EventBus – GitHubを使ってみました。
異なるスレッドからのイベントの通知でもうまくハンドリングしてくれるので、AsyncTaskLoaderでProgressを通知するのにも利用できます。
Broadcastを使って実装するのと比較するとコードがシンプルになって良いです。IntentFilterやIntentにデータを埋め込む際に使うキー文字列を定義したりしなくて済みます。
更に独自のオブジェクトをイベントとして渡すこともできるので、Broadcastでは難しいイベントの通知も簡単に出来ます。
準備 /app/build.gradleのdependenciesにcompile 'de.greenrobot:eventbus:2.4.0'と、1行追加するだけで使えます。
イベントの送信 イベントを送信したいところで、EventBus.getDefault().post("イベント発生");とするだけです。
これは単にStringオブジェクトを渡しているだけですが、独自オブジェクトを定義して渡すことも可能です。
イベントの受信 イベントの購読・解除 Activityでイベントを受信するなら、onResume()でEventBus.getDefault().register(this);とすることでイベントの購読を行います。
この際、onPause()でEventBus.getDefault().unregister(this);で購読解除を忘れないように。
イベントのハンドリング AsyncTaskLoaderからのイベントを受信してUIを更新するなら、onEventMainThread()をActivityに実装します。
public void onEventMainThread(String event){ mTextView.setText(event); } 送信するイベントのオブジェクトごとにこのメソッドを用意してやる必要があります。例えば他にMyEventという独自オブジェクトがある場合、別途public void onEventMainThread(MyEvent event){}を実装してやります。
メソッドをオーバーライドするわけではないので、コード補完は効きません。タイポするとこんな例外が起きてアプリが落ちます。
java.lang.RuntimeException: Unable to start activity ComponentInfo{jp.gcreate.sample.asynctaskloadersample/jp.gcreate.sample.asynctaskloadersample.MainActivity}: de.greenrobot.event.EventBusException: Illegal onEvent method, check for typos: public void jp.gcreate.sample.asynctaskloadersample.MainActivity.onEventBackgroundThrad(jp.gcreate.sample.asynctaskloadersample.MyEvent)
まとめ AsyncTaskLoaderのProgressの通知で使ってみましたが、とても便利だなと思いました。
内部的にはHandlerを使ってイベントのハンドリングを行っているみたいでした。Handlerの具体的な使用例としていい勉強にもなって、個人的には一石二鳥な感じです。ありがたや〜。
便利な半面、無計画に使うとイベントが乱立してカオスな事になりそうなので、実際に使うときには気を付けないといけないなと思いました。
AsyncTaskLoaderにはAsyncTaskのpublishProgress()のような途中経過を通知するメソッドが標準では用意されていません。
そこでブロードキャストを利用してこれを実装します。
Context#sendBroadcast()を使ってもいいのですが、これだと自分のアプリ外にもブロードキャストが送信されてしまうので、LocalBroadcastManagerを利用します。
AsyncTaskLoaderでの処理 AsyncTaskLoader側ではloadInBackground()内で、ブロードキャストの送信を行うだけです。
この際、途中経過のデータはIntentに埋め込んで送信する必要があります。
@Override public String loadInBackground(){ //非同期処理 Intent intent = new Intent(MainActivity.ACTION_PROGRESS) .putExtra("key", "hoge"); LocalBroadcastManager.getInstance(getContext()).sendBroadcast(intent); } MainActivity.ACTION_PROGRESSはインテントフィルタを表す文字列です。
Activity側の実装 //インテントフィルタの定義 public static final String ACTION_PROGRESS = "jp.gcreate.sample.asynctaskloadersample.ACTION_PROGRESS"; //ブロードキャストレシーバーの作成 private BroadcastReceiver mReceiver = new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { String progress = intent.getStringExtra("key"); mTextView.setText(progress); } }; @Override protected void onStart() {
super.onStart(); //ブロードキャストレシーバーの登録 LocalBroadcastManager manager = LocalBroadcastManager.getInstance(this); manager.registerReceiver(mReceiver, new IntentFilter(ACTION_PROGRESS)); } @Override protected void onStop() { super.onStop(); //ブロードキャストレシーバーの解除 LocalBroadcastManager.getInstance(this).unregisterReceiver(mReceiver); } ブロードキャストレシーバーを作成して、ここでAsyncTaskLoaderから送られてくるProgressを受け取りとUIの更新処理を実装します。
後はonStart()でブロードキャストレシーバーの登録、onStop()で解除を行ってやればOKです。
細かいサンプルはGitHubにおいてます。
基本的にはここに書いてあるとおりにやればいいだけの話です。
準備 /app/build.gradleのdependenciesにjunitを追加します。
testCompile 'junit:junit:4.12' testCompile 'org.mockito:mockito-core:1.9.5' この際に注意が必要なのは、androidTestCompileとtestCompileは別物であるということです。
何が別物かというと、テストコードを配置するディレクトリがそれぞれ違います。
その名の通りandroidTestCompileはandroidTestディレクトリに配置したテストコードのコンパイル時にだけ使うライブラリです。同じくtestCompileはtestディレクトリに配置したテストコードのみに使われるライブラリになります。
なお、androidTestディレクトリは自動的に作成されていますが、testディレクトリは自分で作らなければなりません。(ディレクトリは/app/src/test/java/パッケージ名にしてやればOK)
androidTestとtestの切り替え Build Variantsウィンドウを開くと、Test Artifactという欄があります。
Android Instrumentation Testsを選択していると、androidTestディレクトリ以下にあるテストコードが有効化されます。有効化されるというのが適切なのかは分かりませんが、Android Studioからコンパイル対象のソースコードであると認識されます。
Unit Testsに切り替えると、testディレクトリが有効化されます。試しに切り替えてみると、androidTestディレクトリの色が変わって、テストコードのアイコンに赤いJアイコンが出るようになると思います。
IDE上からテストを実行しようと思うと、このBuild Variantsをいちいち切り替えないといけないのが面倒くさいかもしれません。
しかし、ターミナルからGradleを使って実行する場合は、このTest Artifactsの切り替えはしなくてもいいみたいです。Gradleからテストを実行する場合、./gradlew connectedAndroidTestがAndroid Instrumentation Testsを、./gradlew testがUnit Testsを選択してテストを実行するのと同じになります。この場合のテスト結果は/app/build/reports/testsの中に出力されます。
テストコードはViewやActivityなどのUIに関するテストをandroidTestディレクトリに、純粋なJavaコードのテストはtestディレクトリに置くように工夫すべきでしょう。そうしてやれば、ユニットテストにかかる時間を削減できて幸せになれると思います。
Android Studio 1.2でEspresso2.1を使ったUIテストをやってみました。
テストの実行に実機(エミュレータ)が必要なのが面倒くさいですが、実機無しでテストが実行できるようにする方が面倒くさい(というかやり方がわからない)ので、テストができるだけマシだと考えることにしました。
テストを実行する際に、開発者オプションでアニメーションの無効化をしておかないと、アニメーションのせいで同じテストが失敗することがあるのは注意が必要かもしれません。
しかしながら、Android Support Libraryに入っているおかげでテストを実行するまでのハードルが低いのはうれしいところです。
また、EspressoによるUIテストの書き方も非常にシンプルで分かりやすいと思います。
準備 詳しい手順はここに書いてあるとおりです。Get started – android-test-kit
まずは/app/build.gradleにテストで利用するライブラリを追加します。
dependencies {
// Testing-only dependencies
androidTestCompile 'com.android.support.test:runner:0.2'
androidTestCompile 'com.android.support.test:rules:0.2'
androidTestCompile 'com.android.support.test.espresso:espresso-core:2.1'
} /app/build.gradleにtestInstrumentationRunnerを追記します。場所はdefaultConfigの中です。
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
更に以下のおまじないも追加します。
packagingOptions { exclude 'LICENSE.txt' } 最終的な/app/build.gradle 適宜読み替えて使ってください。
apply plugin: 'com.android.application'
android {
compileSdkVersion 22 buildToolsVersion "22.0.1" defaultConfig { applicationId "jp.gcreate.product.developersettingsshortcut"
minSdkVersion 15 targetSdkVersion 22
versionCode 1
versionName "1.0" testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
} buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' } } packagingOptions {
exclude 'LICENSE.txt' } } dependencies {
compile fileTree(dir: 'libs', include: ['*.jar'])
compile 'com.android.support:appcompat-v7:22.1.1'
compile 'com.jakewharton:butterknife:6.1.0'
// Testing-only dependencies androidTestCompile 'com.android.support.test:runner:0.2'
androidTestCompile 'com.android.support.test:rules:0.2'
androidTestCompile 'com.android.support.test.espresso:espresso-core:2.1' } 一時的なバグ? Warning:Conflict with dependency 'com.
LinearLayoutをネストしすぎたりするなど、Viewの階層を深くするとアプリのパフォーマンスに良くないという話はよく聞くと思います。
それと似たような話で、画面を何回描画しているかを確認して、アプリのパフォーマンスに役立てることができきます。今回はそれの紹介です。
確認の仕方 端末の開発者オプションで「GPUオーバードローをデバッグ」を有効にします。
これを有効にすると、目に悪そうな色で画面が表示されるようになります。
この各色が、GPUによって何回上書き描画されているのかを示しています。
青色:1回 緑色:2回 薄赤:3回 濃赤:4回以上 この状態で画面が真っ赤っ赤だと、描画方法を改善した方がいいぞということになります。
対策 例えばFrameLayoutでbackgroundDrawableを持ったViewを何個も重ねていくと、見えているのは一番上のものだけなのに、見えない下の要素まで描画するため上書き回数が増えて赤色になってしまいます。
そのため不要なbackgroundDrawableを描画しないようにすることが、この問題の対策になります。
例えばActivityでgetWindow().setBackgroundDrawable(null)とするだけでも画面の赤色が薄くなると思います。(ただし、これをやるとListViewやGridViewなど、スクロールをともなうViewの描画がおかしくなります)
重ねて描画せざるをえない場合は、canvas.clipRectを使って重なって見えない部分を描画しないようにすることで対応できるようです。
効果 ムダな描画回数を減らすことにつながるので、その分アプリの動きが軽快になるでしょう。
さらにバッテリーにも優しくなると思います。
ただし、アプリのもっさり感解消のための施策としては、優先度は低いのかなと思います。ちまたに出ているアプリでも、割と真っ赤なアプリが多いですし、赤くとも動作がもっさりしているものは少ない印象です。
やらないよりやった方がマシでしょうが、ここを気にするより、メモリの使用量を抑えるといったチューニングの方が、アプリのパフォーマンスにとって効果が高いような気がします。
Android Performance この話はUDACITYのAndroid Performanceという動画を見て知りました。
英語オンリーかつ字幕すらありませんが、大体雰囲気で分かるんじゃないかなと思います。
Android Performance – UDACITY
「このアプリのデザインを参考にしたいんだけど、どうやって作ってるのか知りたい」というときに便利かもしれないコマンドです。
調べたい画面を表示させた状態で、ターミナルからadb shell dumpsys activity topと入力すると、現在表示中のView階層などが表示されます。
View階層だけを調べたいなら、hierarchyviewerを使った方がグラフィカルに見えて便利なのですが、hierarchyviewerはroot権限がないと起動しないので、実機で調べたい画面を表示して解析することができません。
その点、このadb shell dumpsys activity topはroot権限を必要としないので、実機でちょっと調べたいという時に便利だと思います。
どこからどこまでがActionBarの領域で、どこがコンテンツの領域なのかが非常に分かりづらいのですが、Viewに割り振られているIDも一緒に表示されるのである程度把握できると思います。
このIDが表示されるのを利用して、View階層の中でIDの衝突が起こっていないかなんてことを調べるのにも便利かもしれません。
ちなみにadb shell dumpsys activityと最後のtopを省略すると、Activity Managerの情報がズラズラと表示されます。
Broad castがどうなってるかとか、Content Providerがどんなのが動いているかとか、どんなServiceが動いているかとか、スタックがどうなってるかとかが出力されます。
実機でコマンドを打つだけで調べられるので、手軽で便利だと思います。
カスタムViewを作った場合、BaseSaveStateを拡張してViewの状態をカスタムView自身で復元できるようにできます。
この際に注意すべきことが3点あります。
Activityを保持しないを有効にしてチェックする カスタムViewの復元機能を実装したら、必ず開発者オプションのActivityを保持しないを有効にしてちゃんどう動くかどうか確認しましょう。
自分ではちゃんと実装したつもりでも、これを有効にした状態で画面回転させるとアプリが落ちる場合があります。
フィールド名のタイポに注意 BaseSaveStateを拡張したクラスには、必ずpublic static final Parcelable.Creator<BaseSaveStateを拡張したクラス名> CREATORというフィールドが必要です。
このフィールドの名前はCREATORでなければなりません。
CREATERとタイポすると動きません。動かない上にエラーメッセージはjava.lang.RuntimeException: Unable to start activity ComponentInfo{jp.gcreate.sample.savestatecustomview/jp.gcreate.sample.savestatecustomview.MainActivity2Activity}: java.lang.RuntimeException: Parcel android.os.Parcel@18c09797: Unmarshalling unknown type code 2131296303 at offset 264のように、「フィールド名が違います」と教えてくれません。
writeToParcelで書き出す順番 writeToParcelで書き出す順番とコンストラクタで読み出す順番は同じ順番にしなければなりません。
書き出す順番と読み出す順番が異なるとうまく復元することができません。
順番を同じにすることと一緒に忘れていけないのは、最初にsuperを呼び出すことです。
public ImageState(Parcel source) {
super(source);
savedUri = source.readParcelable(Uri.class.getClassLoader());
}
@Override
public void writeToParcel(@NonNull Parcel dest, int flags) {
super.writeToParcel(dest, flags);
dest.writeParcelable(savedUri, flags);
}
``` コンストラクタで`super(source)`を最初に呼び出す、`writeToParcel`の最初で`super.writeToParcel(dest, flags)`を呼び出すことも忘れてはいけません。 単純なことですが、エラーメッセージからどこが悪いのか把握しづらいので、知らないとドはまりするので注意しましょう。 ## サンプル public class UriImageView extends ImageView{
private Uri mUri;
public UriImageView(Context context, AttributeSet attrs) {
super(context, attrs);
setImage();
}
private void setImage() {
if(mUri == null){
setImageDrawable(getContext().getResources().getDrawable(android.R.drawable.btn_star, getContext().getTheme()));
}else{
setImageURI(mUri);
}
}
public void setUri(Uri uri) {
mUri = uri;
setImage();
}
@Override
protected Parcelable onSaveInstanceState() {
Parcelable superState = super.
前回の続きで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で公開中です。