ですから、キーに対する値はなんらかのオブジェクトですが、実はインテントです。「自分自身をもう一度読み込むか、リストのアクティビティを起動するか」の選択を与えるものです。
リスト3 メソッドgetDataの構造
protected List
|
ApiDemosで起動させたいアクティビティを収集する
getDataで作りたいリストは、アプリで起動させたいアクティビティの名前のリストです。そこで、PackageManagerを用いて、アクションが「MAIN」カテゴリが「SAMPLE_CODE」であるアクティビティの情報を集めます。そのために、AndroidManifest.xmlにおいて、各アクティビティのアクションフィルタを設定しているのです。
「自分のアプリにあるアクティビティの情報を集める」という方法はなく、パッケージマネージャはひたすら「インテント」を頼りに、アクティビティの情報を集めます。
リスト4 カテゴリ名が「CATEGORY_SAMPLE_CODE」であるアクティビティのリストを作る
Intent mainIntent = new Intent(Intent.ACTION_MAIN, null);
mainIntent.addCategory(Intent.CATEGORY_SAMPLE_CODE); //このインテントを頼りにアクティビティの情報を集める
PackageManager pm = getPackageManager();
List list = pm.queryIntentActivities(mainIntent, 0);
|
リスト4で集められるのはアクティビティのリストではなく、各アクティビティの情報をまとめてオブジェクトにしたものです。これは「ResolveInfo」というクラスのオブジェクトです。
アクティビティ(の情報)のリストのひとつずつについてラベル名を得る
メソッドgetDataの後半は繰り返し処理の大きなブロックです。これは、リスト4で得たResolveInfoオブジェクトのリストについての繰り返し処理です。
主な処理はResolveInfoオブジェクトから得た、該当するアクティビティのラベルの処理です。
リスト5 ResolveInfoのリストから要素をひとつとって、対応するアクティビティのラベルを得る
for (int i = 0; i < len; i++) {
ResolveInfo info = list.get(i);
CharSequence labelSeq = info.loadLabel(pm);
....
}
|
ラベルが得られなければ、アクティビティ名で代用する
リスト6は小さな処理ですが、2つ意味があります。リスト5のlabelSeqはまだCharSequenceオブジェクトなので、toStringメソッドで文字列にします。また、もしアクティビティにラベルがなかった場合、「info」に対して「activityInfo.name」でアクティビティ名を文字列として得ます。
リスト6 labelという変数に、ラベル名もしくはアクティビティ名を割り当てる
String label = labelSeq != null
? labelSeq.toString()
: info.activityInfo.name;
|
ラベルを使ってやること
これで変数labelに、たとえば「App/Activity/Animation」のような文字列が渡されました。
この文字列に対して、以下のような処理をします。
(1) | 「/」記号で分解し「App」「Activity」「Animation」にします。それぞれ、リスト表示に使います。 |
(2) | (1)の最後の要素「Animation」の場合は、クリックしたらアクティビティAnimationが起動するようにします。 |
(3) | (2)以前の要素は、クリックしたらアクティビティApiDemosが自身を読み込み直すようにします。そのとき、インテントのデータに表示させる内容を渡します。 |
この方法の大変な点
この方法が難しいのは、アクティビティのリストアップに「パッケージマネージャ」を使っている点です。パッケージマネージャには、細かい検索メソッドは備わっていません。リストをクリックして次のページを読み込む(getDataメソッドが呼ばれる)たびに、パッケージマネージャは、同じカテゴリ名SAMPLE_CODEを持つアクティビティを全部探し出してきます。それらを分類して必要な項目だけを表示する作業は、getDataメソッドの中に自分で書かなければなりません。
階層的なようで微妙に違う
そこで、getDataでは以下のような方法をとっています。たとえば、「App/Activity/Animation」というラベルがあったとしますと、以下のような処理で、リストを表示させています。
| 最初のページでは、「App」を表示させる。 |
| 次のページでは、「App/」の次の語(Activity)を表示させる。 |
| 次のページでは、「App/Activity/」の次の語(Animation)を表示させる。 |
| 「App/Activity/Animation」の次の語、はないので、アクティビティAnimationを起動する。 |
この方法は一見階層的処理に見えますが、実際にプログラムを組んでみると微妙に違います。そこで、ApiDemosのソースコードでは、細かい条件分岐をして、なるべく階層的な処理に仕上げようとしています。しかし、結果として、大変読みにくいプログラムになっていると思います。
注意:listはいつも同じ
これから解説するメソッドgetDataで注意しておくことのひとつは、このメソッドに出てくる変数listの値は、getDataメソッドを何度呼び出しても、常に同じです。それは、同じカテゴリを指定したアクティビティのリストだからです。
注意:同じラベルについてlabelPathはいつも同じ
同様、アクティビティのラベルを表すlabelPathの値も、getDataメソッドを何度呼び出しても、各アクティビティについていつも同じです。
prefixの値を変えながらgetDataを繰り返す
では、何が変わるかというと、getDataの引数です。メソッドの定義ではprefix, onCreateメソッドではpathという変数名に入る値です。これが空、"App", "App/Activity", "App/Activity/Animation"と、getDataを呼ぶごとにだんだん「長くなっていく」ことに注意しましょう。
ApiDemosで用いる二つのインテント
インテントの目的だけ把握しておこう
ApiDemosで、リストをクリックしたときに「自分自身を呼び出し直す(そしてgetDataメソッドを呼ぶ)」動作と、「アクティビティを起動する」動作は、異なるインテントを戻す以下のメソッドで区別します。
browseIntent | prefix(path)の値を渡して、自分自身を呼び出し直す。リストで「App」をクリックしたら「Activity」が乗ったリストが出てくる動作 |
activityIntent | 対応するアクティビティを起動する。リストで「Animation」をクリックしたら、アクティビティ「Animation」が起動する動作 |
これらのインテントを用いて実際にアクティビティを起動する作業は、サンプルのオリジナルのコードをご覧ください。
このページでは、「browseIntent」が出てきたら「クリックして次のリストへ行くんだな」「activityIntent」が出てきたら「アクティビティが起動するんだな」と御想像ください。
具体的なラベルを例に用いたgetDataメソッドの説明
途中まで同じ項目の二つのラベル
これからgetDataメソッドの流れを考えるにあたり、以下のラベルを持つ二つのアクティビティの情報があると考えましょう。これらは、パッケージマネージャで読み込まれ、getDataメソッド中の配列listに要素として渡されているとします。
App/Activity/Animation |
App/Activity/ListActivity/MyList |
パッケージマネージャはgetDataメソッドが呼ばれるたびにこの二つを読み込んでくることになります。
ゆえに、listの値はいつも同じで、変数lenの値は常に2です。そして、getData中のリスト7の繰り返しは常に「0回目」と「1回目」の2回になります。
リスト7 getData中の繰り返し処理の部分
for (int i = 0; i < len; i++) {
ResolveInfo info = list.get(i);
//....大変長い処理
}
|
ApiDemosを起動したとき:forループ0回目
最初ですから、prefixは空の文字列です。そこで、prefixPathもnullです。
リスト7に示したforループの0回目には、変数labelの値は"App/Activity/Animation"です。
prefixが空なので、prefixWithSlashも空です。
するとprefixWithSlashの長さは0です。
そこで、リスト8のif文が真となります。実は、これが真でなければ、getDataの処理は全くなされません。
リスト8 forループ中の最初のif文
if (prefixWithSlash.length() == 0 || label.startsWith(prefixWithSlash)) {
....
}
|
labelPathの内容は["App","Activity","Animation"]になります。
prefixPathはnullですから、リスト9の条件文は、リスト10と同じになります。
リスト9 prefixPathがnullかどうかで、nextLabelの値が異なる
String nextLabel = prefixPath == null ? labelPath[0] : labelPath[prefixPath.length];
|
リスト10 prefixPath==nullなので
String nextLabel =labelPath[0];
|
すなわち、nextLabelの値は"App" になります。
また、prefixPathはnullですから、リスト11のif判定は、リスト12の判定と同じになります。
リスト11 if文の判定内容が条件によって違ってくる
if ((prefixPath != null ? prefixPath.length : 0) == labelPath.length-1)
|
リスト12 「prefixPath != null」でないので
if (0 == labelPath.length-1)
|
リスト12にはさらに、具体的な値を入れられます。すなわち、labelPath.length=3ですから、条件は「if(0==2)」となります。
これは、偽ですから、リスト13のelseに進みます。
リスト13 リスト11の判定とelseの関係
if ((prefixPath != null ? prefixPath.length : 0) == labelPath.length-1) {
.......
} else {
if (entries.get(nextLabel) == null) {
.....
}
}
|
リスト13では、elseでまた条件分岐があります。
if文に出てくる「entries」はハッシュマップですが、まだ「空」ですから、キー「nextLabel」の値はありません。そのため、判定は真になります。
そこで、リスト14の作業をします。
メソッドaddItemの最後の引数はメソッドbrowseIntentの戻り値ですが、browseIntent自身がとる引数が、また条件文になっています。
リスト14 elseの中身
} else {
if (entries.get(nextLabel) == null) {
addItem(myData, nextLabel, browseIntent(prefix.equals("") ? nextLabel : prefix + "/" + nextLabel));
entries.put(nextLabel, true);
}
}
|
リスト15 メソッドbrowseIntentのとる引数が条件文
browseIntent(prefix.equals("") ? nextLabel : prefix + "/" + nextLabel));
|
リスト15で prefixは空の文字列ですから、browseIntentの引数の値はnextLabelになります。すなわち"App"です。
ゆえに、リスト14 に示したaddItemメソッドにより、myDataに以下のように、「2つのキー・値の組み合わせを持つハッシュマップ」がひとつ入ります。
| キー | 値 |
マップその1 | "title" | "App" |
マップその2 | "intent" | browseIntent("App") |
これは、「Appと表示し、クリックすると、メソッドgetData("App")を実行する欄」をひとつ表示する意味になります。
ApiDemosを最初に起動したとき:forループ1回目
forループの1回目では、labelの値は「App/Activity/Secure Surface/MyList」です。
prefixはforループの回数に寄らず同じですから、条件分岐は最後のひとつを除いて全てforループ0回目と同じになります。
ゆえに、nextLabelの値は"App"になります。
すると、先のリスト14に示した条件分岐が違ってきます。
リスト16 条件分岐だけ考えると、forループの0回目と、ここだけが違う
if (entries.get(nextLabel) == null)
|
なぜなら ハッシュマップ entriesには、キーが"App", 値がtrueの組み合わせが一つ入っていますから、nullであるという過程は偽になります。
そこで、このあとの作業(リスト15)はスキップされます。
ApiDemosを最初に起動したとき:起動完了時の状態
以上のように、ループが終わった時、残るのは
「Appと表示し、クリックすると、メソッドgetData("App")を実行する欄」をひとつ表示する欄を表示する
という情報のみということになります。
これをクリックすると、今度はprefix = "App"で getDataメソッドが呼ばれます。
図4 ApiDemos起動時、forループ終了時
「App」をクリックしたとき:forループ0回目
図4の状態で「App」をクリックし、"App"を引数にgetDataが呼ばれたらどうなるかを考えます。
まず、listの内容は変わりません。list.sizeは2です。
各ループについて、labelPathのも変わりません。
しかし、prefixが違います。prefix="App"ですから、prefixPathは要素が1個の["App"]になります。
そして、prefixWithSlash="App/"になります。
0回目のループで、リスト8は、prefixWithSlash.length()==0 が今度は偽になります。
しかし、ラベルは"App/"で始まりますので、条件全体はやはり真になります。そこで、リスト9以下の条件を判断していきます。
リスト9では、prefixPathがnullではありませんから、nextLabelの値はリスト17のように書かれます。
リスト17 「prefixPath == null」が偽なので
String nextLabel = labelPath[prefixPath.length];
|
nextLabelは、labelPathの「prefixPath.length番目の要素」になります。
つまり、0から数えて「1」番目の要素ですから、nextLabel = "Activity"になります。
リスト11はif文の判定条件そのものが条件によって変わる形ですが、今度はprefixPath != nullが真になりますから、リスト18のようなiif文になります。
リスト18 「prefixPath != null」が真なので
if(prefixPath.length == labelPath.length-1)
|
リスト18の判定で、前者は1で後者は3ですから、偽になります。そこで、リスト14のelseに行きます。
0回目のループですから、entriesはnullです。そこで、brouseIntentが呼ばれます。
今度は、browseIntentの引数が、"App"+"/"+"Activity"となります。
「App」をクリックしたとき:forループ1回目
1回目のループでは、nextLabelの値が、ループ0回目と同じ"Activity"になりますから、entriesにすでに値が入っていることになり、処理はなされません。
「App」をクリックしたとき:ページ切り替え完了時の状態
以上、「App」をクリックしたとき切り替わる画面には、
「Activityと表示し、クリックすると、メソッドgetData("App/Activity")を実行する欄」をひとつ表示する
という処理がなされます。
図5 「App」をクリックして画面の切り替わりが完了したとき
「Activity」をクリックしたとき:forループ0回目
図5の状態でクリックすると、prefix = "App/Activity"でgetDataが呼ばれます。
今度は、prefixPath=["App","Activity"] と要素が2つです。
ラベルはprefixWithSlash="App/Activity/"で始まりますので、やはり条件全体が真になります。
labelPathは変わらず["App","Activity","Animation"]です。
prefixPath.length = 2になりますので、nextLabel =labelPath[2], すなわち"Animation"となります。
リスト13のif文は起動時以外は「if(prefixPath.length == labelPath.length-1)」になりますが、前者が2, 後者が3ですから、2==3-1で真になります。
これは、nextLabelがラベルの最後の項すなわち起動すべきアクティビティ名にまで来たことを示します。
そこで、今度は、リスト20のように、メソッドactivityIntentが呼ばれます。
リスト20 ifが真となり、activityIntentが呼ばれる
if ((prefixPath != null ? prefixPath.length : 0) == labelPath.length - 1) {
addItem(myData, nextLabel, activityIntent(
info.activityInfo.applicationInfo.packageName,
info.activityInfo.name));
} else {
....//今まではbrowseIntentが呼ばれていた
|
これは、"Animation"という欄を表示し、それをクリックしたらアクティビティAnimationが起動する欄を表示するということです。
もうひとつ大事なのは、entriesがnullのままで置かれるということです。
「Activity」をクリックしたとき:forループ1回目
一方、1回目のループでは、まだリスト13のif文が真になりません。
なぜなら、prefixPathはループが0回目でも1回目でも同じですから、変わらず2ですが、labelPath.length=4です。
つまり、"App/Activity/ListActivity/Secure/Window"のほうはまだ、nextLabelとして"ListActivity"が得られます。
そこで、リスト14のelse以下を行います。0回目のループで「entries」がnullのままだったので、browseIntentが呼ばれる処理になります。
「Activity」をクリックしたとき: ページの切り替え完了後
この過程で初めて、リストに欄が二つ表示されます。すなわち、「クリックすると起動する"Animation"欄」の下に、「クリックするとgetDataが呼ばれる"ListActivity"」という欄が加わります。
図6 「Actiity」をクリックして画面の切り替わりが完了したとき
「ListActivity」をクリックしたとき
さて、図6で"ListActivity"をクリックしたらどうなるでしょうか?
実は、やっぱり、「App/Activity/Animation」「App/Activity/ListActivity/MyList」と二つともラベルが読み込まれます。
しかし、今度はprefixが"App/Activity/ListActivity"です。とすると、0回目のループではリスト8が偽になりますから、「何も処理せず終わり、1回目のループに処理が移ります。
1回目のループでは、nextLabelは"MyList"になります。
そして、とうとう「prefixPath.length == labelPath.length-1」が真になりますので、activityIntentメソッドが呼ばれて「MyListと表示され、クリックすると起動」する欄が追加されることになります。
図7 「ListActivity」をクリックしたとき
以上がApiDemosのリスト表示のしくみです。ずいぶんと面倒なことをやっていますね。
目次へ
清水美樹の本 トップページへ